Skip to main content

structpages

import "github.com/jackielii/structpages"

Package structpages provides a way to define routing using struct tags and methods. It integrates with the [http.ServeMux], allowing you to quickly build up pages and components without too much boilerplate.

Index

Variables

ErrSkipPageRender is a sentinel error that can be returned from a Props method to indicate that the page rendering should be skipped. This is useful for implementing conditional rendering or redirects within page logic.

var ErrSkipPageRender = errors.New("skip page render")

func ID

func ID(ctx context.Context, v any) (string, error)

ID generates a raw HTML ID for a component method (without "#" prefix). Use this for HTML id attributes.

Parameters:

  • ctx: Context containing parseContext (required for method expressions and Ref)
  • v: One of:
  • Method expression (p.UserList) - generates ID from page and method name
  • Ref type (structpages.Ref("PageName.MethodName")) - looks up page/method dynamically
  • Plain string ("my-custom-id") - returned as-is

Example:

<div id={ structpages.ID(ctx, p.UserList) }>
// → <div id="team-management-view-user-list">

<div id={ structpages.ID(ctx, UserStatsWidget) }>
// → <div id="user-stats-widget"> (no page prefix for standalone functions)

<div id={ structpages.ID(ctx, "my-custom-id") }>
// → <div id="my-custom-id">

Returns an error if parseContext is not found in the provided context.

func IDTarget

func IDTarget(ctx context.Context, v any) (string, error)

IDTarget generates a CSS selector (with "#" prefix) for a component method. Use this for HTMX hx-target attributes.

Parameters:

  • ctx: Context containing parseContext (required for method expressions and Ref)
  • v: One of:
  • Method expression (p.UserList) - generates selector from page and method name
  • Ref type (structpages.Ref("PageName.MethodName")) - looks up page/method dynamically
  • string ("body" or "#my-custom-id") - returned as-is

Example:

<button hx-target={ structpages.IDTarget(ctx, p.UserList) }>
// → <button hx-target="#team-management-view-user-list">

<button hx-target={ structpages.IDTarget(ctx, UserStatsWidget) }>
// → <button hx-target="#user-stats-widget"> (no page prefix for standalone functions)

<button hx-target={ structpages.IDTarget(ctx, "body") }>
// → <button hx-target="body">

Returns an error if parseContext is not found in the provided context.

func RenderComponent

func RenderComponent(targetOrMethod any, args ...any) error

RenderComponent creates an error that instructs the framework to render a specific component instead of the default component.

It supports multiple patterns:

  1. Direct component:
comp := MyComponent("data")
return RenderComponent(comp)
  1. Custom RenderTarget with Component() method (for custom TargetSelector implementations):
type customTarget struct { data string }
func (ct customTarget) Is(method any) bool { ... }
func (ct customTarget) Component() component { return MyComponent(ct.data) }
// Custom TargetSelector returns customTarget
// Props can then: return Props{}, RenderComponent(target)
  1. Same-page component (with target from Props):
func (p DashboardPage) Props(r *http.Request, target RenderTarget) (DashboardProps, error) {
if target.Is(UserStatsWidget) {
stats := loadUserStats()
return DashboardProps{}, RenderComponent(target, stats)
}
}
  1. Cross-page component (with method expression):
func (p MyPage) Props(r *http.Request) (Props, error) {
return Props{}, RenderComponent(OtherPage.ErrorComponent, "error message")
}

func URLFor

func URLFor(ctx context.Context, page any, args ...any) (string, error)

URLFor returns the URL for a given page type. If args is provided, it'll replace the path segments. Supported format is similar to http.ServeMux.

The page argument accepts:

  • a typed page value: URLFor(ctx, Page{}, ...)
  • a top-level string (sugar for Ref): URLFor(ctx, "Admin.Settings", ...) resolves the same as URLFor(ctx, Ref("Admin.Settings"), ...). Useful at call sites that can't import the target page (cross-package cycle).
  • a Ref: URLFor(ctx, Ref("PageName"), ...)
  • a []any composition (chain + optional URL fragments): see below.
  • a func(*PageNode) bool predicate for unusual lookups.

Type-based lookup is strict: if the same page type is mounted under multiple parents, URLFor returns an error listing every match instead of silently choosing one. Disambiguate with the []any chain form (recommended; type-safe), Ref("Parent.Field") for cross-package callers, or a func(*PageNode) bool predicate.

Path and query parameters are passed via a map[string]any (preferred — explicit and resilient to route changes):

URLFor(ctx, Page{}, map[string]any{"id": 42})
URLFor(ctx, []any{Page{}, "?foo={bar}"}, map[string]any{"bar": "baz"})

Positional and key/value-pairs forms also work (see formatPathSegments in url_for.go for the full detection order) but require the call site to track parameter position or name conventions.

You can pass []any as the page to join multiple path segments together — strings are concatenated as-is, which is the form used to append a query-string template to a typed page lookup. You can also pass a func(*PageNode) bool predicate to match a specific page when type-based lookup isn't enough.

func WithArgs

func WithArgs(args ...any) func(*StructPages)

WithArgs adds global dependency injection arguments that will be available to all page methods (Props, Middlewares, ServeHTTP etc.).

func WithErrorHandler

func WithErrorHandler(onError func(http.ResponseWriter, *http.Request, error)) func(*StructPages)

WithErrorHandler sets a custom error handler function that will be called when an error occurs during page rendering or request handling. If not set, a default handler returns a generic "Internal Server Error" response.

func WithMiddlewares

func WithMiddlewares(middlewares ...MiddlewareFunc) func(*StructPages)

WithMiddlewares adds global middleware functions that will be applied to all routes. Middleware is executed in the order provided, with the first middleware being the outermost handler. These global middlewares run before any page-specific middlewares.

func WithTargetSelector

func WithTargetSelector(selector TargetSelector) func(*StructPages)

WithTargetSelector sets a custom TargetSelector function that determines which component to render based on the request. The default is HTMXRenderTarget, which handles HTMX partial requests automatically.

The selector function receives the request and page node, and returns a RenderTarget that will be passed to Props. This allows custom logic for component selection.

Example - Custom selector with A/B testing:

sp := Mount(http.NewServeMux(), index{}, "/", "My App",
WithTargetSelector(func(r *http.Request, pn *PageNode) (RenderTarget, error) {
if getABTestVariant(r) == "B" {
// Use different component for variant B
method := pn.Components["ContentB"]
return newMethodRenderTarget("ContentB", method), nil
}
// Fallback to default HTMX behavior
return HTMXRenderTarget(r, pn)
}))

func WithURLPrefix

func WithURLPrefix(prefix string) func(*StructPages)

WithURLPrefix tells structpages that it is being served behind a path prefix that has been stripped before requests reach the registered routes (for example by http.StripPrefix or an upstream reverse proxy). The prefix is prepended to every URL returned by URLFor — both the *StructPages.URLFor method and the request-context URLFor function — so generated URLs point at the externally visible path.

This option only affects URL generation. Route registration is still controlled by the route argument to Mount.

Example: app registered at "/" internally, served at "/admin" externally:

inner := http.NewServeMux()
sp, _ := structpages.Mount(inner, pages{}, "/", "App",
structpages.WithURLPrefix("/admin"))
outer := http.NewServeMux()
outer.Handle("/admin/", http.StripPrefix("/admin", inner))
// sp.URLFor(home{}) now returns "/admin"

func WithWarnEmptyRoute

func WithWarnEmptyRoute(warnFunc func(*PageNode)) func(*StructPages)

WithWarnEmptyRoute sets a custom warning function for pages that have neither a handler method nor children. These pages are automatically skipped during route registration. If warnFunc is nil, a default warning message is printed to stdout. Set warnFunc to a no-op function to suppress warnings entirely.

Example usage:

// Use default warning (prints to stdout)
sp := structpages.Mount(
http.NewServeMux(), index{}, "/", "App",
structpages.WithWarnEmptyRoute(nil),
)

// Custom warning function
sp := structpages.Mount(
http.NewServeMux(), index{}, "/", "App",
structpages.WithWarnEmptyRoute(func(pn *PageNode) {
log.Printf("Skipping empty page: %s", pn.Name)
}),
)

// Suppress warnings entirely
sp := structpages.Mount(
http.NewServeMux(), index{}, "/", "App",
structpages.WithWarnEmptyRoute(func(*PageNode) {}),
)

type MiddlewareFunc

MiddlewareFunc is a function that wraps an http.Handler with additional functionality. It receives both the handler to wrap and the PageNode being handled, allowing middleware to access page metadata like route, title, and other properties.

type MiddlewareFunc func(http.Handler, *PageNode) http.Handler

type Mux

Mux represents any HTTP router that can register handlers using the Handle method. This interface is satisfied by http.ServeMux and must follow the same pattern support for route registration.

type Mux interface {
Handle(pattern string, handler http.Handler)
}

type Option

Option represents a configuration option for StructPages.

type Option func(*StructPages)

type PageNode

PageNode represents a page in the routing tree. It contains metadata about the page including its route, title, and registered methods. PageNodes form a tree structure with parent-child relationships representing nested routes.

type PageNode struct {
Name string
Title string
Method string
Route string

Value reflect.Value
Props map[string]reflect.Method
Components map[string]reflect.Method
Middlewares *reflect.Method
Parent *PageNode
Children []*PageNode
// contains filtered or unexported fields
}

func (*PageNode) All

func (pn *PageNode) All() iter.Seq[*PageNode]

All returns an iterator that walks through this PageNode and all its descendants in depth-first order. This is useful for traversing the entire page tree.

Example:

for node := range pageNode.All() {
fmt.Println(node.FullRoute())
}

func (*PageNode) FullRoute

func (pn *PageNode) FullRoute() string

FullRoute returns the complete route path for this page node, including all parent routes. For example, if a parent has route "/admin" and this node has route "/users", FullRoute returns "/admin/users".

func (PageNode) String

func (pn PageNode) String() string

String returns a human-readable representation of the PageNode, useful for debugging. It includes all properties and recursively formats child nodes with proper indentation.

type Ref

Ref represents a dynamic reference to a page or method by name. Use it when static type references aren't available (e.g., configuration-driven menus, generic components, or code generation scenarios).

For URLFor, the string can be:

  • Page name: Ref("UserManagement")
  • Route path: Ref("/user/management") - must start with /

For IDFor, the string can be:

  • Qualified method: Ref("PageName.MethodName")
  • Simple method: Ref("MethodName") - must be unambiguous across all pages

Both URLFor and IDFor return descriptive errors if the reference is invalid, providing runtime safety for dynamic references.

Example usage:

// Dynamic menu from configuration
menuItems := []struct{ Page Ref; Label string }{
{Ref("HomePage"), "Home"},
{Ref("UserManagement"), "Users"},
}
for _, item := range menuItems {
url, err := URLFor(ctx, item.Page)
// Handle error if page doesn't exist
}

// Dynamic component reference
targetID, err := IDFor(ctx, Ref("UserManagement.UserList"))
type Ref string

type RenderTarget

RenderTarget represents a selected component that will be rendered. It's available to Props methods via dependency injection, allowing Props to load only the data needed for the target component.

RenderTarget is produced by a TargetSelector function (e.g., HTMXRenderTarget). The selector determines which component to render based on the request, and the resulting RenderTarget is passed to Props.

Example usage in Props:

func (p DashboardPage) Props(r *http.Request, target RenderTarget) (DashboardProps, error) {
switch {
case target.Is(UserStatsWidget):
stats := loadUserStats()
return DashboardProps{}, RenderComponent(target, stats)
case target.Is(p.Page):
return DashboardProps{Stats: loadAll()}, nil
}
}
type RenderTarget interface {
// Is checks if this target matches the given method or function reference.
// Works with both page methods and standalone functions.
// Uses method/function expressions for compile-time safety.
//
// For function components, Is() has a side effect: it stores the function
// value when a match is found, enabling lazy evaluation of the hxTarget.
Is(method any) bool
}

func HTMXRenderTarget

func HTMXRenderTarget(r *http.Request, pn *PageNode) (RenderTarget, error)

HTMXRenderTarget is the default TargetSelector for HTMX integration. It automatically selects the appropriate component based on the HX-Target header.

When an HTMX request is detected (via HX-Request header), it matches the HX-Target value against all available component IDs. For example:

  • HX-Target: "content" -> returns methodRenderTarget for Content() method
  • HX-Target: "index-page-todo-list" -> returns methodRenderTarget for TodoList() method
  • HX-Target: "user-stats-widget" (no method match) -> returns functionRenderTarget for lazy evaluation
  • No HX-Target or non-HTMX request -> returns methodRenderTarget for Page() method

This selector works with htmx 1.x and 2.x, where HX-Target carries the bare element id. For htmx 4, use HTMXv4RenderTarget instead.

This is the default TargetSelector for StructPages, making IDFor work seamlessly with HTMX out of the box.

func HTMXv4RenderTarget

func HTMXv4RenderTarget(r *http.Request, pn *PageNode) (RenderTarget, error)

HTMXv4RenderTarget is the htmx 4 variant of HTMXRenderTarget.

htmx 4 reshaped two request headers we care about:

  • HX-Target now carries "<tag>#<id>" (or just "<tag>" for unidentified elements) instead of a bare id.
  • HX-Request-Type was added: "full" when the swap target is <body> or hx-select is in play, "partial" otherwise. The server is expected to honor it.

This selector treats HX-Request-Type=full as a hard hint to render the Page component. Otherwise it picks the matching key from HX-Target — preferring the id portion of "<tag>#<id>" when present, falling back to the tag for id-less targets — and applies the same matching rules as HTMXRenderTarget. A component named e.g. Form matches both `hx-target="#some-form"` (sent as "form#some-form") and `hx-target="form"` (sent as "form").

HX-Source (htmx 4's HX-Trigger replacement) identifies the trigger element rather than the swap target, so it is not used for component routing. Read it from the request directly if you need it.

See https://four.htmx.org/reference/#headers.

Wire it via WithTargetSelector when serving an htmx 4 frontend:

sp, err := structpages.Mount(mux, root{}, "/", "App",
structpages.WithTargetSelector(structpages.HTMXv4RenderTarget))

type StructPages

StructPages holds the parsed page tree context for URL generation. It is returned by Mount and provides URLFor and IDFor methods.

type StructPages struct {
// contains filtered or unexported fields
}

func Mount

func Mount(mux Mux, page any, route, title string, options ...Option) (*StructPages, error)

Mount parses the page tree and registers all routes onto the provided mux. If mux is nil, routes are registered on http.DefaultServeMux. Returns a StructPages that provides URLFor and IDFor methods.

Parameters:

  • mux: Any router satisfying the Mux interface (e.g., http.ServeMux). If nil, uses http.DefaultServeMux.
  • page: A struct instance with route-tagged fields
  • route: The base route path for this page tree (e.g., "/" or "/admin")
  • title: The title for the root page
  • options: Optional configuration (WithErrorHandler, WithMiddlewares, etc.) and dependency injection args

Example with custom mux:

mux := http.NewServeMux()
sp, err := structpages.Mount(mux, index{}, "/", "My App",
structpages.WithErrorHandler(customHandler))
sp.URLFor(index.Page)
http.ListenAndServe(":8080", mux)

Example with DefaultServeMux:

sp, err := structpages.Mount(nil, index{}, "/", "My App")
http.ListenAndServe(":8080", nil)

func Parse

func Parse(page any, route, title string, options ...Option) (*StructPages, error)

Parse builds a page tree from page without registering any HTTP routes. The returned *StructPages exposes URLFor, ID, IDTarget, and PageContext, but never touches a mux.

Use Parse for tests and tooling that need URL/ID resolution against the real page tree but don't want to spin up a server. For production use, prefer Mount, which performs the same parse step and additionally registers handlers.

Options behave identically to Mount: WithArgs, WithURLPrefix, WithErrorHandler, etc. all apply. WithMiddlewares and other mux-affecting options are accepted but never observed (no handlers are wired).

Example (test):

sp, err := structpages.Parse(webPages{}, "/", "App")
if err != nil { t.Fatal(err) }
ctx := sp.PageContext(context.Background())
body := renderTo(ctx, List{}.Page(props))

func (*StructPages) ID

func (sp *StructPages) ID(v any) (string, error)

ID generates a raw HTML ID for a component method (without "#" prefix). Use this for HTML id attributes. It works without context by using the structpages's parseContext directly.

Example:

sp.ID(p.UserList)
// → "team-management-view-user-list"

sp.ID(UserStatsWidget)
// → "user-stats-widget" (no page prefix for standalone functions)

func (*StructPages) IDTarget

func (sp *StructPages) IDTarget(v any) (string, error)

IDTarget generates a CSS selector (with "#" prefix) for a component method. Use this for HTMX hx-target attributes. It works without context by using the structpages's parseContext directly.

Example:

sp.IDTarget(p.UserList)
// → "#team-management-view-user-list"

sp.IDTarget(UserStatsWidget)
// → "#user-stats-widget" (no page prefix for standalone functions)

func (*StructPages) PageContext

func (sp *StructPages) PageContext(ctx context.Context) context.Context

PageContext returns a context derived from ctx with sp's page tree attached. URLFor, ID, and IDTarget invoked with the returned context resolve against sp instead of failing with "parse context not found in context".

Use this in tests that render templ components with context.Background(): wrap the bare context once via PageContext and the renders behave as if they ran under a real request.

sp, _ := structpages.Parse(webPages{}, "/", "App")
ctx := sp.PageContext(context.Background())
html := mustRender(ctx, MyPage{}.Page(props))

func (*StructPages) URLFor

func (sp *StructPages) URLFor(page any, args ...any) (string, error)

URLFor returns the URL for a given page type. If args is provided, it'll replace the path segments. Supported format is similar to http.ServeMux.

Unlike the context-based URLFor function, this method doesn't have access to pre-extracted URL parameters from the current request, so all required parameters must be provided as args.

Type-based lookup is strict: if the same page type is mounted under multiple parents, URLFor returns an error listing every match instead of silently choosing one. Disambiguate with the []any chain form (recommended; type-safe), Ref("Parent.Field") for cross-package callers, or a func(*PageNode) bool predicate.

Path and query parameters are passed via a map[string]any (preferred — explicit and resilient to route changes):

sp.URLFor(Page{}, map[string]any{"id": 42})
sp.URLFor([]any{Page{}, "?foo={bar}"}, map[string]any{"bar": "baz"})

Positional and key/value-pairs forms also work (see formatPathSegments in url_for.go for the full detection order) but require the call site to track parameter position or name conventions.

You can pass []any as the page to join multiple path segments together — strings are concatenated as-is. You can also pass a func(*PageNode) bool predicate to match a specific page when type-based lookup isn't enough.

type TargetSelector

TargetSelector determines which component to render for a request. It returns a RenderTarget that will be passed to Props.

The default selector is HTMXRenderTarget, which handles HTMX partial requests. Custom selectors can be provided via WithTargetSelector option.

type TargetSelector func(r *http.Request, pn *PageNode) (RenderTarget, error)

Generated by gomarkdoc