From structpages
Guides building Go web apps with structpages framework: struct-based routing, templ templates, HTMX partial rendering, Props methods, URL generation, Render patterns, and middleware.
npx claudepluginhub jackielii/structpagesThis skill uses the workspace's default tool permissions.
structpages provides struct-based routing for Go web apps, integrating with `http.ServeMux`, templ templating, and HTMX.
Provides Go web server architecture with net/http 1.22+ routing, project structure patterns, graceful shutdown, and dependency injection. Use for building Go apps, layouts, and dependencies.
Provides idiomatic Go patterns for backend APIs with Gin, Echo, Fiber: standard project structure, custom error handling, handler dependency injection, concurrency best practices.
Share bugs, ideas, or general feedback.
structpages provides struct-based routing for Go web apps, integrating with http.ServeMux, templ templating, and HTMX.
For detailed API docs, see reference.md. For real-world patterns and examples, see examples.md.
Routes are struct fields with route: tags. Format: route:"[METHOD] /path [Title]"
type pages struct {
home `route:"/{$} Home"` // exact root match
about `route:"/about About"` // all methods (default)
create `route:"POST /create Create"` // POST only
detail `route:"/item/{id} Item"` // path parameter
files `route:"/files/{path...} Files"` // wildcard
}
If no method is given, the route accepts all methods (internally stored as "ALL").
Nesting creates URL hierarchies:
type pages struct {
admin adminPages `route:"/admin Admin"`
}
type adminPages struct {
dashboard `route:"/{$} Dashboard"` // -> /admin/
users `route:"/users Users"` // -> /admin/users
}
There are three main patterns — choose based on what the page does.
Pattern A: Props + Page/Content (renders HTML)
type MyPage struct{}
type MyPageProps struct {
Items []Item
}
// Props fetches data. Parameters are type-matched via DI.
func (p MyPage) Props(r *http.Request, appCtx *AppContext) (MyPageProps, error) {
items, err := appCtx.Store.GetItems(r.Context())
if err != nil {
return MyPageProps{}, err
}
return MyPageProps{Items: items}, nil
}
// Page wraps in layout (used for full page loads — non-HTMX, or HTMX with no matching target)
templ (p MyPage) Page(props MyPageProps) {
@AppShellLayout() {
@p.Content(props)
}
}
// Content renders the body (used by convention for HTMX partials targeting "#…content")
templ (p MyPage) Content(props MyPageProps) {
<div>...</div>
}
Pattern B: ServeHTTP that writes, then re-renders a sibling component (most common HTMX form action)
type AddTodo struct{}
func (a AddTodo) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
text := r.FormValue("text")
if text != "" {
store.Add(text)
}
// Render the sibling page's TodoList component as the response
return structpages.RenderComponent(Index.TodoList)
}
RenderComponent(SomePage.SomeMethod) resolves to that page's component and renders it. This is the canonical pattern for POST/DELETE handlers that update state and return a refreshed partial.
Pattern C: ServeHTTP for redirects (no HTML response)
type SubmitForm struct{}
func (p SubmitForm) ServeHTTP(w http.ResponseWriter, r *http.Request, appCtx *AppContext) error {
// perform action...
http.Redirect(w, r, "/somewhere", http.StatusSeeOther)
return nil
}
ServeHTTP supports four signatures (see reference.md for details). The DI form (extra arg types beyond w, r) buffers the response only when the method has a return value.
structpages.URLFor(ctx, page, args...) returns (string, error). In templ, attribute values can take (string, error) directly:
<a href={ structpages.URLFor(ctx, MyPage{}) }>Link</a>
<a href={ structpages.URLFor(ctx, DetailPage{}, item.ID) }>Detail</a>
<form action={ structpages.URLFor(ctx, SavePage{}, item.ID) } method="POST">
For appending query strings, pass a []any of segments:
url, err := structpages.URLFor(ctx,
[]any{MyList{}, "?page={page}&q={q}"},
"page", pageNum, "q", query,
)
When you need a plain string (not in a templ attribute that handles errors), wrap with a small must helper:
func must[T any](v T, err error) T {
if err != nil { panic(err) }
return v
}
myURL := must(structpages.URLFor(ctx, MyPage{}))
Optional convenience wrappers. Some apps define short local wrappers like urlFor, idFor, idForTarget — e.g. to return templ.SafeURL or to shorten the package qualifier. These are app-level conveniences, not framework functions:
// in your app — purely optional
func urlFor(ctx context.Context, page any, args ...any) (templ.SafeURL, error) {
s, err := structpages.URLFor(ctx, page, args...)
return templ.SafeURL(s), err
}
func idFor(ctx context.Context, v any) (string, error) { return structpages.ID(ctx, v) }
func idForTarget(ctx context.Context, v any) (string, error) { return structpages.IDTarget(ctx, v) }
The rest of this guide uses the framework names (structpages.URLFor, structpages.ID, structpages.IDTarget) directly.
All HTMX requests for a page go to the SAME route. structpages picks which component to render from the HX-Target header by matching element IDs against component method names.
structpages.ID / structpages.IDTarget generate deterministic element IDs from method references (ID returns "my-page-user-list"; IDTarget returns "#my-page-user-list"). For plain string arguments both functions return the string unchanged — IDTarget("body") is "body", not "#body".
// Set element ID on the component's wrapper
<div id={ structpages.ID(ctx, MyPage.UserList) }>
@p.UserList(props.Users)
</div>
// HTMX targeting
<input hx-get={ structpages.URLFor(ctx, MyPage{}) }
hx-target={ structpages.IDTarget(ctx, MyPage.UserList) }
hx-swap="outerHTML" />
For pages with multiple HTMX-updatable sections, inject RenderTarget into Props to load only the data each section needs:
func (p MyPage) Props(r *http.Request, appCtx *AppContext, sel structpages.RenderTarget) (MyPageProps, error) {
switch {
case sel.Is(MyPage.UserList):
users, err := p.userListData(r, appCtx)
if err != nil { return MyPageProps{}, err }
return MyPageProps{}, structpages.RenderComponent(MyPage.UserList, users)
case sel.Is(MyPage.GroupList):
groups, err := p.groupListData(r, appCtx)
if err != nil { return MyPageProps{}, err }
return MyPageProps{}, structpages.RenderComponent(MyPage.GroupList, groups)
case sel.Is(MyPage.Page), sel.Is(MyPage.Content):
// Full page — load everything
return MyPageProps{Users: …, Groups: …}, nil
}
return MyPageProps{}, nil
}
Note: only methods named Props are auto-invoked. *Props-suffixed helpers (e.g. userListData above; some codebases call them UserListProps) are just regular methods the user calls from inside Props — there's no priority resolution.
RenderComponent in ServeHTTP for write+rerender flows:
func (p MyDelete) ServeHTTP(w http.ResponseWriter, r *http.Request, appCtx *AppContext) error {
if err := store.Delete(...); err != nil { return err }
items, _ := loadItems(r, appCtx)
return structpages.RenderComponent(MyPage.ItemList, items)
}
For function targets specifically (standalone templ funcs like UserStatsWidget), target.Is(fn) must be called before RenderComponent(target, args...) — Is() stores the function pointer for later rendering. For method targets, Is() is the recommended pattern but not strictly required (the method is captured at construction).
Global middleware via WithMiddlewares. Page-specific via Middlewares() method (also applies to all descendants):
func (p ProtectedPages) Middlewares(appCtx *AppContext) []structpages.MiddlewareFunc {
return []structpages.MiddlewareFunc{RequireAuth(appCtx)}
}
MiddlewareFunc signature: func(http.Handler, *PageNode) http.Handler — receives the PageNode so middleware can inspect route metadata.
Register deps via WithArgs. They're matched by type into method parameters:
sp, err := structpages.Mount(mux, TopPages{}, "/", "App",
structpages.WithArgs(appCtx),
)
// Now any Props/ServeHTTP/Middlewares/Init method can receive *AppContext
func (p MyPage) Props(r *http.Request, appCtx *AppContext) (Props, error) { ... }
Each registered type appears once. The matcher coerces between pointer and value forms and falls back to assignability, so a single *AppContext registration also satisfies parameters of any interface it implements. To register two values of the same underlying type, define named types to disambiguate.
Generic types and interface types are supported as well — see generics_injection_test.go for the tested matrix (pointer semantics, interface injection, slices/maps, complex constraints, type parameters).
*PageNode is always available for injection (the framework adds the current node automatically).
r.PathValue("param"), not function arguments.structpages.URLFor.RenderComponent is returned as an error — when returned, the Props return values (other than the error) are ignored.target.Is(fn) is required before RenderComponent(target, args...) for function targets; recommended for method targets.ErrSkipPageRender is only honored from Props (e.g. after writing a redirect). Returning it from ServeHTTP does nothing special.structpages.Ref("FieldName") to disambiguate.ID and IDTarget unchanged — IDTarget("body") returns "body", not "#body".form: struct tag is not read by the framework — only route: is. Anything else on a route field is ignored.