structpages

package module
v0.1.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2025 License: BSD-3-Clause Imports: 16 Imported by: 1

README

structpages

CI Go Reference codecov Go Report Card

Struct Pages provides a way to define routing using struct tags and methods. It integrates with Go's http.ServeMux, allowing you to quickly build web applications with minimal boilerplate.

Status: Alpha - This package is in early development and may have breaking changes in the future. Currently used in a medium-sized project, but not yet battle-tested in production.

Features

  • 🏗️ Struct-based routing - Define routes using struct tags
  • 🎨 Templ support - Built-in integration with Templ
  • HTMX-friendly - Automatic partial rendering support
  • 🔧 Middleware - Standard Go middleware pattern
  • 🎯 Type-safe URLs - Generate URLs from struct references
  • 📦 Dependency injection - Pass dependencies to handlers via options

Installation

go get github.com/jackielii/structpages

Quick Start

Define your page structure using struct tags:

package main

import (
    "log"
    "net/http"
    "github.com/jackielii/structpages"
)

type index struct {
    product `route:"/product Product"`
    team    `route:"/team Team"`
    contact `route:"/contact Contact"`
}

// Implement the Page method using Templ
templ (index) Page() {
    <html>
        <body>
            <h1>Welcome</h1>
            <nav>
                <a href="/product">Product</a>
                <a href="/team">Team</a>
                <a href="/contact">Contact</a>
            </nav>
        </body>
    </html>
}

func main() {
    mux := http.NewServeMux()
    _, err := structpages.Mount(mux, index{}, "/", "Home")
    if err != nil {
        log.Fatal(err)
    }

    log.Println("Starting server on :8080")
    http.ListenAndServe(":8080", mux)
}

Route definitions use the format [method] path [Title]:

  • /path - All methods, no title
  • POST /path - POST requests only
  • GET /path Page Title - GET requests with title "Page Title"

Documentation

Examples

Check out the examples directory for complete working applications:

  • Simple - Basic routing and page rendering
  • HTMX - HTMX integration with partial updates
  • Todo - Full todo application with database

Contributing

See CONTRIBUTING.md for development setup and guidelines.

License

MIT License - see LICENSE file for details.

Documentation

Overview

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

Constants

This section is empty.

Variables

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

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.

Functions

func ID added in v0.1.0

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 added in v0.1.0

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 added in v0.0.9

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)

2. 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)

3. 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)
	}
}

4. 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

If multiple page type matches are found, the first one is returned. In such situation, use a func(*PageNode) bool as page argument to match a specific page.

Additionally, you can pass []any to page to join multiple path segments together. Strings will be joined as is. Example:

URLFor(ctx, []any{Page{}, "?foo={bar}"}, "bar", "baz")

It also supports a func(*PageNode) bool as the Page argument to match a specific page. It can be useful when you have multiple pages with the same type but different routes.

func WithArgs added in v0.0.20

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 added in v0.1.0

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 WithWarnEmptyRoute added in v0.0.11

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) {}),
)

Types

type MiddlewareFunc

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

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 Mux added in v0.0.15

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

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 Option added in v0.0.20

type Option func(*StructPages)

Option represents a configuration option for StructPages.

type PageNode

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
}

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.

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 added in v0.0.15

type Ref string

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 RenderTarget added in v0.0.15

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
}

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
    }
}

func HTMXRenderTarget added in v0.1.0

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 is the default TargetSelector for StructPages, making IDFor work seamlessly with HTMX out of the box.

type StructPages

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

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

func Mount added in v0.0.15

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 (*StructPages) ID added in v0.1.0

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 added in v0.1.0

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) URLFor added in v0.0.15

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.

If multiple page type matches are found, the first one is returned. In such situation, use a func(*PageNode) bool as page argument to match a specific page.

Additionally, you can pass []any to page to join multiple path segments together. Strings will be joined as is. Example:

sp.URLFor([]any{Page{}, "?foo={bar}"}, "bar", "baz")

It also supports a func(*PageNode) bool as the Page argument to match a specific page. It can be useful when you have multiple pages with the same type but different routes.

type TargetSelector added in v0.1.0

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

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.

Directories

Path Synopsis
chirouter module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL