render

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: MIT Imports: 27 Imported by: 1

README

⚡ nanite-render

The lock-free, zero-alloc composition hub for Go server-side rendering

Compose any template engine with a hot render path that allocates 0 bytes on cache hits. Layouts, components, slots, state, and per-request FuncMaps — all wired into a single 60ns hot loop.


Badges

Allocations (hot path) 0 B/op
Cache hit ~60 ns
Concurrency 100% lock-free (atomic.Pointer everywhere)
Engine surface Jade · html/template · plain HTML · templ
HTMX 100% first-class (all 12 response headers + OOB + 3 trigger lifecycles)
License MIT
Go 1.25+

Hero

         ┌────────────────────────────┐
         │  Your template engine      │  Jade, html/template, templ,
         │  (jade, templ, html/tmpl)  │  plain HTML, or a custom Engine
         └──────────────┬─────────────┘
                        │  Engine.Compile / Engine.Execute
                        ▼
         ┌────────────────────────────┐
         │  nanite-render core        │
         │  • Lock-free SoA cache     │  ~60ns hit, 0 allocs
         │  • Per-request FuncMap     │  closures, not globals
         │  • Layout + view + slots   │  React-style composition
         │  • Components + State      │  UseState[T], children, slots
         │  • Pooled writer           │  zero-alloc byte streaming
         └──────────────┬─────────────┘
                        │  router-agnostic RenderContext
                        ▼
         ┌────────────────────────────┐
         │  Your router               │  nanite, chi, gin, stdlib http
         └────────────────────────────┘

The core has zero router dependencies. The nano/ subpackage is the only code that imports xDarkicex/nanite.


Why nanite-render

  • Not a parser. Jade parses jade. templ parses templ. html/template parses html/template. nanite-render composes around them — it's the cache + composition + lifecycle layer that makes them all feel like one system.
  • Lock-free hot path. Every read in the Registry goes through atomic.Pointer. No sync.RWMutex on any render path. This matters at the 190k req/sec that the nanite router targets.
  • Zero allocations on render. The cache stores pre-compiled programs; the executor walks a Structure-of-Arrays node stream with no per-render allocation. BenchmarkRegistry_RenderHotPath reports 0 B/op.
  • React-style composition in Go. Components, children, named slots, per-render state (UseState[T]), and type-safe props (BindProps[T]).
  • Router-agnostic. Use it with nanite, chi, gin, or plain net/http. The adapters are thin and live outside the core.
  • Inspired by the original GO-Portfolio pipeline — per-part caching, per-render funcMap, layout+view composition — but rebuilt lock-free and zero-alloc.

Benchmarks

Measured on Apple M2, Go 1.25, 3 KB HTML document (30 cards + nav + footer).

Hot-path primitives
Operation ns/op allocs B/op
Cache.Get (lock-free, off-heap) 11 0 0
SoA bytecode executor (our renderer) 85 0 0
SoA tree walk (fallback) 14,474 0 0
End-to-end render (3 KB doc)
Path ns/op allocs
nanite bytecode executor (plain HTML) 85 0
templ (direct) 1,395 20
templ (via nanite adapter) 1,523 22
SoA tree walk (fallback) 14,474 0
nanite cached (jade/HTMLTemplate) 40,088 567*
html/template compiled 40,076 567*
raw re-parse every render (GO-Portfolio style) 148,600 1,967

* html/template's reflection + re-escaping — identical to the raw compiled baseline, not nanite-render's overhead.

The bytecode executor compiles the template once into a flat instruction stream: all static HTML (tags, attrs, text, escaping) is coalesced into a single output buffer at compile time — exactly what templ's compiler does — so a static-heavy template renders as one write. It is 16× faster than templ on the same document (85 ns vs 1.4 µs) because templ still writes piece-by-piece while we merge the whole static region.

Cache latency savings (p50/p99/p99.9, 20k samples)
Path p50 p99 p99.9
nanite hot (cached) 66.8 µs 104.7 µs 253.8 µs
nanite cold (compile) 249.3 µs 546.3 µs 745.6 µs
raw html/template re-parse 241.9 µs 616.5 µs 1,367.8 µs
raw html/template compiled 71.1 µs 235.5 µs 393.5 µs

The cache saves 182.5 µs at p50 — 3.7× faster than re-rendering, and tames the p99.9 tail from 1.37 ms (re-parse) to 254 µs (cached).

Compile-time (one-time, cached forever)
Operation ns/op allocs
Compile (parse → program) 80,035 1,043
Minify 65,207 31
Minification data savings
raw bytes:        6,068
minified bytes:   3,938   (35.1% saved)
The templ story

templ is the fastest engine (~2.4 µs, 20 allocs) because it compiles templates to Go — no reflection. nanite-render's own SoA executor is 0 allocs but a generic tree walker (23.5 µs). html/template is the alloc-heavy baseline (567 allocs) — its reflection + re-escaping is inherent to its design. Pick whichever engine; the composition layer (cache, state, slots, components, memoization) is identical on top.

Run them yourself:

go test ./bench/ -bench=. -benchmem -benchtime=1s
go test ./bench/ -run TestLatency -v
go test ./bench/ -run TestMinify -v

Used by xDarkicex


Quick start

package main

import (
    "net/http"

    "github.com/xDarkicex/nanite-render"
    "github.com/xDarkicex/nanite-render/engine"
)

func main() {
    // Compose engines into one registry.
    reg := render.New(
        render.WithEngines(
            engine.NewJade(),
            engine.NewHTMLTemplate(),
            engine.NewHTML(),   // plain HTML, no data binding
        ),
        render.WithDefaultLoader(render.NewFileLoader("./views", ".jade")),
        render.WithCacheSize(2048),
    )

    // Works with plain net/http — no router needed.
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        bw := render.AcquireWriter(w)
        defer render.ReleaseWriter(bw)
        rc := render.AcquireContext(bw, r)
        defer render.ReleaseContext(rc)
        rc.Loader = reg.DefaultLoader()

        if err := reg.Render(rc, "jade", "index", map[string]any{"Title": "Hello"}); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
    })

    http.ListenAndServe(":8080", nil)
}

Router integrations

xDarkicex/nanite (the fast one)

The nano adapter is a direct-call helper — no middleware indirection. Handlers call nano.Render at the end, exactly like the original GO-Portfolio pattern.

import (
    "github.com/xDarkicex/nanite"
    "github.com/xDarkicex/nanite-render/nano"
    "github.com/xDarkicex/nanite-render"
    "github.com/xDarkicex/nanite-render/engine"
)

func main() {
    reg := render.New(
        render.WithEngines(engine.NewJade(), engine.NewHTMLTemplate()),
        render.WithDefaultLoader(render.NewFileLoader("./views", ".jade")),
        render.WithCacheSize(2048),
        render.WithConditional(),   // ETag / 304
        render.WithCompression(6, 1024),
    )

    r := nanite.New()
    r.Get("/posts/:slug", func(c *nanite.Context) {
        post := loadPost(c.Param("slug"))
        // Direct call, no middleware, no setup.
        if err := nano.Render(c, reg, "jade", "posts/show", post); err != nil {
            c.Error(http.StatusInternalServerError, err)
        }
    })
    r.Start(":8080")
}

Fluent variant — compose layout + view + parts with a chainable builder:

r.Get("/posts/:slug", func(c *nanite.Context) {
    post := loadPost(c.Param("slug"))
    if err := nano.Page(reg, c).
        Engine(render.EngineJade).
        Layout("layouts/app").
        View("posts/show").
        With(post).
        Render(); err != nil {
        c.Error(http.StatusInternalServerError, err)
    }
})

Or the direct-call — one line, no builder:

r.Get("/posts/:slug", func(c *nanite.Context) {
    post := loadPost(c.Param("slug"))
    if err := nano.RenderPage(c, reg, "layouts/app", "posts/show", post); err != nil {
        c.Error(http.StatusInternalServerError, err)
    }
})

Both acquire a pooled RenderContext and ByteWriter, run the engine, and release them — the fluent builder's Render() releases automatically via its WithDone hook. No middleware to mount, nothing to configure per-request.

The same fluent builder works in the core without nanite:

reg.Page(rc).
    Engine(render.EngineJade).          // typed constant, or:
    EngineInstance(engine.NewJade()).   // a live engine value
    Layout("layouts/app").
    View("posts/show").
    Parts("partials/nav", "partials/footer").
    With(post).
    RenderComposition()   // or .Render() for layout+view only

Engine names are a distinct type — Engine("jad") is a compile error. Use the constants (EngineJade, EngineHTML, EngineHTMLTemplate, EngineTempl) or render.CustomEngine("my-engine") for a user-defined engine.

chi / gin / stdlib http

The core is router-agnostic. Any handler that can produce an http.ResponseWriter and *http.Request works:

func renderHandler(w http.ResponseWriter, r *http.Request) {
    bw := render.AcquireWriter(w)
    defer render.ReleaseWriter(bw)
    rc := render.AcquireContext(bw, r)
    defer render.ReleaseContext(rc)
    rc.Loader = reg.DefaultLoader()

    err := reg.Render(rc, "jade", "index", data)
    if errors.Is(err, render.ErrTemplateNotFound) {
        http.Error(w, "not found", http.StatusNotFound)
        return
    }
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

// chi
r := chi.NewRouter()
r.Get("/", renderHandler)

// gin
g := gin.Default()
g.GET("/", func(c *gin.Context) { renderHandler(c.Writer, c.Request) })

The RenderContext is the only thing that crosses the router boundary — it's a plain struct holding a writer, a loader, and per-request config. No router types leak into the core.


The cache

The Cache is an off-heap, lock-free, cache-line-padded program store. It's the heart of the hot path.

// Get a cache; the Registry lazily creates one if not provided.
cache := reg.Cache()

// Per-part caching — layout, view, and partials are separate entries.
cache.Put("jade", "layouts/app", layoutProgram)
cache.Put("jade", "posts/show", viewProgram)

p, ok := cache.Get("jade", "posts/show")  // ~50ns, 0 allocs
  • Keys are (engine, name) pairs. No string concat on the hot path — the FNV-1a hash composes both directly.
  • Off-heap slots via xDarkicex/memory.MmapAnonymous. The GC never scans the slot array.
  • Cache-line padded slots. One cache line per slot on x86 (128B on Apple Silicon), so concurrent slots don't false-share.
  • Lock-free. Get is a seqlock read: load seq, load payload, load seq again, verify. Put is a CAS-loop.
  • Tag-based invalidation. Group entries and drop them together.
reg.SetTag("jade", "layouts/app", "chrome")   // tag this entry
reg.SetTag("jade", "posts/show", "content")
n := reg.InvalidateTag("chrome")              // drop all chrome-tagged entries

Cache-level stats are exposed:

stats := cache.Stats()  // CacheStats{Hits, Misses, Evicts, Capacity}

Custom FuncMaps (per-request)

The original GO-Portfolio built a fresh FuncMap per render — closures capturing the request handler's locals. nanite-render does the same, but makes it declarative.

Register a factory:

reg.FuncMap(func(rc *render.RenderContext) template.FuncMap {
    return template.FuncMap{
        "currentUser": func() *User { return userFromContext(rc) },
        "csrfToken":   func() string { return issueToken(rc) },
        "shout":       func(s string) string { return strings.ToUpper(s) + "!" },
    }
})

The factory runs on every render and the result is layered on top of engine defaults. Per-request values win.

Or set directly on a specific render context:

rc.WithFuncMap(template.FuncMap{
    "currentUser": func() *User { return c.Params... },
})

Data injectors run before the FuncMap and mutate the data map with whatever keys you want:

reg.Inject(func(rc *render.RenderContext, data map[string]any) {
    data["current_user"] = loadUser(rc.Request)
    data["flashes"]      = loadFlashes(rc.Request)
    data["theme"]        = themeFromCookie(rc.Request)
})

No hardcoded keys — inject anything.


React-style components

nanite-render gives you a React-flavoured composition model in Go — components, typed props, hooks, context, error boundaries, and Suspense, all server-side, all zero-alloc where it matters.

React concept nanite-render equivalent
JSX component cr.Define("NAME").Render(fn).Register(cr) — the fluent builder
Props c.Data + render.BindProps[T] — typed, zero-alloc
props.children c.WriteChildren() + named slots c.WriteSlot("name")
Layouts (layout.tsx / children in layout) c.Yield() — writes the pre-rendered view body in the two-pass pipeline
useState / state hooks c.UseState(key, initial) / render.UseState[T]
Context (createContext / useContext) c.ProvideContext / c.UseContext — zero-alloc stack, auto-scoped
Error Boundaries (componentDidCatch) cr.Define(...).ErrorBoundary(fn)
<Suspense> / fallback cr.Define(...).Async().Fallback(fn) — server-side, streams via HTMX OOB
React.memo cr.Memoize(name, keyer)
createPortal (render elsewhere) WithOOB(id) — HTMX out-of-band swaps
Server Actions ("use server") cr.Define(...).Action(name, fn) — colocated mutations, universal handler, secure by default
useActionState / form validation rc.SetFormError / c.GetFormError + ErrValidation — flash errors, inline re-render, no cookies or redirects
<Head> / generateMetadata c.SetTitle / c.AddMeta + <NANO_HEAD/> — two-pass head injection, no body buffering
useId c.UseId() — per-request unique ids, zero-alloc first 256
Asset deps (import side effects) c.RequiresCSS / c.RequiresJS + <NANO_ASSETS/> — deduplicated head emission
HOC (withAuth(Widget)) cr.Define(...).Use(mw) — dispatcher-wrapping middleware, gates async forks
Inline composition (<Navbar/>) c.Render("Navbar", props)

The same component runs identically from templ, html/template, jade, and plain HTML — the React layer is engine-agnostic.

The fluent builder
cr := render.NewComponentRegistry()

cr.Define("NAVBAR").
    WithFuncs(template.FuncMap{
        "currentUser": func() *User { return loadUser() },
    }).
    RenderChildren(func(c *render.ComponentContext) error {
        c.WriteString("<nav class='navbar'>")
        c.WriteChildren()
        c.WriteString("</nav>")
        return nil
    }).
    Register(cr)

reg.AttachComponents(cr)

Then use it in any template:

html
  body
    NAVBAR
      ul
        li Home
        li About
Named slots
cr.Define("CARD").
    Render(func(c *render.ComponentContext) error {
        c.WriteString(`<div class="card">`)
        c.WriteString(`<div class="header">`); c.WriteSlot("header"); c.WriteString(`</div>`)
        c.WriteString(`<div class="body">`);   c.WriteSlot("body");   c.WriteString(`</div>`)
        c.WriteString(`<div class="footer">`); c.WriteSlot("footer"); c.WriteString(`</div>`)
        c.WriteString(`</div>`)
        return nil
    }).
    Register(cr)
CARD
  header
    h1 Welcome
  body
    p This is the body
  footer
    button Submit
Per-component state (hooks)
cr.Define("COUNTER").
    Render(func(c *render.ComponentContext) error {
        count, setCount := render.UseState(c.State, "count", 0)
        setCount(count + 1)
        _, err := c.WriteString("<span>" + strconv.Itoa(count) + "</span>")
        return err
    }).
    Register(cr)

State is per-render and shared across components. UseState[T] is generic — no type assertions.

The React layer is first-class on ComponentContext — state, typed props, and inline composition, uniformly across every engine:

cr.Define("CARD").
    Render(func(c *render.ComponentContext) error {
        val, setVal := c.UseState("key", 0)     // React-style hook
        setVal(val.(int) + 1)
        props := render.Props[CardProps](c)      // typed props (nanite:"key" tags)
        c.WriteSlot("header")                    // named slots
        c.WriteChildren()                        // slot-less children
        return c.Render("Navbar", props)         // compose another component inline
    }).
    Register(cr)

The same component runs identically from templ (render.RenderComponent(ctx, w, "CARD", props)), html/template ({{ component "CARD" . }}), and plain HTML (<CARD/>).

Type-safe props
type NavbarProps struct {
    Theme string `nanite:"theme"`
    Size  int    `nanite:"size"`
}

cr.Define("NAVBAR").
    Render(func(c *render.ComponentContext) error {
        props := render.BindProps[NavbarProps](c.Data)
        // props.Theme, props.Size are strongly typed
        return nil
    }).
    Register(cr)

Zero allocations. BindProps[T] binds nanite:"key" tags from a map[string]any into a typed struct with 0 B/op, 0 allocs on the scalar fast path:

  • Per-type field layouts (byte offsets + exact types + resolved tag keys) are built once per distinct T via reflection and cached; the hot path never touches reflection.
  • Scalar fields (string, bool, int*, uint*, float*) are written directly via unsafe.Pointer arithmetic — typed stores at computed struct offsets.
  • The props struct stays on the stack (the runtime's noescape intrinsic defeats escape analysis' conservative handling of unsafe.Pointer args).
  • Custom named types (type UserID string) and complex fields (slices, maps, pointers, nested structs) fall through to a reflect-based slow path that respects assignability.

Measured (Apple M2, scalar 4-field struct): 137 ns/op, 0 B/op, down from 246 ns/op and 48 B/op for the previous reflect implementation.


Superpowers

Every engine gets the framework's cross-cutting superpowers automatically:

Superpower Template syntax What it does
component {{ component "NAVBAR" . }} Renders a registered component inline
yield {{ yield }} Injects the pre-rendered view body (layout composition)
useState {{ useState "count" 0 }} React-style per-render state
get / set {{ get "count" }} / {{ set "count" 5 }} State access
<SLOW_DATA/> with Async() + Fallback() Server-side Suspense — fallback hits the wire immediately, real output streams as an HTMX OOB chunk when the worker finishes Turns Sum(component_times) into Max(component_times)
ErrorBoundary(fn) on a Definition Isolates panics in the component's render — boundary runs against a fresh context, the failed component's bytes are discarded, the rest of the page continues One broken widget never kills the whole request
ProvideContext(key, val) / UseContext(key) on ComponentContext Push/pop a value onto a fixed-size stack on the request context; reverse-scan lookup; auto-pop on scope exit; zero heap allocations Cascading state without prop-drilling, with template-engine reach via useContext

The component and yield superpowers are injected into the FuncMap of any engine that compiles via html/template (jade, html-template). The plain HTML engine uses the SoA executor's native <COMPONENT/> dispatch instead.

Layout composition is seamless:

// layouts/app.jade
html
  head
    title= .Title
  body
    NAVBAR
    {{ yield }}
    FOOTER

// posts/show.jade
h1= .Post.Title
p= .Post.Body
err := reg.RenderPage(rc, "layouts/app", "posts/show", data)
Component-level yield (c.Yield())

For component-based layouts (gsx, fluent components), ComponentContext.Yield() writes the pre-rendered view body:

cr.Define("APP_LAYOUT").
    Render(func(c *render.ComponentContext) error {
        c.WriteString(`<html><body><main>`)
        if err := c.Yield(); err != nil { return err }
        c.WriteString(`</main></body></html>`)
        return nil
    }).
    Register(cr)

Yield() writes rc.ViewBytes — set by the two-pass pipeline after rendering the view, before the layout renders. Standalone renders write nothing. This is the hook the gsx @yield directive compiles to.

templ-native layout composition

templ composes natively — a layout wraps a view component (Layout(view)). Register both and RenderPageWith routes to templ's own composition:

eng.Register("view", func(data any) templ.Component { return View(data) })
eng.RegisterLayout("layouts/app", func(child templ.Component) templ.Component {
    return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
        io.WriteString(w, "<html><body>")
        child.Render(ctx, w)          // the view, injected natively
        io.WriteString(w, "</body></html>")
        return nil
    })
})

err := reg.RenderPageWith(rc, render.EngineTempl, "layouts/app", "view", data)

The layout and view both receive the framework context, so state, slots, and superpowers work inside templ too (rc := render.FromContext(ctx)).

Mixed-engine composition (the GO-Portfolio pattern)

The handler names the view; the layout maps it in; partials are components. RenderPageEngines composes a layout and view rendered by different engines — the layout as plain HTML (compiled to bytecode, ~85 ns) with a <YIELD/> view slot, the view as templ (or jade, or html-template):

// Layout file (plain HTML → bytecode):
//   <html><body><NAVBAR/><YIELD/><FOOTER/></body></html>
//
// View: a templ component.
eng.Register("posts/show", func(data any) templ.Component { return View(data) })

// Partials: React-style components (memoizable).
cr.Define("NAVBAR").Render(func(c *render.ComponentContext) error { ... }).Register(cr)
cr.Memoize("NAVBAR", func(rc *render.RenderContext, data any) string { return "static" })

// The handler call — name the view, the layout wraps it:
err := reg.RenderPageEngines(rc,
    render.EngineHTML,  "layouts/app",   // bytecode layout
    render.EngineTempl, "posts/show",    // templ view
    data)

Output: <html><body><nav ...>nav</nav><h1>the post</h1><footer>footer</footer></body></html>. The view is rendered to a buffer, the layout's <YIELD/> injects it, and the partials dispatch through the ComponentRegistry — with state, slots, props, and memoization available to every part.

Per-component memoization

GO-Portfolio cached each part; we apply the same idea per component. cr.Memoize caches a component's rendered HTML by key:

// Data-independent → render once, cache forever.
cr.Memoize("Navbar", func(rc *render.RenderContext, data any) string { return "static" })

// Keyed by data → cache per ID.
cr.Memoize("UserCard", func(rc *render.RenderContext, data any) string {
    return data.(UserCardProps).ID
})

Repeat keys serve the cached HTML directly — no re-render.


Server-side <Suspense> (Async / Fallback)

Slow components don't block the rest of the page. Opt in per-component with Async() + Fallback():

cr.Define("USER_PROFILE").
    Async().
    Fallback(func(c *render.ComponentContext) error {
        // Skeleton hits the wire immediately (TTFB ≈ 0ms).
        return c.WriteString(`<div id="profile" class="skeleton">Loading…</div>`)
    }).
    Render(func(c *render.ComponentContext) error {
        // Expensive query / IO — runs in a worker goroutine.
        user := db.LoadUser(c.Data.(ProfileID))
        return c.WriteString(fmt.Sprintf(`<div id="profile">%s</div>`, user.Name))
    }).
    Register(cr)

When the executor hits USER_PROFILE:

  1. Inline: the Fallback output is written and flushed to the response immediately. The browser paints the skeleton.
  2. Background: a worker goroutine runs the render to a pooled buffer.
  3. Streaming: when the worker finishes, the buffered bytes are wrapped in <div id="user_profile" hx-swap-oob="true">…</div> and emitted as a trailing chunk.

The win: Sum(component_times) becomes Max(component_times) for independent components. Cancellation is wired through rc.Request.Context() — a client disconnect aborts the worker and releases its buffer.

The coordinator is lazy — zero channels, zero goroutines, zero allocations on the non-async path. Use rc.Suspense() to inspect whether suspense is in flight.

Call rc.CloseSuspense() after the render walk completes (typically in a defer in the handler) so the trailing OOB chunks flush before the response closes.


Error Boundaries (panic isolation)

A panic deep inside a component used to crash the whole request. With ErrorBoundary(), only the failed component is replaced — the rest of the page renders normally.

cr.Define("DASHBOARD_WIDGET").
    ErrorBoundary(func(c *render.ComponentContext, err any) error {
        // Safe to log the raw panic value (it's the unfiltered
        // value passed to panic()). Be careful NOT to echo it
        // directly into the response — internal errors may
        // contain credentials, stack traces, etc.
        log.Printf("widget failed: %v", err)
        return c.WriteString(`<div class="error">Widget unavailable</div>`)
    }).
    Render(func(c *render.ComponentContext) error {
        // If this panics, only THIS component fails. The rest of
        // the page keeps rendering.
        panic("database timeout")
    })

How it works: the component's render runs into an isolated buffer (same pattern as WithOOB). On panic, the buffer is discarded, the boundary is invoked with a fresh context pointing at the live response, and the boundary's writes replace the failed component in the page. Zero overhead for components without a boundary — no defer/recover is set up unless one is registered.

Async + ErrorBoundary: workers that panic invoke their boundary too. The boundary's output replaces the expected OOB chunk, so the client sees a consistent error shape (<div id="..." hx-swap-oob="true">...error...</div>). A panic in the boundary itself falls back to a generic <!-- error boundary failed --> placeholder so the page never crashes.

Nested safety: the boundary call is itself wrapped in defer/recover. If the boundary panics or returns an error, the generic placeholder is emitted and rendering continues.

Without a boundary: a panic re-panics out of the component (sync) or silently drops the OOB chunk (async). Use boundaries when you want graceful degradation; rely on re-panic when you want loud failures during development.


Cascading Context (React-style, zero-alloc)

Prop-drilling theme/user/locale/CSRF-token through 15 layers is painful. ProvideContext + UseContext solves it without heap allocations:

cr.Define("LAYOUT").
    Render(func(c *render.ComponentContext) error {
        c.ProvideContext("theme", "dark")
        c.ProvideContext("user", currentUser)
        return c.RenderChildren(/* ... */)
    })

cr.Define("DEEP_BUTTON").
    Render(func(c *render.ComponentContext) error {
        theme := c.UseContext("theme").(string)  // "dark"
        return c.WriteString(`<button class="` + theme + `">...</button>`)
    })

How it works: a fixed-size [32]contextKV array lives directly on *RenderContext. The first 32 nested bindings land in this inline array — PushContext is a pure memory move (zero alloc). If a render tree exceeds 32 nested bindings (unusual — admin UIs with deeply nested layouts), pushes spill into a heap slice so the framework never panics.

Scope isolation: the dispatcher pops the stack back to the saved depth when each component's render returns. Bindings made inside LAYOUT.Render are visible to DEEP_BUTTON (nested), but NOT to a sibling component rendered after LAYOUT finishes.

Templates too: the default FuncMap already includes useState, get, set, and useContext — batteries included. Templates call {{ useContext "theme" }} directly, no opt-in.

Async workers get a fresh *RenderContext (their own empty stack), so they don't inherit cascading state from the inline render. Provide what the worker needs in its Fallback or pass via props.


Colocated Server Actions (Next.js server actions style)

Mutation logic lives right next to the component that triggers it. The component declares its own HTTP handlers; one universal handler dispatches them on any router.

cr.Define("LIKE_BUTTON").
    Action("toggle", func(rc *render.RenderContext, props map[string]any) error {
        db.Exec("UPDATE likes ...") // user's own storage
        return nil
    }).
    Render(func(c *render.ComponentContext) error {
        liked := queryLikes()
        // c.ActionURL auto-generates: /_nano/action/LIKE_BUTTON/toggle
        return c.WriteString(`<button hx-post="` + c.ActionURL("toggle") + `">` +
            strings.Repeat("❤", liked) + `</button>`)
    }).
    Register(cr)

Mount one handler on any router:

// chi
r.Post("/_nano/action/*", reg.HandleAction)
// stdlib
mux.HandleFunc("/_nano/action/", reg.HandleAction)

The flow when the action URL is POSTed:

  1. Security baseline (secure by default): method must be POST, HX-Request must be true, and the Origin/Referer host must match the request Host — a CSRF baseline that requires zero setup. All checks fail with 403.
  2. Dispatch: the path is parsed as {prefix}/{COMPONENT}/{action}; unknown component or action → 404.
  3. Props: the body is parsed — JSON (application/json) decodes into a map[string]any with real types (BindProps-compatible); form bodies get best-effort conversion ("42" → int, "true" → bool, "3.14" → float64, else string).
  4. Mutate: the action closure runs with a fresh RenderContext. HX-Trigger events it records are emitted on the response.
  5. Re-render: the component re-renders with the post-action state. WithOOB(id) components get their output wrapped in an HTMX OOB chunk; otherwise raw HTML is returned for the client's hx-target to swap inline.

The state contract (important): the framework does not persist state across requests — that's the user's job (DB, session, cookie). The action mutates storage; the re-render is a pure function of (props, request). Per-request state set in the action is visible to the re-render in the same request. This is not React memory semantics.

Configuration: the URL prefix defaults to /_nano/action/; override with render.WithActionPrefix("/api/act/").

Flash form validation (React useActionState style)

Validation errors flow action → re-render without session cookies or redirects. The action records per-field errors and returns ErrValidation; HandleAction intercepts it and re-renders the component at 200 with the errors readable inline:

cr.Define("LOGIN_FORM").
    Action("submit", func(rc *render.RenderContext, props map[string]any) error {
        if !valid {
            rc.SetFormError("email", "Invalid format")
            rc.SetFormError("password", "Too short")
            return fmt.Errorf("login: %w", render.ErrValidation) // wrapped ok
        }
        return nil
    }).
    Render(func(c *render.ComponentContext) error {
        if e := c.GetFormError("email"); e != "" {
            c.WriteString(`<span class="error">` + e + `</span>`)
        }
        // ...
        return nil
    }).
    Register(cr)

Why 200, not 422: HTMX does not swap 4xx/5xx responses by default — it fires htmx:responseError and leaves the DOM untouched. The 200 response lets HTMX swap the form containing the inline error spans. (If you want 422 semantics, handle htmx:responseError client-side.)

Flash semantics: errors live for exactly one request — the action sets them, the re-render reads them, and pool reuse clears them. No persistence, no cookies, no redirects. The errors are display state; the component re-renders from its own data source.

Implementation: formErrors [8]formErrorKV inline array on RenderContext (zero alloc), spilling to a heap slice past 8. SetFormError/GetFormError on both RenderContext and ComponentContext, plus a {{ formError "email" }} default-FuncMap helper for template engines. Match the sentinel with errors.Is — any non-ErrValidation error still returns 500.


Deep Document Head Management (Next.js <Head>)

A deeply nested component can inject <title> / <meta> into the document head — without buffering the body or hurting TTFB.

How it works: the two-pass page pipeline already renders the view before the layout streams (that's what yield needs). So any component in the view can call c.SetTitle / c.AddMeta during its render, and the head state is transferred to the layout's context before it streams. The layout places <NANO_HEAD/> (plain HTML) or {{ nanoHead }} (template engines) inside <head> — it emits the collected tags inline, zero allocation:

cr.Define("USER_PROFILE").
    Render(func(c *render.ComponentContext) error {
        user := c.Data.(User)
        c.SetTitle(user.Name + " - Profile")                    // → <title>…</title>
        c.AddMeta("description", "Profile page for " + user.Name) // → <meta name=…>
        return c.WriteString(`<div>…</div>`)
    }).
    Register(cr)
<!-- layouts/app.html -->
<html>
  <head><NANO_HEAD/></head>   <!-- emits title + meta collected during the view -->
  <body>{{ yield }}</body>
</html>

Values are HTML-escaped (user data in head tags is an injection vector). AddMeta is last-write-wins per name; up to 16 meta tags live in a zero-alloc inline array, spilling to a heap slice past that.

Metadata pre-flight (optional): cr.Define("PROFILE").Metadata(func(rc *render.RenderContext, data any) error { rc.SetTitle(...) }) runs before the render walk when the view name matches the component name. Mostly redundant with the two-pass transfer above, but covers views that aren't registered components or metadata that must run unconditionally.

HTMX partial swaps: HTMX hoists <title> tags from response bodies automatically — a component on an hx-get can just c.WriteString("<title>Saved!</title>") and the tab title updates. Meta tags use hx-swap-oob="true" (htmx 2.0 head merge).

Stable server-side IDs (useId)
cr.Define("INPUT_FIELD").
    Render(func(c *render.ComponentContext) error {
        id := c.UseId() // "nano-1", "nano-2", …
        c.WriteString(`<label for="` + id + `">Name</label>`)
        c.WriteString(`<input id="` + id + `" type="text">`)
        return nil
    })

The first 256 ids come from a precomputed static array — zero allocation. The sequence is per-request (reset on pool reuse) and continues across the view → layout boundary so page-wide ids never collide. Unlike React, ids are a per-request sequence rather than stable per component instance — uniqueness (what label/input pairing needs) is what's guaranteed.


Asset Dependency Graph (<NANO_ASSETS/>)

Components declare their CSS/JS dependencies; the framework collects them during the render pass, deduplicates, and emits them once in the head. No invalid markup from <link>/<script> inside component bodies, no duplicate downloads from components rendered in loops:

cr.Define("CHART_WIDGET").
    Render(func(c *render.ComponentContext) error {
        c.RequiresCSS("/static/css/chart.css")
        c.RequiresJS("/static/js/chart.js")
        // ... component body, safe to render in a loop
    }).
    Register(cr)
<!-- layouts/app.html -->
<html>
  <head><NANO_HEAD/><NANO_ASSETS/></head>
  <body>{{ yield }}</body>
</html>

<NANO_ASSETS/> (plain HTML) / {{ nanoAssets }} (template engines) emits:

<link rel="stylesheet" href="/static/css/chart.css">
<script defer src="/static/js/chart.js"></script>

Deduplication: a linear scan over the inline arrays — at ≤16 slots it's already optimal (no bitset needed for string paths). First occurrence wins, so emission order = first render order (deterministic — the executor walks the tree in order). A card rendered 50 times in a loop emits one tag; the browser downloads the asset once.

Hydration prop serialization (WriteHydrateProps)

Bridge server state into client-side frameworks (Alpine.js x-data, HTMX extensions, vanilla JS) without hand-rolled string concatenation:

cr.Define("INTERACTIVE_SLIDER").
    Render(func(c *render.ComponentContext) error {
        props := render.BindProps[SliderProps](c.Data)
        c.WriteString(`<div `)
        c.WriteHydrateProps("x-data", props) // x-data='{"min":0,"max":100}'
        c.WriteString(`>`)
        return nil
    })

The JSON is written inside single quotes with every byte HTML-escaped (via escapeBytes, streaming — no extra string(b) conversion). encoding/json itself HTML-escapes <>& to <...>, so injection is double-covered; browsers entity-decode the attribute, so the client sees the exact JSON.

Honest allocation note: json.Marshal allocates — this is a correctness helper (escaping, no manual concat), not a zero-alloc one. It runs once per component per render. Marshal failures (channels, functions, cycles) return an error and nothing is written.

Zero-alloc: two [16]string inline arrays (CSS + JS) on the RenderContext with heap-slice spillover past 16 — the same pattern as meta tags and form errors. Values are HTML-escaped.

Partial swaps: assets are collected on full page loads; later hx-get/hx-post swaps inherit the already-loaded head. If a component is loaded via swap without a prior full page load, its assets won't be in the head — load the page first (or include the tags explicitly in the swap response).


Component Middleware (.Use()) — React HOC pattern

Router-agnostic Higher-Order Components: wrap a component's dispatch with gating logic — feature flags, A/B tests, component-level authorization:

func RequireAdmin(next render.ComponentRenderFunc) render.ComponentRenderFunc {
    return func(c *render.ComponentContext) error {
        if role, ok := c.UseContext("role").(string); !ok || role != "admin" {
            return nil // abort — component renders nothing
        }
        return next(c)
    }
}

cr.Define("ADMIN_BUTTON").
    Use(RequireAdmin, LogRender).   // first = outermost
    Render(func(c *render.ComponentContext) error {
        return c.WriteString(`<button>Delete</button>`)
    }).
    Register(cr)

The dispatcher topology (this is what makes it secure):

  • Middleware wraps the component's dispatcher, not just the render function. The chain runs on the main thread before any async fork — so it has full access to cascading context (c.UseContext), and an abort (not calling next) means the async fallback is never written and the worker is never spawned. Protecting a component with .Use(RequireAdmin) stays protective even if someone later marks it .Async().
  • The async worker runs the raw render function — middleware runs exactly once, on the main thread.
  • Sync components: the chain runs inside the render machinery, so OOB buffering and the error boundary wrap the middleware. Middleware writes join the component's output (OOB-wrapped when dirty); middleware panics are caught by the boundary.
  • Folded once at Register — the hot path calls a single function pointer. Zero runtime iteration, zero allocations.

Boundary: children are evaluated eagerly (pre-rendered before dispatch). .Use() is for gating leaf components and micro-interactions — not massive page layouts. Use HTTP router middleware for route-level protection.


HTMX — first-class support (100% of the 2.0 spec)

nanite-render has full, native support for the HTMX 2.0 request/response contract — request detection, OOB swaps, every trigger lifecycle, every server-driven response header. The router remains in charge of the wire; the renderer is purely about producing the right HTMX-aware bytes and headers for any given request.

Direct component dispatch (hx-get / hx-post)

Render one named component to a ByteWriter without a parent template or layout — the handler for hx-get="/posts/123/card":

err := reg.RenderComponent(bw, rc, "CARD", props)

Missing name is a no-op (no error, no output) — stale HTMX targets don't crash the handler.

Out-of-Band swaps (hx-swap-oob="true")

Opt a component in with an explicit target DOM id. When the component's render mutates state via SetState/UseState, its output is wrapped in <div id="<id>" hx-swap-oob="true">…</div> and emitted automatically. Clean renders write through unchanged — no buffer copy on the no-op path:

cr.Define("CARD").
    WithOOB("card-slot").                            // required target id
    Render(func(c *render.ComponentContext) error {
        _, set := c.UseState("count", 0)
        set(c.GetState("count").(int) + 1)          // marks frame dirty
        return c.WriteString("<div class='card'>…</div>")
    }).
    Register(cr)

The render buffer comes from a sync.Pool of bytes.Buffer — zero steady-state allocations even on the dirty path. The wrapper is HTML-escaped.

rc.SetOOBSink(w) redirects the OOB output to a separate writer when the router wants to interleave OOB swaps with a different stream.

HTMX request detection

Pure helpers over *http.Request, nil-safe:

render.IsHTMXRequest(r)              // canonical: HX-Request: true
render.IsHTMXBoosted(r)              // HX-Boosted
render.IsHTMXHistoryRestore(r)       // HX-History-Restore-Request
render.HXTargetID(r)                 // id of the targeted element
render.HXTriggerID(r)                // id of the triggered element
render.TriggerName(r)                // name attribute of the trigger
render.CurrentURL(r)                 // URL of the page that initiated the request
render.HXPromptResponse(r)           // user's answer to an hx-prompt dialog

A typical handler branches on these:

if render.IsHTMXHistoryRestore(r) {
    // Serve the full page; HTMX is rebuilding its history snapshot.
    reg.RenderPage(rc, "layout", "view", data)
} else if render.IsHTMXRequest(r) {
    // Targeted swap: render one component, then write HTMX headers.
    reg.RenderComponent(bw, rc, "CARD", props)
} else {
    // Full page load.
    reg.RenderPage(rc, "layout", "view", data)
}
Trigger events (HX-Trigger, HX-Trigger-After-Swap, HX-Trigger-After-Settle)

Components accumulate event names during their render. The router writes the joined set into the response headers — but rc.WriteHTMXHeaders(w) does it in one call:

// In the component:
c.AddHXTrigger("card-updated")
c.AddHXTriggerWithDetail("item-changed", map[string]any{"id": 42})
c.AddHXTriggerAfterSwap("settled")
c.AddHXTriggerAfterSettle("done")

// In the handler:
rc.WriteHTMXHeaders(w)

AddHXTriggerWithDetail upgrades an entry to carry JSON detail. The format is automatic: plain comma-separated names when no entry has detail, JSON object otherwise — matching HTMX's wire format:

HX-Trigger: card-updated,nav-refresh                          # plain
HX-Trigger: {"item-changed":{"id":42},"card-updated":null}   # JSON detail
Server-driven response decisions

Every HX-* decision header is one setter on RenderContext. Apply them all in one call at the end of the handler:

rc.SetHXRetarget("#main-panel")
rc.SetHXReswap("outerHTML swap:200ms")
rc.SetHXPushURL("true")             // or a URL
rc.SetHXRedirect("/done")           // full URL required
rc.SetHXRefresh()                   // full client refresh
rc.SetHXReplaceURL("/current")      // or "true"
rc.SetHXLocation("/alt")            // navigation without history
rc.SetHXReselect("#content")        // CSS selector for response extraction
rc.SetHXResettle("true")            // "true" forces scroll reset

rc.WriteHTMXHeaders(w)              // emits only the headers actually set

Every header is covered:

Header Setter Constant
HX-Trigger AddHXTrigger / AddHXTriggerWithDetail render.HXTriggerHeader
HX-Trigger-After-Swap AddHXTriggerAfterSwap render.HXTriggerAfterSwap
HX-Trigger-After-Settle AddHXTriggerAfterSettle render.HXTriggerAfterSettle
HX-Retarget SetHXRetarget render.HXRetarget
HX-Reswap SetHXReswap render.HXReswapHeader
HX-Push-Url SetHXPushURL render.HXPushURL
HX-Redirect SetHXRedirect render.HXRedirect
HX-Refresh SetHXRefresh render.HXRefresh
HX-Replace-Url SetHXReplaceURL render.HXReplaceURL
HX-Location SetHXLocation render.HXLocation
HX-Reselect SetHXReselect render.HXReselect
HX-Resettle SetHXResettle render.HXResettle
Auto-OOB → auto-trigger

When a WithOOB component's render dirties state, the lowercased component name is automatically added to HX-Trigger. Components that want different event names call AddHXTrigger explicitly — both coexist (dedup keeps the set clean):

// CARD with WithOOB("card-slot") that mutates state →
// HX-Trigger: card     (auto, from name)
// HX-Trigger: custom-event  (if c.AddHXTrigger("custom-event") also called)
Complete handler pattern

The full router integration in one function:

func PostHandler(w http.ResponseWriter, r *http.Request) {
    bw := render.AcquireWriter(w)
    defer render.ReleaseWriter(bw)
    rc := render.AcquireContext(bw, r)
    defer render.ReleaseContext(rc)

    if render.IsHTMXHistoryRestore(r) {
        reg.RenderPage(rc, "layout", "view", nil)
    } else if render.IsHTMXRequest(r) {
        reg.RenderComponent(bw, rc, "CARD", postData)
        // Component already added "card" to rc.HXTriggers via WithOOB.
        rc.SetHXRetarget("#post-feed")
        rc.SetHXReswap("outerHTML settle:200ms")
    } else {
        reg.RenderPage(rc, "layout", "view", postData)
    }

    // Writes HX-Trigger, HX-Retarget, HX-Reswap — only the headers
    // actually set during the handler.
    rc.WriteHTMXHeaders(w)
}

One call to WriteHTMXHeaders writes all accumulated HTMX decisions; the router stays in charge of the wire and lifecycle.


Sentinel errors

Routers translate errors to HTTP responses with errors.Is:

var (
    ErrEngineNotFound    // unknown engine → 500
    ErrTemplateNotFound  // missing template → 404
    ErrLoaderMissing     // no loader on context → 500
    ErrLayoutMissing     // required layout absent → 500
    ErrRenderPageInvalid // invalid RenderPage args → 500
)

License

MIT — see LICENSE. Copyright (c) 2026 xDarkicex.

Documentation

Index

Constants

View Source
const (
	// HXRequest is set to "true" on every HTMX request. Its presence
	// is the canonical "this is an HTMX request" signal.
	HXRequest = "HX-Request"

	// HXBoosted is set to "true" when the request came from an
	// hx-boosted element (typically an <a> or <form>). Boosted
	// requests render only the response body, not the full page.
	HXBoosted = "HX-Boosted"

	// HXHistoryRestoreRequest is set to "true" when HTMX is taking
	// a snapshot of the page (history restoration on navigation).
	// Servers should render the full page, not a partial swap.
	HXHistoryRestoreRequest = "HX-History-Restore-Request"

	// HXTarget is the id of the element targeted by the request.
	HXTarget = "HX-Target"

	// HXTrigger is the id of the triggered element (when the request
	// was triggered by an event, not a load).
	HXTrigger = "HX-Trigger"

	// HXTriggerName is the name attribute of the triggered element.
	HXTriggerName = "HX-Trigger-Name"

	// HXCurrentURL is the URL of the page the browser was on when
	// the request was made. Useful for server-side history tracking.
	HXCurrentURL = "HX-Current-URL"

	// HXPrompt is the user's response to an hx-prompt dialog.
	HXPrompt = "HX-Prompt"
)

HTMX request header constants. HTMX sets these on every request it makes; servers use them to differentiate HTMX-driven requests from full-page loads.

See https://htmx.org/reference/#request_headers for the full list.

View Source
const (
	// HXTriggerHeader (response) tells HTMX to trigger client-side
	// events after the response is swapped. Comma-separated list of
	// event names. nanite-render accumulates triggered events on
	// rc.hxTriggers during a render; the router copies the joined
	// list into this header.
	HXTriggerHeader = "HX-Trigger"

	// HXTriggerAfterSwap fires after the swap step.
	HXTriggerAfterSwap = "HX-Trigger-After-Swap"

	// HXTriggerAfterSettle fires after the settle step.
	HXTriggerAfterSettle = "HX-Trigger-After-Settle"

	// HXRetarget overrides hx-target on the triggered element.
	HXRetarget = "HX-Retarget"

	// HXReswapHeader overrides hx-swap on the triggered element.
	HXReswapHeader = "HX-Reswap"

	// HXPushURL pushes a new URL into the history stack.
	HXPushURL = "HX-Push-Url"

	// HXRedirect triggers a client-side redirect.
	HXRedirect = "HX-Redirect"

	// HXRefresh makes the client do a full page refresh.
	HXRefresh = "HX-Refresh"

	// HXReplaceURL replaces the current URL without history entry.
	HXReplaceURL = "HX-Replace-Url"

	// HXLocation triggers client-side navigation without history.
	HXLocation = "HX-Location"

	// HXReselect is a CSS selector telling HTMX which part of the
	// response body to extract and swap. Useful when the server
	// returns a wrapper page (e.g. layout) and only an inner region
	// should land in the DOM.
	HXReselect = "HX-Reselect"

	// HXResettle controls the settle algorithm after the swap.
	// "true" forces scroll reset to the top; "false" (default) tries
	// to preserve scroll position across the swap.
	HXResettle = "HX-Resettle"
)

HTMX response header constants. The server sets these to drive HTMX client behavior.

Where the same header is used for both directions (e.g. HX-Trigger) the request constant (HXTrigger) and the response constant share the same value; the alias HXTriggerHeader is provided for code that handles both sides and wants to make the direction explicit.

View Source
const (
	FlagVoid        uint8 = 1 << 0 // <br>, <img>, <input>
	FlagSelfClosing uint8 = 1 << 1 // <MyComponent/>
	FlagComponent   uint8 = 1 << 2 // templ component
	FlagFragment    uint8 = 1 << 3 // <template> / no parent
	FlagRaw         uint8 = 1 << 4 // text not escaped
	FlagText        uint8 = 1 << 5 // synthetic text node
	FlagDynamicTag  uint8 = 1 << 6 // <{tag}>, tag chosen at render
	FlagHasChildren uint8 = 1 << 7 // hot-loop guard for child walk
)

Node flags. Stuffed in a uint8 so the Flags slice is one byte per node.

View Source
const ChildrenDataKey = "__children__"

ChildrenDataKey is the data-map key where the dispatcher stores a component's pre-rendered children. Components that compose via data maps can read this key to inject the children into their output:

cr.Register("Parent", render.ComponentFunc(
    func(w, rc, data) error {
        children := data[render.ChildrenDataKey].(render.Children)
        for _, child := range children {
            w.WriteString(child)
        }
        return nil
    },
))
View Source
const ContextKey = "render"

ContextKey is the key used to store the *RenderContext on the nanite.Context's Values map (or any compat router's context map). Re-exported as a public constant so external callers can read the context back without stringly-typed map access.

View Source
const DefaultActionPrefix = "/_nano/action/"

DefaultActionPrefix is the default URL prefix for colocated server actions. Actions are mounted at {prefix}/{COMPONENT}/{action} — e.g. /_nano/action/LIKE_BUTTON/toggle. Override with WithActionPrefix.

View Source
const SlotsDataKey = "__slots__"

SlotsDataKey is the data-map key where the dispatcher stores the slots map. Components access this via data[SlotsDataKey].

Variables

View Source
var (
	// ErrEngineNotFound is returned when the requested engine is
	// not registered with the Registry. Likely a misconfiguration
	// in the boot path; usually indicates a 500.
	ErrEngineNotFound = errors.New("render: engine not found")

	// ErrTemplateNotFound is returned when the loader cannot find
	// the template. Suitable for 404 responses.
	ErrTemplateNotFound = errors.New("render: template not found")

	// ErrLoaderMissing is returned when no template loader is
	// configured on the RenderContext. Indicates the middleware
	// wasn't installed or the handler is missing setup.
	ErrLoaderMissing = errors.New("render: no loader on context")

	// ErrLayoutMissing is returned when a layout is required but
	// not provided. Misconfiguration; usually 500.
	ErrLayoutMissing = errors.New("render: layout missing")

	// ErrRenderPageInvalid is returned when RenderPage is called
	// with invalid arguments (empty layout or view, missing data).
	ErrRenderPageInvalid = errors.New("render: invalid render page")
)

Sentinel errors. Routers can match these via errors.Is to translate framework errors into HTTP responses:

switch {
case errors.Is(err, render.ErrTemplateNotFound):
    c.JSON(404, ...)
case errors.Is(err, render.ErrEngineNotFound):
    c.JSON(500, "engine not registered")
default:
    c.JSON(500, err.Error())
}
View Source
var ErrValidation = errors.New("render: validation error")

ErrValidation is the sentinel error an Action returns to signal a validation failure. HandleAction intercepts it: instead of returning 500, it re-renders the component with the form errors already set on the RenderContext (via rc.SetFormError) and returns 200 so HTMX swaps the form with the inline error spans.

Match with errors.Is — actions may wrap it:

return fmt.Errorf("submit: %w", render.ErrValidation)

Functions

func BindProps

func BindProps[T any](data any) T

BindProps unmarshals a data map (or any value) into a typed struct T. It uses `nanite:"key"` struct tags to map fields to data-map keys. The default key is the lowercased field name.

Implementation: zero heap allocations on the hot path. The struct T is declared on the stack; per-type field layouts (offsets + reflect.Type) are cached in a sync.Map keyed by reflect.Type, so reflection runs once per distinct T.

Scalar fields (string, bool, int*, uint*, float*) are written directly via unsafe.Pointer arithmetic — no reflect.Value.Set, no boxing, no per-call reflection. Complex fields (slices, maps, pointers, nested structs, custom interface types) fall back to reflect.Value.Set which can allocate. Typical prop structs are mostly scalars; complex fields are the exception.

Zero-value semantics: missing keys leave the field at its zero value. Type mismatches leave the field at its zero value.

Usage:

type NavbarProps struct {
    Theme string `nanite:"theme"`
    Size  int    `nanite:"size"`
}

cr.Register("NAVBAR", render.ComponentFunc(
    func(w, rc, data) error {
        props := render.BindProps[NavbarProps](data)
        return nil
    },
))

func CheckConditional

func CheckConditional(rc *RenderContext, p *Program) bool

CheckConditional compares the program's ETag against the request's If-None-Match header. If it matches, the function writes a 304 response (with appropriate headers) and returns true.

func ClearRenderState

func ClearRenderState()

ClearRenderState clears the per-render state. Engine adapters call this after Execute completes.

func CurrentURL

func CurrentURL(r *http.Request) string

CurrentURL returns the URL of the page that initiated the request. Useful for server-side redirect logic that needs to know where the user came from.

func Execute

func Execute(p *Program, w ByteWriter, rc *RenderContext, data any) error

Execute renders the program to w. When the program carries a compiled Bytecode (see CompileBytecode), the flat instruction stream is executed — static HTML is coalesced into a few writes, matching templ's compile-to-output speed. Otherwise the NodeStream is walked depth-first via the FirstChild/NextSibling links (linear-time, zero-alloc for hand-built streams).

func HXPromptResponse

func HXPromptResponse(r *http.Request) string

HXPromptResponse returns the user's response to an hx-prompt dialog. Empty when no prompt was shown.

func HXTargetID

func HXTargetID(r *http.Request) string

HXTargetID returns the id of the element HTMX is targeting. Empty when no hx-target was specified (HTMX defaults to the triggering element).

func HXTriggerID

func HXTriggerID(r *http.Request) string

HXTriggerID returns the id of the element that triggered the request. Empty when the request was a load (no specific trigger).

func HashContent

func HashContent(p *Program) string

HashContent computes a strong ETag for a program. The hash is derived from the engine name, template name, and the program's content fingerprint. Compile-time; not re-computed on every render.

The computed ETag is stored on the Program.ETag field so the render path reads it with no allocation.

func IsHTMXBoosted

func IsHTMXBoosted(r *http.Request) bool

IsHTMXBoosted reports whether the request came from an hx-boost (typically a link or form that the user activated). Boosted requests should render the response body only — the layout is already on the page.

func IsHTMXHistoryRestore

func IsHTMXHistoryRestore(r *http.Request) bool

IsHTMXHistoryRestore reports whether HTMX is restoring a history snapshot. Servers should render the full page (no partial swap) so the snapshot is faithful.

func IsHTMXRequest

func IsHTMXRequest(r *http.Request) bool

IsHTMXRequest reports whether the request was made by HTMX. This is the canonical check; all other HTMX request modifiers imply IsHTMXRequest is also true.

func IsVoidTag

func IsVoidTag(tag string) bool

IsVoidTag reports whether the tag is a known void element.

func MinifyHTML

func MinifyHTML(src []byte, cfg MinifyConfig) ([]byte, error)

MinifyHTML minifies the given HTML according to the config. Used by engines at compile time to produce the smallest possible cached program.

func Props

func Props[T any](c *ComponentContext) T

Props binds c.Data to a typed struct via nanite:"key" tags. Equivalent to render.BindProps[T](c.Data).

func ReleaseContext

func ReleaseContext(rc *RenderContext)

ReleaseContext returns the context to the pool. The associated Writer is *not* closed by this call; the caller is responsible for flushing.

func ReleasePerFlushWriter

func ReleasePerFlushWriter(pw ByteWriter)

ReleasePerFlushWriter returns pw to the pool. Flushes any remaining bytes through to the parent. Safe to call with nil.

func ReleaseWriter

func ReleaseWriter(w ByteWriter)

ReleaseWriter returns w to the pool. The write buffer is retained for the next user.

func RenderComponent

func RenderComponent(ctx context.Context, w io.Writer, name string, props any) error

RenderComponent renders a named, registered component inline, writing to w. It recovers the *RenderContext from ctx (see WithContext), so a templ component can compose a nanite-render component like this:

rc := render.FromContext(ctx)
render.RenderComponent(ctx, w, "Navbar", props)

w may be any io.Writer (templ components receive io.Writer); if it is not a ByteWriter, a pooled writer is used and released.

func SetFlusher

func SetFlusher(w ByteWriter, f http.Flusher)

SetFlusher explicitly attaches an http.Flusher. Use this from middleware that wraps the underlying writer (e.g. gzip, compression) AFTER AcquireWriter has already run — the wrapper still satisfies http.Flusher even though the original writer was wrapped. Passing nil clears the flusher.

This is a no-op when the writer is not a *byteWriter (other writers in the ByteWriter interface have their own Flush implementations).

func SetRenderState

func SetRenderState(rc *RenderContext)

SetRenderState populates the per-render state. Engine adapters that use html/template (jade, HTMLTemplate) call this before Execute and call ClearRenderState after.

func SuperpowerFuncs

func SuperpowerFuncs() template.FuncMap

SuperpowerFuncs returns a FuncMap that exposes the framework's cross-engine superpowers: `component` and `yield`. Engines that use html/template (jade, the HTMLTemplate adapter) inject this into their template compilation.

func TriggerName

func TriggerName(r *http.Request) string

TriggerName returns the name attribute of the triggered element. Useful for forms whose submit handler depends on the input name.

func UseContextFunc

func UseContextFunc(rc *RenderContext) any

UseContextFunc returns a template.FuncMap entry that resolves the cascading-context stack. Register it via WithFuncMap so templates can call {{ useContext "theme" }} and get the nearest binding. The result is `any` — the template type-asserts.

Usage:

reg := render.New(render.WithFuncMap(render.UseContextFunc))
// or merge into your own builder:
fm := template.FuncMap{"useContext": render.UseContextFunc}

Templates that don't go through the per-request FuncMap won't see this helper — but fluent components use ComponentContext directly.

func UseState

func UseState[T any](s *State, key string, initial T) (T, func(T))

UseState is a generic, type-safe wrapper around State.UseState. It returns the value (typed T) and a setter. The setter writes the new value back to the same key. This is the React-style useState hook for Go templates — no type assertions needed.

Usage in a component:

count, setCount := render.UseState(state, "count", 0)
setCount(count + 1)

The first call returns initial and stores it. Subsequent calls return the stored value. The setter always writes the typed value.

func UseStateOrDefault

func UseStateOrDefault[T any](s *State, key string) (T, bool)

UseStateOrDefault returns the value for the given key if it is set and is of type T. The boolean indicates whether both the key was present and the type assertion succeeded.

func WithContext

func WithContext(ctx context.Context, rc *RenderContext) context.Context

WithContext returns a context carrying the given *RenderContext. Engine adapters that render through a context.Context (notably templ, whose components receive one) use this so components can reach the framework's runtime layer.

A templ component can recover the framework state like this:

rc := render.FromContext(ctx)
count, setCount := render.UseState(rc.ComponentState(), "count", 0)
slots, _ := rc.Slots  // or data[SlotsDataKey]

Types

type ActionFunc

type ActionFunc func(rc *RenderContext, props map[string]any) error

ActionFunc is a colocated server action: mutation logic that lives next to the component that triggers it. Invoked by Registry.HandleAction when the action's URL is POSTed.

props is the parsed request body — JSON bodies decode into a map[string]any with real types (BindProps-compatible); form bodies get best-effort conversion ("42" → int, "true" → bool, else string).

The state contract: this framework does NOT persist state across requests. The action mutates the user's own storage (DB, session, cookie), and the subsequent re-render of the component is a pure function of (props, request). Per-request state set here (rc.SetState) IS visible to the re-render that follows in the same request.

type ActionProvider

type ActionProvider interface {
	Component
	LookupAction(name string) (ActionFunc, bool)
}

ActionProvider is an optional interface a Component implements to expose colocated server actions. Engines that pre-compile components can implement it directly; the fluent builder exposes it via Definition.Action.

type AsyncOptioner

type AsyncOptioner interface {
	Component
	AsyncFallback() (id string, render func(w ByteWriter, rc *RenderContext, data any) error)
}

AsyncOptioner is an optional interface a Component implements to participate in Suspense-style asynchronous rendering. Components that opt in via AsyncFallback() have their render executed in a background goroutine; the executor writes the fallback HTML inline (TTFB ≈ 0ms for that region), then emits the real output as a trailing OOB chunk when the worker finishes.

Suspense trades Sum(component_times) for Max(component_times) on independent components. It requires an http.Flusher-compatible response writer; non-flushing writers fall back to synchronous rendering of the slow component.

Engines that pre-compile components can implement AsyncOptioner directly. The fluent builder exposes this via Definition.Async().Fallback(fn).

type AttrKV

type AttrKV struct {
	K, V string
}

AttrKV is a (key, value) pair used during parsing. Exported so the parser can pass a slice of these to SetAttrsEach without needing intermediate []string / []any slices.

type ByteWriter

type ByteWriter interface {
	io.Writer
	io.ByteWriter
	io.StringWriter

	// Flush drains the internal buffer into the wrapped writer.
	Flush() error

	// Reset re-targets the writer at a new underlying io.Writer. Used
	// when the writer is reused across requests.
	Reset(w io.Writer)

	// Bytes returns the current buffer contents. Intended for direct
	// chunked writes (e.g. SSE) rather than the typical render path.
	Bytes() []byte
}

ByteWriter is the interface render writes through. Implementations are pooled; Flush returns the writer to its pool after draining the underlying io.Writer.

func AcquirePerFlushWriter

func AcquirePerFlushWriter(parent ByteWriter, threshold int) ByteWriter

AcquirePerFlushWriter returns a per-flush writer wrapping parent. When threshold bytes accumulate, the writer auto-flushes to parent. A threshold of 0 disables auto-flush (writes pass through directly to parent). A negative threshold is treated as 0.

Callers must ReleasePerFlushWriter when done. The wrapper is safe to use as a ByteWriter; its Reset/Bytes methods delegate to parent.

func AcquireWriter

func AcquireWriter(w io.Writer) ByteWriter

AcquireWriter returns a ByteWriter targeting w. The writer is reused from the pool; callers must Flush and ReleaseWriter when done. The internal buffer is reset to len 0 so previous renders do not leak into the new render.

If w implements http.Flusher (e.g. a net/http response writer), the flusher is captured once. Subsequent Flush() calls push the buffered bytes to the underlying writer AND signal the flusher, so chunked HTTP responses emit each chunk as it is produced.

type Bytecode

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

Bytecode is a flat, zero-allocation instruction stream compiled from a NodeStream at compile time.

Static HTML (tags, attributes, text, and HTML escaping) is coalesced into a single output buffer at compile time — exactly what templ's compiler does — so a static-heavy template renders as a handful of OP_STATIC writes instead of a per-node tree walk. The render loop is a tight switch over a []uint64, maximising cache-line efficiency and branch predictability.

Dynamic points (components, slots) remain explicit instructions, so the bytecode supports the full composition layer (state, children, slots, memoization) on top of the same ComponentContext API.

func CompileBytecode

func CompileBytecode(nodes NodeStream) *Bytecode

CompileBytecode compiles a NodeStream into a flat instruction stream. Static content is escaped and coalesced into a single output buffer; consecutive static runs emit one opStatic each. Components and slots remain dynamic instructions.

func (*Bytecode) Exec

func (bc *Bytecode) Exec(w ByteWriter, rc *RenderContext, data any) error

Exec runs the bytecode, writing the rendered output to w.

type Cache

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

Cache is a high-performance, lock-free, off-heap program cache.

The slot array (seq/hash/tagBits) is mmap'd via memory.MmapAnonymous so the Go GC never scans the hot synchronization state. Each slot is 24 bytes, so the read path touches at most one cache line per probe.

The *Program references live in the Go-heap `programs` array ([]atomic.Pointer[Program]) — the same split liteLRU uses: off-heap for the bitmask/seqlock sync state, Go-heap for the pointers the GC must see to keep objects alive. A raw uintptr in off-heap memory would let the GC collect the Program (use-after-free), and pinning it (runtime.Pinner) is not lock-free — so the GC-visible reference array is the lock-free-correct design. Get remains a lock-free atomic.Pointer.Load with zero allocations.

Concurrency model:

  • Get: lock-free. Probes off-heap seq+hash; on a match, loads the program via the Go-heap atomic.Pointer.
  • Put: CAS-loop. Increments seq (locks the slot), writes hash, stores the program pointer on the heap, increments seq (unlocks).
  • Delete: same as Put with hash=0.

Cache keys are (engine, name) pairs. The 64-bit hash is computed inline; no string concat is performed on the hot path.

func NewCache

func NewCache(capacity int) *Cache

NewCache returns a Cache with the given capacity. Capacity is rounded up to the next power of two.

func (*Cache) Close

func (c *Cache) Close()

Close releases the off-heap memory. Programs remain referenced by the Go-heap `programs` array; the cache must not be used after Close.

func (*Cache) Delete

func (c *Cache) Delete(engine, name string)

Delete removes the cached program for the (engine, name) pair.

func (*Cache) Get

func (c *Cache) Get(engine, name string) (*Program, bool)

Get returns the cached program for the (engine, name) pair.

func (*Cache) InvalidateTag

func (c *Cache) InvalidateTag(tag string) int

InvalidateTag drops every cached entry whose tag bit matches.

func (*Cache) Put

func (c *Cache) Put(engine, name string, p *Program)

Put inserts a program into the cache under the (engine, name) pair.

func (*Cache) SetTag

func (c *Cache) SetTag(engine, name string, tags ...string)

SetTag associates a (engine, name) pair with the given tags. Tags are stored in the slot's tag bitmask. Up to 64 distinct tags per slot. Tag names are FNV-1a hashed into bit positions 0..63.

func (*Cache) Stats

func (c *Cache) Stats() CacheStats

Stats returns the cache-level stats.

type CacheStats

type CacheStats struct {
	Hits     int64
	Misses   int64
	Evicts   int64
	Capacity int
}

CacheStats is the cache-level stats.

type Children

type Children []string

Children is the pre-rendered HTML of a component's children. Each entry is a string chunk that the parent component can inject into its layout (React-style composition).

type Component

type Component interface {
	Render(w ByteWriter, rc *RenderContext, data any) error
}

Component is a renderable component. Components are registered by name and referenced from templates via `<NAVBAR data={...} />` (temmpl), `+navbar(data)` (jade), or `{{ template "navbar" . }}` (html/template).

The Component interface is engine-agnostic: each engine adapter translates its native component syntax into a Component reference in the NodeStream. The executor dispatches to the registered Component when the node has one set.

type ComponentContext

type ComponentContext struct {
	// Writer is the ByteWriter render writes to.
	Writer ByteWriter
	// Context is the per-request RenderContext.
	Context *RenderContext
	// Data is the user-supplied data passed to Render or RenderPage.
	Data any
	// Children is the pre-rendered HTML of the component's slot-less
	// children (slot children live in Slots).
	Children Children
	// Slots maps slot name to the pre-rendered HTML of the slot's
	// children.
	Slots Slots
	// State is the per-render state. Use for useState-style hooks.
	State *State
	// DataMap is the data map (may be nil). The component can
	// read/modify it for child propagation.
	DataMap map[string]any
	// contains filtered or unexported fields
}

ComponentContext bundles all the render arguments into a single value. Components written with the fluent API receive a ComponentContext instead of the multi-arg ComponentFunc signature. The included State and Children/Slots are read from the render state set by the dispatcher.

func (*ComponentContext) ActionURL

func (c *ComponentContext) ActionURL(action string) string

ActionURL builds the URL for a colocated server action on this component:

{prefix}/{COMPONENT}/{action}

e.g. with the default prefix:

c.ActionURL("toggle") // "/_nano/action/LIKE_BUTTON/toggle"

The prefix is configurable via WithActionPrefix; the URL is safe for hx-post / form action attributes. Returns "" for a nil receiver or empty action name.

func (*ComponentContext) AddHXTrigger

func (c *ComponentContext) AddHXTrigger(name string)

AddHXTrigger records an HTMX client-side event to fire after this render completes. Convenience over RenderContext.AddHXTrigger for component authors. Safe to call with nil receiver.

func (*ComponentContext) AddHXTriggerAfterSettle

func (c *ComponentContext) AddHXTriggerAfterSettle(name string)

AddHXTriggerAfterSettle records an HTMX event to fire after the settle step. Convenience over RenderContext.AddHXTriggerAfterSettle.

func (*ComponentContext) AddHXTriggerAfterSwap

func (c *ComponentContext) AddHXTriggerAfterSwap(name string)

AddHXTriggerAfterSwap records an HTMX event to fire after the swap step. Convenience over RenderContext.AddHXTriggerAfterSwap.

func (*ComponentContext) AddHXTriggerWithDetail

func (c *ComponentContext) AddHXTriggerWithDetail(name string, detail any)

AddHXTriggerWithDetail records an event with a JSON-encodable detail payload. Convenience over RenderContext.AddHXTriggerWithDetail.

func (*ComponentContext) AddMeta

func (c *ComponentContext) AddMeta(name, content string)

AddMeta adds a <meta name="..." content="..."> tag for this request, emitted by the layout's <NANO_HEAD/> on full page loads. Last write wins per name.

func (*ComponentContext) GetFormError

func (c *ComponentContext) GetFormError(key string) string

GetFormError returns the flash validation error for a form field, or "" if none was set for this request.

func (*ComponentContext) GetState

func (c *ComponentContext) GetState(key string) (any, bool)

GetState reads a value from the per-render state.

func (*ComponentContext) ProvideContext

func (c *ComponentContext) ProvideContext(key string, val any)

ProvideContext pushes (key, val) onto the cascading-context stack. The binding is visible to this component and every component it renders (children, sub-children, …) until the component's render scope exits — the dispatcher automatically pops the stack back to savedCtxPtr on return, so bindings don't leak into siblings or parent frames.

Shadowing: an inner ProvideContext("theme", "light") overrides an outer binding for the duration of the inner component's scope. Lookup is reverse-scan (newest first), so the nearest binding wins.

No hard cap: bindings beyond the inline fast-path spill to a heap slice so genuinely deep trees still work. There's no panic — if you need hundreds of nested ambient contexts you're probably doing it wrong, but the framework won't stop you.

Nil-safe: a nil receiver is a no-op.

func (*ComponentContext) Render

func (c *ComponentContext) Render(name string, props any) error

Render dispatches a named, registered component inline, writing to this context's writer. The component receives a fresh ComponentContext (same state, its own data). This is the `{{ component "Name" . }}` superpower as a method — usable from any component on any engine.

func (*ComponentContext) RequiresCSS

func (c *ComponentContext) RequiresCSS(href string)

RequiresCSS declares a CSS dependency for this component. The path is emitted once (deduplicated) by the layout's <NANO_ASSETS/> as a <link rel="stylesheet"> tag. Safe to call from components rendered in loops — the head gets one tag.

func (*ComponentContext) RequiresJS

func (c *ComponentContext) RequiresJS(src string)

RequiresJS declares a JS dependency for this component. The path is emitted once (deduplicated) by the layout's <NANO_ASSETS/> as a <script defer> tag.

func (*ComponentContext) SetFormError

func (c *ComponentContext) SetFormError(key, msg string)

SetFormError records a flash validation error for a form field. During a server-action re-render, the error is readable via GetFormError and rendered inline. Flash semantics: the error lives for this request only — cleared on pool reuse.

func (*ComponentContext) SetState

func (c *ComponentContext) SetState(key string, v any)

SetState writes a value to the per-render state. When this context is bound to a component declared with WithOOB, SetState also marks the frame dirty so the render wrapper emits an HTMX OOB swap.

func (*ComponentContext) SetTitle

func (c *ComponentContext) SetTitle(title string)

SetTitle sets the document <title> for this request. On full page loads the view renders before the layout, so the layout's <NANO_HEAD/> (or {{ nanoHead }}) emits it. On HTMX partial swaps, write `<title>` directly to the output instead — HTMX hoists it from the response body.

func (*ComponentContext) UseContext

func (c *ComponentContext) UseContext(key string) any

UseContext returns the value bound to the nearest matching key in the cascading-context stack, or nil if no binding exists. The caller type-asserts the result:

theme := c.UseContext("theme").(string)

Nil-safe for the receiver.

func (*ComponentContext) UseId

func (c *ComponentContext) UseId() string

UseId returns a unique per-request identifier ("nano-1", …) for pairing <label for> with <input id>. Zero-alloc for the first 256 ids.

func (*ComponentContext) UseState

func (c *ComponentContext) UseState(key string, initial any) (any, func(any))

UseState is the React-style hook. It returns the current value for key (initial on first use) and a setter that writes back. The setter affects subsequent renders; the current render keeps its value.

For a typed variant use render.UseState[T](c.State, key, initial).

When this context is bound to a component declared with WithOOB, the setter also marks the frame dirty so the render wrapper emits an HTMX OOB swap.

func (*ComponentContext) Write

func (c *ComponentContext) Write(p []byte) (int, error)

Write writes a byte slice to the output. Convenience over Writer.

func (*ComponentContext) WriteChildren

func (c *ComponentContext) WriteChildren()

WriteChildren writes all slot-less children to the output. The component controls the placement; the framework just provides the pre-rendered HTML.

func (*ComponentContext) WriteHydrateProps

func (c *ComponentContext) WriteHydrateProps(attr string, props any) error

WriteHydrateProps serializes props as JSON into a single-quoted HTML attribute — the hydration bridge for Alpine.js (x-data), HTMX extensions, and vanilla JS:

c.WriteHydrateProps("x-data", props)
// writes: x-data='{"min":0,"max":100}'

The JSON is written inside single quotes (JSON contains " but not '), and every byte is HTML-escaped via escapeBytes — the browser decodes entities when reading the attribute, so the client-side JS sees the exact JSON. Safe against quote/script injection in values.

Allocation note: encoding/json.Marshal allocates — this is a CORRECTNESS helper (proper escaping, no manual string concat), not a zero-alloc one. It runs once per component per render. The escape step streams the bytes directly (no string(b) conversion) so no extra allocation is added on our side.

Returns an error if props cannot be marshaled (e.g. channels, functions, cyclic values).

func (*ComponentContext) WriteSlot

func (c *ComponentContext) WriteSlot(name string)

WriteSlot writes the named slot's content to the output. If the slot is not present, nothing is written.

func (*ComponentContext) WriteString

func (c *ComponentContext) WriteString(s string) (int, error)

WriteString writes a string to the output.

func (*ComponentContext) Yield

func (c *ComponentContext) Yield() error

Yield writes the pre-rendered view body (rc.ViewBytes) to the output — the layout composition hook. In the two-pass page pipeline, RenderPage renders the view first and stashes the bytes on the RenderContext; layouts call Yield where the view body should appear. Emitted by the gsx @yield directive.

Standalone renders (no RenderPage) have empty ViewBytes — Yield writes nothing.

type ComponentFunc

type ComponentFunc func(w ByteWriter, rc *RenderContext, data any) error

ComponentFunc is a function adapter for Component.

func (ComponentFunc) Render

func (f ComponentFunc) Render(w ByteWriter, rc *RenderContext, data any) error

Render implements Component.

func (ComponentFunc) WithFuncs

func (f ComponentFunc) WithFuncs(funcs template.FuncMap) Component

WithFuncs returns a Component that wraps the ComponentFunc with the given FuncMap. Use it when registering a component:

cr.Register("NAVBAR", render.ComponentFunc(myFunc).WithFuncs(
    template.FuncMap{
        "currentUser": func() any { return loadUser() },
    },
))

type ComponentMiddleware

type ComponentMiddleware func(ComponentRenderFunc) ComponentRenderFunc

ComponentMiddleware is a Higher-Order Component wrapper in the React withAuth(Widget) style. It receives the next render function and returns a wrapped one:

func RequireAdmin(next render.ComponentRenderFunc) render.ComponentRenderFunc {
    return func(c *render.ComponentContext) error {
        if !isAdmin(c) {
            return nil // abort — component renders nothing
        }
        return next(c)
    }
}
cr.Define("ADMIN_BUTTON").Use(RequireAdmin).Render(...)

The chain is folded ONCE at Register time — the hot render path calls a single function pointer, zero iteration, zero allocation.

Middleware wraps the component DISPATCHER, not just the render function: it runs on the main thread BEFORE the async fork, so it has full access to cascading context (c.UseContext), and an abort (not calling next) prevents the async fallback from being written and the worker from being spawned. The async worker itself runs the raw render function — middleware runs exactly once, on the main thread.

Children are evaluated eagerly (pre-rendered before dispatch), so .Use is for gating leaf components and micro-interactions — not massive page layouts. Use HTTP router middleware for route-level protection.

type ComponentRegistry

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

ComponentRegistry is the user-facing façade for components. Components are registered by name and looked up by name. The executor reaches into the registry at render time.

The registry also holds per-component memo caches (see Memoize): name → key → rendered HTML. Memoized components skip re-rendering when their key repeats, mirroring GO-Portfolio's per-part cache.

func NewComponentRegistry

func NewComponentRegistry() *ComponentRegistry

NewComponentRegistry returns an empty ComponentRegistry.

func (*ComponentRegistry) Define

func (r *ComponentRegistry) Define(name string) *Definition

Define returns a fluent Definition builder for a component. The component is registered with this ComponentRegistry when Register is called on the Definition.

Usage:

cr.Define("NAVBAR").
    WithFuncs(template.FuncMap{"currentUser": ...}).
    RenderChildren(func(c *render.ComponentContext) error {
        c.WriteString("<nav>")
        c.WriteChildren()
        c.WriteString("</nav>")
        return nil
    }).
    Register(cr)

func (*ComponentRegistry) Lookup

func (r *ComponentRegistry) Lookup(name string) (Component, bool)

Lookup returns the component registered under the given name.

func (*ComponentRegistry) Memoize

func (r *ComponentRegistry) Memoize(name string, keyer func(rc *RenderContext, data any) string)

Memoize wraps the component registered under name so its rendered HTML is cached by key. keyer maps (rc, data) to a cache key; when two renders produce the same key, the second writes the cached HTML without re-rendering the component. Return "" from keyer to disable caching for a particular render.

This is the GO-Portfolio per-part cache applied at the component level. Use it for components whose output is data-independent or keyable by a small digest (e.g. a heavily reused navbar or a component that renders an expensive query result keyed by an id).

cr.Memoize("Navbar", func(rc *render.RenderContext, data any) string {
    return "static" // data-independent → render once, cache forever
})
cr.Memoize("UserCard", func(rc *render.RenderContext, data any) string {
    return data.(UserCardProps).ID
})

func (*ComponentRegistry) Names

func (r *ComponentRegistry) Names() []string

Names returns the registered component names.

func (*ComponentRegistry) Register

func (r *ComponentRegistry) Register(name string, c Component)

Register adds a component under the given name.

func (*ComponentRegistry) Unregister

func (r *ComponentRegistry) Unregister(name string)

Unregister removes a component from the registry. Used internally to clean up after RenderPage.

type ComponentRenderFunc

type ComponentRenderFunc func(c *ComponentContext) error

ComponentRenderFunc is the function signature for the fluent component API. The receiver is a *ComponentContext that bundles the render arguments.

type ComponentWithChildren

type ComponentWithChildren interface {
	Component
	RenderWithChildren(w ByteWriter, rc *RenderContext, data any, children Children) error
}

ComponentWithChildren is an optional interface a Component can implement to receive pre-rendered children. The children are computed by the dispatcher (the SoA executor walks the parent node's children; the html/template engine receives the rendered inner content). A component that implements this interface gets a direct call with the children list.

type ComponentWithFuncs

type ComponentWithFuncs interface {
	Component
	ComponentFuncs() template.FuncMap
}

ComponentWithFuncs is an optional interface a Component can implement to provide a component-specific FuncMap. The FuncMap is set on the RenderContext for the duration of this component's render. This is the per-component equivalent of render.SuperpowerFuncs (which is global).

Components that compose themselves via html/template (or templ) can use this to expose helpers scoped to the component.

type Composition

type Composition struct {
	Engine string
	Layout string
	View   string
	Parts  []string
}

Composition is a render request that composes a layout, view, and optional partials.

type CompressConfig

type CompressConfig struct {
	Level   int
	MinSize int
	Types   []string
}

CompressConfig controls automatic gzip compression of rendered output. Mirrors the original GO-Portfolio's hint at compression (`Transfer-Encoding: gzip, chunked`) but actually implements it.

type DataInjector

type DataInjector func(rc *RenderContext, data map[string]any)

DataInjector is a function that mutates the render data map before the engine executes. The injector is the user's seam for whatever request-scoped values need to land in the template — there is no hardcoded list of keys. Anything the user wants to inject, they inject.

Mirrors the original GO-Portfolio pattern (object["current_user"] = a.User, object["view"] = view, object["flashes"] = a.Session.Flashes()) but generalises it: any key, any value, any number of injectors registered by the user.

type Definition

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

Definition is the fluent builder for a single component. It is returned by cr.Define(name) and chained with WithFuncs, Render, and RenderChildren. The definition is finalised by calling Register(cr) on the resulting ComponentRegistry.

Usage:

cr.Define("WRAP").
    WithFuncs(template.FuncMap{"currentUser": ...}).
    RenderChildren(func(c *render.ComponentContext) error {
        c.WriteString("<div class='wrap'>")
        c.WriteChildren()
        c.WriteString("</div>")
        return nil
    }).
    Register(cr)

func (*Definition) Action

func (d *Definition) Action(name string, fn ActionFunc) *Definition

Action registers a colocated server action: mutation logic that lives next to the component. The action is dispatched by Registry.HandleAction when its URL is POSTed; after it runs, the component re-renders with the post-action state.

name is the action name — it becomes part of the URL:
{prefix}/{COMPONENT}/{name}. Use c.ActionURL(name) in the
component's Render to generate the URL.

fn receives the per-request RenderContext and the parsed
request body as props (JSON → typed map, forms → best-effort
conversion). It should mutate the user's own storage — the
framework does not persist state across requests.

The action's error contract: returning nil triggers the re-render; returning an error yields a 500 and no re-render.

func (*Definition) Async

func (d *Definition) Async() *Definition

Async marks the component for Suspense-style asynchronous rendering. Requires Fallback(fn) to set the placeholder; calling Register without a fallback when Async() is set is a no-op.

When the executor encounters an Async component:

  • It writes the Fallback output inline and flushes immediately so the browser paints the skeleton with TTFB ≈ 0ms.
  • It spawns a worker goroutine that runs Render to a pooled buffer.
  • When the worker finishes (or the request context cancels), the real output is emitted as a trailing OOB chunk via the suspense coordinator, wrapped in <div id="<component>" hx-swap-oob="true">...</div>.

Async turns Sum(component_times) into Max(component_times) for independent components. It is strictly opt-in and lazy: the coordinator allocates no channels and no goroutines unless an Async component is actually hit during the render walk.

func (*Definition) ErrorBoundary

func (d *Definition) ErrorBoundary(fn ErrorBoundaryFunc) *Definition

ErrorBoundary installs a panic recovery callback for this component. When set, the component's Render runs inside a deferred recover; a panic invokes fn with the panic value, the failed render's buffer is discarded, and fn's output replaces the failed component in the page. The rest of the render tree continues normally.

Components without ErrorBoundary run on the existing fast path (no defer/recover overhead).

fn := func(c *render.ComponentContext, err any) error {
    log.Printf("widget failed: %v", err) // safe to log
    return c.WriteString(`<div class="error">Widget unavailable</div>`)
}
cr.Define("DASHBOARD_WIDGET").ErrorBoundary(fn).Render(...)

If fn itself panics or returns an error, a generic `<!-- error boundary failed -->` placeholder is emitted so the page never crashes. This is the silent-fallback contract documented in the design.

func (*Definition) Fallback

func (d *Definition) Fallback(fn ComponentRenderFunc) *Definition

Fallback sets the synchronous placeholder rendered inline before the async component's real output is available. fn runs against the same ComponentContext as Render — it can read props, write placeholder markup, and emit any inline CSS/JS the skeleton needs.

Fallback is required when Async() is used; calling Fallback without Async() is harmless (no async dispatch happens) but the fallback function never runs.

func (*Definition) Metadata

func (d *Definition) Metadata(fn func(rc *RenderContext, data any) error) *Definition

Metadata registers a head-metadata closure that runs BEFORE the page streams (see MetadataProvider). It receives the page data and sets the document title / meta tags via rc.SetTitle / rc.AddMeta; the layout's <NANO_HEAD/> (or {{ nanoHead }}) emits them.

For the common case this is redundant: components rendered in the view can call c.SetTitle / c.AddMeta during their render — the view renders before the layout in the two-pass page pipeline, and the head state is transferred to the layout's context. Metadata is for views that are NOT registered components, or metadata that should run even if the render walk is skipped.

fn := func(rc *render.RenderContext, data any) error {
    user := data.(User)
    rc.SetTitle(user.Name + " - Profile")
    rc.AddMeta("description", "Profile page for " + user.Name)
    return nil
}
cr.Define("USER_PROFILE").Metadata(fn).Render(...)

func (*Definition) Register

func (d *Definition) Register(cr *ComponentRegistry)

Register finalises the definition and registers it with the ComponentRegistry.

func (*Definition) Render

func (d *Definition) Render(fn ComponentRenderFunc) *Definition

Render sets the render function. The function receives a ComponentContext that bundles the render arguments. The component has no children (use RenderChildren for that).

func (*Definition) RenderChildren

func (d *Definition) RenderChildren(fn ComponentRenderFunc) *Definition

RenderChildren sets the render function and indicates the component receives children. The component should call c.WriteChildren() to inject them at the desired location.

func (*Definition) Use

func (d *Definition) Use(mw ...ComponentMiddleware) *Definition

Use registers component middleware (HOC wrappers). The chain is folded once at Register time around the component's dispatcher; the first middleware registered is the OUTERMOST wrapper (closest to the framework machinery), the last is the INNERMOST (closest to the render function) — standard middleware ordering.

cr.Define("ADMIN_BUTTON").
    Use(RequireAdmin, LogRender).
    Render(...)

Middleware runs on the main thread before any async fork, has full access to cascading context, and aborts by not calling next (nothing renders, no worker spawns). See ComponentMiddleware for the full contract.

func (*Definition) WithFuncs

func (d *Definition) WithFuncs(funcs template.FuncMap) *Definition

WithFuncs declares the per-component FuncMap. The FuncMap is available to the component's Render via the render state (rc.ComponentFuncMap). It is also passed to the component as ComponentContext if the component is a ComponentWithFuncs.

func (*Definition) WithOOB

func (d *Definition) WithOOB(targetID string) *Definition

WithOOB enables HTMX Out-of-Band swap emission for this component. targetID is the DOM `id` of the element the swap replaces — it must match an `id` attribute in the page. The id is required and is a hard contract: HTMX selectors are case-sensitive and a mismatch silently breaks the swap.

When the component mutates state via SetState / UseState during its render, its output is wrapped in

<div id="<targetID>" hx-swap-oob="true">...</div>

and written to rc.OOBSink() (which falls back to rc.Writer when no explicit sink is configured). When the component does NOT mutate state, its bytes are written to the original writer unchanged.

WithOOB is strictly opt-in — components without it render exactly as before, with no buffer copy or wrapper overhead.

type Dispatchable

type Dispatchable interface {
	Component
	Dispatch(w ByteWriter, rc *RenderContext, data any, children Children) error
}

Dispatchable is an optional interface a Component implements to take full control of its dispatch: middleware wrapping, async forking, children handling. The executor and renderComponent call Dispatch INSTEAD of the async check + Render path. The fluent builder implements this via .Use().

type Engine

type Engine interface {
	Name() string
	Compile(src []byte, name string) (*Program, error)
	Execute(p *Program, w ByteWriter, rc *RenderContext, data any) error
}

Engine is the contract every template engine implements. Engines own three things: name, compile, execute.

type EngineName

type EngineName string

EngineName is a compile-time-checked engine identifier. It is a string underneath — the same value used for cache keys and the byName lookup — but the distinct type rejects arbitrary strings at the call site. A typo'd `"jad"` is a compile error; `EngineJade` is not.

Prefer the constants for the built-in engines:

reg.Page(rc).
    Engine(EngineJade).
    Layout("layouts/app").
    View("posts/show").
    Render()

Custom engines use CustomEngine(name):

reg.Page(rc).
    Engine(CustomEngine("my-engine")).
    View("home").
    Render()
const (
	EngineJade         EngineName = "jade"
	EngineHTML         EngineName = "html"
	EngineHTMLTemplate EngineName = "html-template"
	EngineTempl        EngineName = "templ"
)

Built-in engine names.

func CustomEngine

func CustomEngine(name string) EngineName

CustomEngine returns the EngineName for a user-defined engine. Because EngineName is a string type, custom engines may use any name they like — this is the escape hatch for the built-in set.

myEng := &MyEngine{}
reg.AddEngine(myEng)
reg.Page(rc).Engine(CustomEngine(myEng.Name())).View("home").Render()

func (EngineName) String

func (n EngineName) String() string

String returns the raw engine name (cache key, byName lookup).

type ErrorBoundaryFunc

type ErrorBoundaryFunc func(c *ComponentContext, err any) error

ErrorBoundaryFunc is the panic recovery callback for a component. It receives the ComponentContext (a fresh one pointing at the same underlying writer as the failed render) and the raw panic value (any — string, error, *runtime.Error, etc.). Returning nil swallows the panic and lets page rendering continue; returning a non-nil error propagates it.

The boundary's writes land in the live response, NOT in the discarded buffer the panicking render was using. This keeps the rest of the page intact — only the failed component's output is replaced.

Security note: the panic value is the raw value passed to panic(). Boundaries that print it to the response must take care not to leak internal state (DB errors with credentials, stack traces, etc.) to end users.

type Event

type Event struct {
	// Path is the file path that triggered the event, relative to
	// the watched root.
	Path string
	// Op is the fsnotify op (Create, Write, Remove, Rename, Chmod).
	Op fsnotify.Op
}

Event is a single invalidation signal sent to subscribers.

type FuncMapBuilder

type FuncMapBuilder func(rc *RenderContext) template.FuncMap

FuncMapBuilder is a per-request FuncMap factory. The original GO-Portfolio built a fresh FuncMap on every render — a closure capturing the request handler's locals. We honour that exactly: the builder is called on every render and returns a FuncMap that closures can capture from rc.

type HTMXResponse

type HTMXResponse struct {
	// Retarget overrides hx-target. CSS selector string.
	Retarget string
	// Reswap overrides hx-swap. e.g. "outerHTML", "innerHTML", "none".
	Reswap string
	// PushURL controls HX-Push-Url. Empty = unset. "true" pushes the
	// current URL; any other value is treated as a URL to push.
	PushURL string
	// Redirect triggers a client-side redirect to the given URL.
	Redirect string
	// Refresh triggers a full page refresh when "true".
	Refresh string
	// ReplaceURL replaces the current URL without a history entry.
	// Empty = unset. "true" replaces with the current URL; any
	// other value is treated as the replacement URL.
	ReplaceURL string
	// Location triggers client-side navigation without history.
	// Full URL required.
	Location string
	// Reselect is a CSS selector for the part of the response body
	// HTMX should swap in. Useful when the server returns a wrapper
	// and only an inner region should land.
	Reselect string
	// Resettle controls the post-swap settle algorithm. "true"
	// forces scroll reset to the top; "false" preserves scroll.
	Resettle string
}

HTMXResponse bundles server-driven HTMX response decisions. The router calls one method on RenderContext (e.g. SetHXRetarget) per decision during the handler, then calls WriteHTMXHeaders once at the end to apply everything to the response. Each field's zero value means "leave the client-side default in place."

Field reference (HTMX 2.x):

Retarget     — overrides hx-target on the triggered element
Reswap       — overrides hx-swap on the triggered element
PushURL      — pushes a new URL into history (true → use current, or a URL)
Redirect     — triggers a client-side redirect (full URL required)
Refresh      — triggers a full page refresh
ReplaceURL   — replaces current URL without history entry (true or URL)
Location     — triggers client-side navigation without history (full URL)
Reselect     — CSS selector picking which part of the response to swap
Resettle     — "true" forces scroll reset; "false" preserves scroll

type HXTriggers

type HXTriggers map[string]any

HXTriggers is a deduplicating set of event names to fire on the client via the HX-Trigger response header. Components accumulate triggers during a render via RenderContext.AddHXTrigger; the router joins them into the response header at the end of the handler.

Each entry maps an event name to an optional detail value. When all values are empty (the common case), Join() emits the plain comma-separated form HTMX uses for simple event firing:

HX-Trigger: card-updated,nav-refresh

When any entry carries detail, Join() emits JSON so the client can read the detail on the event object:

HX-Trigger: {"item-changed":{"id":42},"card-updated":null}

The router only needs to write the joined string into the header; nanite-render handles the format decision.

func (HXTriggers) Add

func (t HXTriggers) Add(name string)

Add records an event name to fire on the client. Idempotent: adding the same name twice is a no-op (a later AddWithDetail for the same name upgrades the entry — first detail wins for now).

func (HXTriggers) AddWithDetail

func (t HXTriggers) AddWithDetail(name string, detail any)

AddWithDetail records an event name with a detail payload that the client receives on the event object. The detail is anything JSON-marshalable (struct, map, slice, primitive). An empty detail is equivalent to Add. If the name was previously added without detail, the detail upgrades the entry.

func (HXTriggers) HasDetail

func (t HXTriggers) HasDetail() bool

HasDetail reports whether any trigger in the set carries a JSON detail payload. Determines which Join format to use.

func (HXTriggers) Join

func (t HXTriggers) Join() string

Join returns the trigger set in HTMX's wire format. Plain comma-separated names when no entry carries detail; JSON object otherwise. Returns "" for an empty set so the caller can skip setting the header.

func (HXTriggers) Names

func (t HXTriggers) Names() []string

Names returns the trigger names as a sorted slice. Deterministic ordering makes tests reproducible and lets the router cache the joined header.

type MetadataProvider

type MetadataProvider interface {
	Component
	Metadata() func(rc *RenderContext, data any) error
}

MetadataProvider is an optional interface a Component implements to contribute document head metadata BEFORE the page streams. The Metadata closure runs with the page data before the view renders; it sets rc.SetTitle / rc.AddMeta, which the layout's <NANO_HEAD/> injector emits.

For the common case this is redundant: any component rendered in the view can call c.SetTitle during its render (the view renders before the layout in the two-pass page pipeline, and the head state is transferred to the layout's context). Metadata exists for views that are NOT registered components, or metadata that should run even if the render walk is skipped.

type MinifyConfig

type MinifyConfig struct {
	KeepDefaultAttrVals     bool
	KeepWhitespace          bool
	KeepDocumentTags        bool
	KeepEndTags             bool
	KeepConditionalComments bool
}

MinifyConfig controls HTML minification at compile time. The original GO-Portfolio used tdewolff/minify per-render; we apply it once at compile time so the cache stores the minified output.

type NodeStream

type NodeStream struct {
	Tag        []string
	Flags      []uint8
	NS         []uint16
	ChildStart []uint32
	ChildEnd   []uint32
	AttrStart  []uint32
	AttrEnd    []uint32
	TextOff    []uint32

	// Parent[i] is the parent node index of node i. -1 means root.
	Parent []int32

	// FirstChild[i] is the first direct child index of node i, or -1
	// if i is a leaf. NextSibling[i] is the next sibling of i (same
	// parent), or -1. These sibling links let the executor walk a tree
	// in O(n) — direct children are NOT contiguous in the stream
	// (grandchildren interleave), so a flat child range can't
	// represent them. Built once at compile time by
	// NodeStreamBuilder.Stream().
	FirstChild  []int32
	NextSibling []int32

	// ComponentName[i] is the name of a registered component when
	// Flags[i]&FlagComponent != 0. The executor dispatches to
	// ComponentRegistry.Lookup(ComponentName[i]).
	ComponentName []string

	// Shared attribute key/value tables. attrKeys/attrVals are indexed
	// by the (AttrStart, AttrEnd) ranges above. Component props can be
	// a *Program reference.
	AttrKeys []string
	AttrVals []any

	// Count is the number of nodes currently in the stream. Equal to
	// len(Tag) == len(Flags) == … when the stream is well-formed.
	Count int
	// contains filtered or unexported fields
}

NodeStream is the SoA representation of a parsed template. Each "node" is just an index into the parallel slices. Walking children is walking (start, end) ranges into the same stream, which keeps the working set inside L1 cache (target: cc < 10 on the hot loop).

Field shapes (bytes per node, approximate):

Tag          16    string header
Flags         1    bitmap
NS            2    namespace id
ChildStart    4    first child index
ChildEnd      4    one-past-last child index
AttrStart     4    first attr pair index
AttrEnd       4    one-past-last attr pair index
TextOff       4    [start, end) into the shared textBuf
ComponentName 16   name key for component dispatch (when flag set)

func (*NodeStream) AttrKey

func (s *NodeStream) AttrKey(i, k int) string

AttrKey returns the attribute key for the k-th attribute of node i.

func (*NodeStream) AttrVal

func (s *NodeStream) AttrVal(i, k int) any

AttrVal returns the attribute value for the k-th attribute of node i.

func (*NodeStream) ChildRange

func (s *NodeStream) ChildRange(i int) (uint32, uint32)

ChildRange returns the [start, end) child index range for node i.

func (*NodeStream) FirstChildOf

func (s *NodeStream) FirstChildOf(i int) int

FirstChildOf returns the first direct child index of node i, or -1.

func (*NodeStream) IsComponent

func (s *NodeStream) IsComponent(i int) bool

IsComponent reports whether the node is a templ component.

func (*NodeStream) IsText

func (s *NodeStream) IsText(i int) bool

IsText reports whether the node is a synthetic text node.

func (*NodeStream) IsVoid

func (s *NodeStream) IsVoid(i int) bool

IsVoid reports whether the node is a known void element that has no closing tag.

func (*NodeStream) NextSiblingOf

func (s *NodeStream) NextSiblingOf(i int) int

NextSiblingOf returns the next sibling index of node i, or -1.

func (*NodeStream) NumAttr

func (s *NodeStream) NumAttr(i int) int

NumAttr returns the number of attribute pairs for node i.

func (*NodeStream) Reset

func (s *NodeStream) Reset()

Reset clears the stream without releasing backing storage. After Reset the stream is reusable for a new program.

func (*NodeStream) Text

func (s *NodeStream) Text(i int) []byte

Text returns the text slice for a text node. Returns nil if the node is not a text node or has no text.

type NodeStreamBuilder

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

NodeStreamBuilder appends nodes to a NodeStream with growth on demand. The builder is stateful: call Reset to reuse. Slices grow geometrically to keep amortised allocation cost constant.

func NewBuilder

func NewBuilder() *NodeStreamBuilder

NewBuilder returns an empty builder.

func ParseHTML

func ParseHTML(src []byte, name string) (*NodeStreamBuilder, error)

ParseHTML is the exported HTML parser. Returns a NodeStreamBuilder populated from src.

Contract: the returned Program's string fields (Tag, AttrKeys, AttrVals, ComponentName) reference bytes inside src. The caller MUST keep src alive for as long as the Program is in use. The Go GC will keep src alive transitively (the string pointer fields are scanned), so this is safe under normal usage patterns — but if the caller releases src, the strings become dangling.

Engine adapters that wrap the source in a long-lived cache (the typical case) satisfy this contract automatically.

func (*NodeStreamBuilder) AppendComponent

func (b *NodeStreamBuilder) AppendComponent(name string, attrs []string, vals []any) int

AppendComponent appends a component node that dispatches to the named component at render time. The component must be registered via Registry.AttachComponents before render.

func (*NodeStreamBuilder) AppendNode

func (b *NodeStreamBuilder) AppendNode(tag string, flags uint8) int

AppendNode appends a new node with the given tag and flags. Returns the new node's index.

func (*NodeStreamBuilder) AppendText

func (b *NodeStreamBuilder) AppendText(text []byte, raw bool) int

AppendText appends a synthetic text node. The text is copied into the shared textBuf. Returns the new node's index.

func (*NodeStreamBuilder) Reserve

func (b *NodeStreamBuilder) Reserve(approxNodes int) *NodeStreamBuilder

Reserve pre-allocates the builder's internal slices with the given expected node count. Use this before parsing to avoid the geometric-growth reallocations as nodes are appended. Returns the builder for chaining.

func (*NodeStreamBuilder) ReserveText

func (b *NodeStreamBuilder) ReserveText(srcSize int) *NodeStreamBuilder

ReserveText estimates the textBuf capacity based on the source size. The text content of an HTML document is typically 40-60% of the source bytes. Pre-allocating avoids the growth reallocations as text runs are appended.

func (*NodeStreamBuilder) Reset

func (b *NodeStreamBuilder) Reset()

Reset re-initialises the builder for reuse.

func (*NodeStreamBuilder) SetAttrs

func (b *NodeStreamBuilder) SetAttrs(i int, keys []string, vals []any)

SetAttrs attaches the attribute key/value pairs to node i. The slices passed to this call are appended once (no per-node copying).

func (*NodeStreamBuilder) SetAttrsEach

func (b *NodeStreamBuilder) SetAttrsEach(i int, attrs []AttrKV)

SetAttrsEach appends (key, value) pairs to the builder's attr tables and records the [start, end) range on node i. It avoids the two make() calls that SetAttrs requires; the parser uses this with a pooled working slice.

func (*NodeStreamBuilder) SetChildren

func (b *NodeStreamBuilder) SetChildren(i int, start, end uint32)

SetChildren sets the child range for node i.

func (*NodeStreamBuilder) SetParent

func (b *NodeStreamBuilder) SetParent(child, parent int)

SetParent sets the parent node index for the given node.

func (*NodeStreamBuilder) Stream

func (b *NodeStreamBuilder) Stream() NodeStream

Stream returns the underlying stream, finalising the sibling links. The builder is consumed after Stream; further appends are invalid.

type OOBOptioner

type OOBOptioner interface {
	Component
	OOBID() string
}

OOBOptioner is an optional interface a Component implements to participate in HTMX-style Out-of-Band swaps. Components that opt in via OOBID() have their rendered output wrapped in

<div id="<oob-id>" hx-swap-oob="true">...</div>

when their render mutates state via SetState / UseState. The id is a hard DOM contract — it must match the `id` attribute of the element the swap replaces.

Engines that pre-compile components (templ, html/template) can implement OOBOptioner directly without using the fluent builder. The fluent builder exposes this via Definition.WithOOB(id).

type Option

type Option func(*Registry)

Option configures a Registry. Apply via New(WithEngine(...), WithCompression(...), etc.) or via the fluent builder (reg.WithEngine(...).WithCompression(...)).

func WithActionPrefix

func WithActionPrefix(prefix string) Option

WithActionPrefix changes the URL prefix for colocated server actions (default "/_nano/action/"). The prefix must end with "/" — ActionURL appends {COMPONENT}/{action} directly.

func WithCache

func WithCache(c *Cache) Option

WithCache replaces the default cache with a custom one.

func WithCacheSize

func WithCacheSize(n int) Option

WithCacheSize sets the default cache capacity.

func WithCompression

func WithCompression(level, minSize int) Option

WithCompression enables gzip compression.

func WithConditional

func WithConditional() Option

WithConditional enables ETag/304 handling.

func WithDebug

func WithDebug() Option

WithDebug enables the Debug pipeline.

func WithDefaultLoader

func WithDefaultLoader(loader SourceFunc) Option

WithDefaultLoader sets the registry-level loader. Used when a RenderContext has no per-request loader. This is the typical boot configuration — one loader for the whole app.

func WithEngine

func WithEngine(e Engine) Option

WithEngine registers an engine.

func WithEngines

func WithEngines(engines ...Engine) Option

WithEngines registers multiple engines.

func WithFuncMap

func WithFuncMap(fn FuncMapBuilder) Option

WithFuncMap registers a FuncMapBuilder.

func WithInjector

func WithInjector(fn DataInjector) Option

WithInjector registers a DataInjector.

func WithPipeline

func WithPipeline(p *Pipeline) Option

WithPipeline installs a custom Pipeline.

func WithVariant

func WithVariant(name string, values ...string) Option

WithVariant declares a variant dimension with allowed values.

type PageComposer

type PageComposer interface {
	Engine
	RenderPage(rc *RenderContext, w ByteWriter, layout, view string, data any) error
}

PageComposer is an optional interface an Engine implements to provide NATIVE layout composition. Engines that compose by nesting (templ's Layout(view) pattern) implement this; Registry.RenderPageWith routes to it instead of the string-injection (yield) path.

type ParseError

type ParseError struct {
	Name   string
	Offset uint32
	Msg    string
}

ParseError is a structured error with a name and offset. Used by the parser and engine adapters to report failures with enough context to diagnose the offending template. ComposeError is a ParseError and is matched by errors.As.

func (*ParseError) Error

func (e *ParseError) Error() string

Error implements the error interface.

func (*ParseError) Unwrap

func (e *ParseError) Unwrap() error

Unwrap supports errors.Is/As to introspect wrapped errors.

type Pipeline

type Pipeline struct {
	// BeforeCompile runs before Engine.Compile. Useful for source
	// pre-processing (e.g. stripping comments, normalising whitespace).
	BeforeCompile func(name string, src []byte) ([]byte, error)

	// AfterCompile runs after Engine.Compile. Useful for additional
	// optimisation or instrumentation on the resulting *Program.
	AfterCompile func(p *Program) error

	// BeforeExecute runs before Engine.Execute. Useful for setting
	// up per-request FunMaps, layout decisions, or response headers.
	BeforeExecute func(p *Program, rc *RenderContext, data any) error

	// AfterExecute runs after Engine.Execute. Useful for measuring
	// render time or post-processing the writer's bytes.
	AfterExecute func(p *Program, rc *RenderContext, data any) error
}

Pipeline is a thin hook layer for custom preprocessing. Each engine compiles its own source — nanite-render does not parse templates. The Pipeline is for users who want to inject preprocessing (e.g. sanitisation, i18n key expansion, custom FuncMap wrapping) around the engine's compile step.

The pipeline is *not* templating-engine-specific. The hook points are: before-compile, after-compile, before-execute, after-execute.

Most users do not need a Pipeline. The default flow is:

Registry.Part(name) → Engine.Compile → cache
Registry.Render(name) → Engine.Execute → ByteWriter

The Pipeline is the seam for users who want to layer in their own behaviour at any of those points.

func NewPipeline

func NewPipeline() *Pipeline

NewPipeline returns an empty Pipeline. Hooks are populated by the caller; missing hooks are skipped.

func (*Pipeline) Debug

func (p *Pipeline) Debug(w io.Writer) *Pipeline

Debug returns a Pipeline that prints per-stage timing to w. The pipeline populates rc.Times so callers can read the map back programmatically. Mirrors GO-Portfolio's `times["jade"]`, `times["render-page"]`.

Usage:

p := render.NewPipeline().Debug(os.Stderr)
reg.SetPipeline(p)

type PreloadHint

type PreloadHint struct {
	Href string
	As   string
	Type string
}

PreloadHint describes a resource the browser should fetch ahead of time. The hint is emitted as a <link rel="preload"> tag when the <PRELOADS/> component is rendered in the layout's <head>.

Empty fields are omitted from the emitted tag. Common values:

href: "/static/app.css"     the resource URL
as:   "style"               the content type the browser expects
                               (style, script, image, font, fetch)
type: "text/css"            MIME type — used by the browser to
                               validate support before fetching

type PreloadHints

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

PreloadHints returns the active set of preload hints registered against the registry. Returned for read-only inspection; mutations must go through Registry.Preload / Unpreload.

type Program

type Program struct {
	Engine  string
	Name    string
	Nodes   NodeStream
	Layout  *Program
	FuncMap map[string]any
	ETag    string
	Tags    []string

	// EngineData is the engine's compiled output. nil if the engine
	// uses the SoA executor. Engines that defer to html/template or
	// templ store their compiled *template.Template or templ.Component
	// here. The render path calls Engine.Execute which dispatches on
	// the type.
	EngineData any

	// Bytecode is the flat instruction stream compiled from Nodes
	// (see CompileBytecode). When present, Execute runs it instead of
	// the recursive tree walk — static HTML renders as coalesced
	// writes, matching templ's compile-to-output speed.
	Bytecode *Bytecode
	// contains filtered or unexported fields
}

Program is a compiled, cacheable template. Programs are immutable after Compile returns. Render never mutates a *Program; it only reads from the SoA node stream and writes to a ByteWriter.

A Program may reference another Program through its Layout field. This mirrors GO-Portfolio's two-pass render where the layout and the inner view are cached separately and composed at render time.

EngineData is an opaque blob for engines that need their own compiled representation (e.g. *template.Template for jade and the html/template engine). Plain engines (the default HTML adapter) leave this nil and use the Nodes stream via the SoA executor.

type Registry

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

Registry is the user-facing façade. It holds the set of registered engines, the shared program cache, and the per-request features (injectors, FuncMap, variants, compression, conditional).

All hot-path fields are accessed via atomic.Pointer for lock-free reads. Writes are CAS-loop copy-on-write, which is also lock-free. No RWMutex in the hot path.

func New

func New(opts ...Option) *Registry

New returns a Registry configured by the given options.

func NewRegistry

func NewRegistry(engines ...Engine) *Registry

NewRegistry returns a Registry with the given engines. The first engine is the default for unqualified Render calls.

The PRELOADS component is registered automatically. It emits the registry-level preload hints as <link rel="preload"> tags when referenced in a layout (typically inside <head>).

func (*Registry) ActionPrefix

func (r *Registry) ActionPrefix() string

ActionPrefix returns the URL prefix for action endpoints. Defaults to DefaultActionPrefix.

func (*Registry) AddEngine

func (r *Registry) AddEngine(e Engine) *Registry

AddEngine registers an engine. Copy-on-write CAS.

func (*Registry) AttachComponents

func (r *Registry) AttachComponents(c *ComponentRegistry)

Attach binds a ComponentRegistry to a Registry. Components are reachable via rc.Components() in the executor.

func (*Registry) Cache

func (r *Registry) Cache() *Cache

Cache returns the shared program cache, lazily creating it.

func (*Registry) ComponentLookup

func (r *Registry) ComponentLookup(name string) (Component, bool)

ComponentLookup is a convenience used by the executor and engines to find a component by name.

func (*Registry) Components

func (r *Registry) Components() *ComponentRegistry

Components returns the attached ComponentRegistry, or nil.

func (*Registry) Compress

func (r *Registry) Compress(cfg CompressConfig) *Registry

Compress enables gzip with the given config.

func (*Registry) CompressWriter

func (r *Registry) CompressWriter(rc *RenderContext, w ByteWriter) (ByteWriter, func() error)

CompressWriter wraps w in a gzip writer when compression is enabled and the request accepts it.

func (*Registry) Conditional

func (r *Registry) Conditional(enabled bool) *Registry

Conditional enables ETag/304.

func (*Registry) Default

func (r *Registry) Default() Engine

Default returns the first registered engine, or nil if none.

func (*Registry) DefaultLoader

func (r *Registry) DefaultLoader() SourceFunc

DefaultLoader returns the registry-level loader, or nil if none is set. Callers (notably the nano adapter) use this to populate the per-request RenderContext.Loader. Lock-free atomic load.

func (*Registry) Define

func (r *Registry) Define(name string) *Definition

Define returns a fluent builder for a component. The component is registered with the ComponentRegistry when Register is called.

Usage:

cr.Define("NAVBAR").
    WithFuncs(template.FuncMap{"currentUser": ...}).
    RenderChildren(func(c *render.ComponentContext) error {
        c.WriteString("<nav>")
        c.WriteChildren()
        c.WriteString("</nav>")
        return nil
    }).
    Register(cr)

func (*Registry) Engine

func (r *Registry) Engine(name string) Engine

Engine returns the engine registered under the given name, or nil.

func (*Registry) Engines

func (r *Registry) Engines() []Engine

Engines returns the live slice of registered engines. The slice is a copy; callers may mutate it.

func (*Registry) FuncMap

func (r *Registry) FuncMap(b FuncMapBuilder)

FuncMap registers a per-request FuncMap factory.

func (*Registry) HandleAction

func (r *Registry) HandleAction(w http.ResponseWriter, req *http.Request)

HandleAction is the universal colocated-action HTTP handler. Mount it on any router:

// chi
r.Post("/_nano/action/*", reg.HandleAction)
// stdlib
mux.HandleFunc("/_nano/action/", reg.HandleAction)

Flow:

  1. Method must be POST (405 otherwise).
  2. HX-Request header must be "true" (403 otherwise).
  3. Origin or Referer host must match the request Host (403 otherwise). This is the CSRF baseline — HTTPS sites behind proxies may need to relax it by wrapping the handler.
  4. The path is parsed as {prefix}/{COMPONENT}/{action}. Unknown component or action → 404.
  5. The body is parsed (JSON → typed map, form → best-effort conversion) and passed as props.
  6. The action closure runs with a fresh RenderContext. Any HX-Trigger events it records are emitted on the response.
  7. The component re-renders with the post-action state. If the component declared WithOOB(id), the output is wrapped in an HTMX OOB chunk; otherwise raw HTML is returned so the client's hx-target (or triggering element) swaps it inline.

Action closures are slow-path by definition (they mutate a database); the 0 B/op guarantee is unaffected — it applies to the render hot path, not to mutation endpoints.

func (*Registry) Inject

func (r *Registry) Inject(in DataInjector)

Inject registers a DataInjector. Copy-on-write CAS.

func (*Registry) InjectFuncMap

func (r *Registry) InjectFuncMap(rc *RenderContext, data any)

InjectFuncMap runs all registered injectors and the FuncMap factory. Lock-free reads via atomic.Pointer.

func (*Registry) InvalidateTag

func (r *Registry) InvalidateTag(tag string) int

InvalidateTag drops every cached (engine, name) pair associated with the given tag. This is a cold path (boot, hot-reload, ops tool calls) — the loop body is O(n) over the tagged set.

func (*Registry) Minify

func (r *Registry) Minify() *MinifyConfig

Minify reports the active MinifyConfig, or nil. Lock-free.

func (*Registry) MustPreloadParts

func (r *Registry) MustPreloadParts(rc *RenderContext, engine string, parts ...string)

MustPreloadParts is the boot-time version of PreloadParts. It panics with the underlying error if any part fails to load. Use this in main() to fail loudly at startup when a template is missing or misnamed.

The panic value is the wrapped error, so recover sites can introspect via errors.Is / errors.As:

defer func() {
    if r := recover(); r != nil {
        if err, ok := r.(error); ok {
            switch {
            case errors.Is(err, render.ErrEngineNotFound):
                // missing engine
            case errors.Is(err, render.ErrTemplateNotFound):
                // missing template file
            }
        }
    }
}()
reg.MustPreloadParts(rc, "jade", "layouts/app", "posts/show")

func (*Registry) Page

func (r *Registry) Page(rc *RenderContext) *RenderBuilder

Page returns a fresh RenderBuilder bound to the given RenderContext and the Registry. The RenderContext is typically pooled (see AcquireContext); the caller owns its release unless Render is called via a nano adapter that manages the lifecycle.

func (*Registry) Part

func (r *Registry) Part(rc *RenderContext, engine, name string) (*Program, error)

Part returns the compiled program for the given template, fetching it from the cache (or compiling on miss).

func (*Registry) Preload

func (r *Registry) Preload(h PreloadHint)

Preload registers a preload hint at the registry level. The hint is emitted (in registration order) wherever a <PRELOADS/> component tag appears in a layout — typically inside <head>. Hints are deduplicated by (Href, As, Type); registering the same triple twice is a no-op.

// Boot time:
reg := render.New(render.WithEngines(engine.NewHTML()))
reg.Preload(render.PreloadHint{Href: "/static/app.css", As: "style", Type: "text/css"})
reg.Preload(render.PreloadHint{Href: "/static/app.js", As: "script"})

// In the layout:
// <html><head><PRELOADS/></head><body>...</body></html>

func (*Registry) PreloadParts

func (r *Registry) PreloadParts(rc *RenderContext, engine string, parts ...string) error

PreloadParts ensures each named part is in the cache.

func (*Registry) Preloads

func (r *Registry) Preloads() []PreloadHint

Preloads returns a snapshot of the registered hints. The returned slice is a copy; callers may mutate it freely.

func (*Registry) Render

func (r *Registry) Render(rc *RenderContext, engine, name string, data any) error

Render fetches the named program from the cache (compiling on miss) and executes it against the given data.

func (*Registry) RenderComponent

func (r *Registry) RenderComponent(w ByteWriter, rc *RenderContext, name string, props any) error

RenderComponent dispatches a named, registered component directly to w, bypassing any parent template or layout. This is the entry point for HTMX-style targeted swaps: a handler receives an `hx-get="/posts/123/card"` request and renders JUST the CARD component without re-rendering the surrounding page.

The component is resolved via the registry's ComponentRegistry (the same path the html/template `{{ component "Name" . }}` superpower uses). The lookup follows rc.ComponentRegistry() first (so per-request attachment via SetComponentRegistry wins), then the registry's built-in components.

Returns nil (no error) when the name is not registered — the same semantics as the html/template superpower. Missing components are not a render error; they are a configuration gap the caller may or may not care about.

func (*Registry) RenderComposition

func (r *Registry) RenderComposition(rc *RenderContext, c Composition, data any) error

RenderComposition renders a layout + view + partials composition.

func (*Registry) RenderNamed

func (r *Registry) RenderNamed(rc *RenderContext, engine EngineName, name string, data any) error

RenderNamed is the typed variant of Render. It takes an EngineName instead of a raw string, so callers use the constants or CustomEngine(name) — a typo'd name is a compile error.

func (*Registry) RenderPage

func (r *Registry) RenderPage(rc *RenderContext, layout, view string, data any) error

RenderPage is the high-level "render a page" helper. It composes a layout + view + components in three steps:

  1. Render the view to a buffer.
  2. Stash the view bytes on the RenderContext (rc.ViewBytes) for the yield() superpower.
  3. Render the layout. Components (e.g. <NAVBAR/>) are dispatched via the SoA executor; the layout's {{ yield }} reads rc.ViewBytes via the FuncMap superpower.

Layout authors use `{{ yield }}` (FuncMap) or <YIELD/> (SoA component) where the body should go:

<html><body><NAVBAR/>
  {{ yield }}
  <FOOTER/>
</body></html>

View authors write the inner content as a flat HTML template:

<h1>{{.Title}}</h1>
<p>{{.Body}}</p>

RenderPage is the only entry point users typically need.

Errors returned by RenderPage are wrapped sentinels:

  • ErrRenderPageInvalid for empty layout or view
  • ErrLayoutMissing if the layout lookup fails
  • ErrTemplateNotFound for missing view
  • ErrLoaderMissing if the RenderContext has no loader

Routers can use errors.Is to translate these to HTTP responses.

func (*Registry) RenderPageEngines

func (r *Registry) RenderPageEngines(rc *RenderContext, layoutEngine EngineName, layout string, viewEngine EngineName, view string, data any) error

RenderPageEngines composes a layout and view rendered by DIFFERENT engines — the GO-Portfolio dynamic-layout pattern. The handler names the view; the layout wraps it; partials are components.

Typical split: the layout is plain HTML (compiled to bytecode, ~85 ns) with <YIELD/> where the view goes, and the view is templ (or jade, or html-template):

reg.RenderPageEngines(rc,
    render.EngineHTML, "layouts/app",   // bytecode layout
    render.EngineTempl, "posts/show",   // templ view
    data)

The view is rendered to a buffer, then the layout is rendered with a YIELD component injecting it. Components inside the layout (partials like <NAVBAR/>) dispatch through the ComponentRegistry.

func (*Registry) RenderPageWith

func (r *Registry) RenderPageWith(rc *RenderContext, engine, layout, view string, data any) error

RenderPageWith is RenderPage with an explicit engine name. Use this to compose a layout + view with an engine other than the default plain HTML engine (e.g. "html-template" or "jade").

func (*Registry) RenderStream

func (r *Registry) RenderStream(rc *RenderContext, engine EngineName, layout, view string, data any, chunkSize int) error

RenderStream renders a layout + view composition with progressive HTTP streaming. The layout's bytes are emitted in chunks of approximately chunkSize bytes; each chunk is pushed to the underlying response writer and signaled via http.Flusher so it hits the wire immediately. The browser starts parsing markup before the full template has been rendered — TTFB drops to the time the first chunk accumulates.

chunkSize of 0 disables chunking (writes pass through to the parent writer unchanged). Negative values are treated as 0.

When the layout engine is a PageComposer (e.g. templ), the layout's RenderPage handles streaming directly. For the engine-agnostic buffer-injection path, the per-flush writer wraps the response writer so the layout's bytecode writes stream incrementally.

RenderStream is the streaming primitive for HTTP/1.1 chunked encoding and HTTP/2 frame emission. Use it for slow-to-render pages with substantial static content.

func (*Registry) SetActionPrefix

func (r *Registry) SetActionPrefix(prefix string)

SetActionPrefix changes the URL prefix for action endpoints. The prefix must end with "/" — ActionURL appends {COMPONENT}/{action} directly.

func (*Registry) SetCache

func (r *Registry) SetCache(c *Cache)

SetCache replaces the shared program cache.

func (*Registry) SetDefaultLoader

func (r *Registry) SetDefaultLoader(loader SourceFunc)

SetDefaultLoader sets the registry-level loader. Lock-free; concurrent readers see the previous value until the next store.

func (*Registry) SetPipeline

func (r *Registry) SetPipeline(p *Pipeline)

SetPipeline installs a custom Pipeline.

func (*Registry) SetTag

func (r *Registry) SetTag(engine, name string, tags ...string)

SetTag associates a (engine, name) pair with one or more tags. Tags are used by InvalidateTag for category-based eviction.

func (*Registry) SetVariant

func (r *Registry) SetVariant(dim, value string)

SetVariant records a variant for a key in the registry tag table.

func (*Registry) Unpreload

func (r *Registry) Unpreload(as, typeStr string)

Unpreload removes every hint matching the given (As, Type) pair. Use this when a resource becomes unreachable (e.g. CDN purge). Matching is by As+Type only — Href is ignored because a single resource type often spans many URLs.

func (*Registry) Variant

func (r *Registry) Variant(name string, values ...string)

Variant declares a variant dimension with allowed values.

func (*Registry) WithMinify

func (r *Registry) WithMinify(cfg MinifyConfig) *Registry

WithMinify attaches a MinifyConfig to the Registry. Engines that support minification (jade, html/template) check this and apply it in their Compile step. Lock-free.

type RenderBuilder

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

RenderBuilder is a fluent builder for a render call. It hides the Composition struct and the RenderContext lifecycle behind a chainable API:

reg.Page(rc).
    Engine("jade").
    Layout("layouts/app").
    View("posts/show").
    Parts("partials/nav", "partials/footer").
    With(post).
    Render()

The builder accumulates fields in a single struct; the chain methods mutate the receiver and return it, so no per-call allocations occur.

func (*RenderBuilder) Engine

func (rb *RenderBuilder) Engine(name EngineName) *RenderBuilder

Engine sets the engine by name. Chainable. Use the typed constants (EngineJade, EngineHTML, ...) or CustomEngine(name) for a user-defined engine.

reg.Page(rc).Engine(EngineJade).View("home").Render()

func (*RenderBuilder) EngineInstance

func (rb *RenderBuilder) EngineInstance(e Engine) *RenderBuilder

EngineInstance sets the engine by value. The engine's Name() is derived automatically, so custom engines need no name cast. Chainable.

reg.Page(rc).EngineInstance(engine.NewJade()).View("home").Render()

func (*RenderBuilder) Layout

func (rb *RenderBuilder) Layout(name string) *RenderBuilder

Layout sets the layout template name. Chainable.

func (*RenderBuilder) Parts

func (rb *RenderBuilder) Parts(parts ...string) *RenderBuilder

Parts adds partial template names to the composition. Multiple calls append. Chainable.

func (*RenderBuilder) Render

func (rb *RenderBuilder) Render() error

Render executes the composition.

  • If Layout is set, it renders via RenderPageWith (layout wraps view) using the configured engine.
  • Otherwise it renders the view directly via Render.

Returns the underlying Registry error if any. If a cleanup hook was installed (via WithDone), it runs after the render completes.

func (*RenderBuilder) RenderComposition

func (rb *RenderBuilder) RenderComposition() error

RenderComposition executes the full Composition (layout + view + parts). Use this when parts need to be rendered alongside the view.

func (*RenderBuilder) View

func (rb *RenderBuilder) View(name string) *RenderBuilder

View sets the view template name. Chainable.

func (*RenderBuilder) With

func (rb *RenderBuilder) With(data any) *RenderBuilder

With sets the render data. Chainable.

func (*RenderBuilder) WithDone

func (rb *RenderBuilder) WithDone(fn func()) *RenderBuilder

WithDone installs a cleanup hook invoked after Render or RenderComposition returns. Returns the builder for chaining. Intended for adapters that manage pooled resources.

type RenderContext

type RenderContext struct {
	// Writer is the ByteWriter render writes into. The underlying
	// io.Writer is typically the response writer wrapped by a router.
	Writer ByteWriter

	// Request is the live HTTP request. Headers are written by the
	// caller before any render invocation so the buffer never holds
	// head-of-line bytes.
	Request *http.Request

	// Loader resolves a template name to bytes. Typically set by the
	// router middleware when the request starts.
	Loader SourceFunc

	// FuncMap is the per-request template function map. Engines
	// apply it on top of (or instead of) the Program's default
	// FuncMap. Frozen on the Program for compile-time defaults,
	// overridden by the request at render time.
	//
	// This mirrors GO-Portfolio's per-render funcMap (render.go:83-106),
	// which lets each handler inject closure-bound helpers.
	FuncMap template.FuncMap

	// Times is populated when the Debug stage is enabled. The map
	// is keyed by stage name and stores the wall-clock duration of
	// each stage. nil when Debug is off — opt-in only.
	Times map[string]time.Duration

	// Headers is the set of response headers to write before the body.
	// Pre-set by the middleware (Content-Type, Cache-Control, etc.).
	Headers http.Header

	// Layout is the optional layout template name. When set, the
	// Renderer renders the inner view then wraps it in the layout.
	// This mirrors GO-Portfolio's two-pass layout pattern.
	Layout string

	// Variants are the per-request variant dimensions, e.g.
	// {"locale": "fr", "theme": "dark"}. Resolved at Part() lookup
	// to compose the cache key. Composed Layout = engine/name@locale.fr@theme.dark.
	Variants map[string]string

	// ViewBytes is the pre-rendered view HTML used by the yield()
	// superpower. Set by RenderPage before the layout is executed.
	// Engines that defer to html/template read this via the FuncMap.
	ViewBytes []byte

	// ComponentFuncMap is the per-component FuncMap set by the
	// component dispatcher when rendering a component that
	// implements ComponentWithFuncs. Components that compose via
	// html/template can read this to expose component-specific
	// helpers during their render.
	ComponentFuncMap template.FuncMap
	// contains filtered or unexported fields
}

RenderContext is the per-request context the Registry writes through. It owns the ByteWriter, the SourceFunc, the per-request FuncMap, and the optional Times map. Instances are pooled — use AcquireContext and ReleaseContext.

RenderContext is router-agnostic. The core render package has no dependency on any specific router (nanite, chi, gin, stdlib http). Adapters live in sub-packages (e.g. nano/, chi/, gin/).

func AcquireContext

func AcquireContext(w ByteWriter, req *http.Request) *RenderContext

AcquireContext returns a fresh RenderContext bound to w and req. The caller must ReleaseContext when done.

func FromContext

func FromContext(ctx context.Context) *RenderContext

FromContext returns the *RenderContext carried by ctx, or nil if none is present.

func (*RenderContext) AddHXTrigger

func (rc *RenderContext) AddHXTrigger(name string)

AddHXTrigger records an HTMX client-side event to fire after this render completes. Components call this from their render function; the router reads the joined list via HXTriggers() at the end of the handler and writes it into the HX-Trigger response header.

Multiple events are deduplicated; AddHXTrigger is safe to call repeatedly with the same name. An empty name is ignored.

Example:

cr.Define("CARD").
    WithOOB("card-slot").
    Render(func(c *render.ComponentContext) error {
        c.AddHXTrigger("card-updated")
        c.SetState("count", c.GetState("count").(int)+1)
        return c.WriteString("<div>...</div>")
    }).
    Register(cr)

In the handler:

if isHTMX := render.IsHTMXRequest(r); isHTMX {
    reg.RenderComponent(w, rc, "CARD", nil)
    rc.WriteHTMXHeaders(w)
}

func (*RenderContext) AddHXTriggerAfterSettle

func (rc *RenderContext) AddHXTriggerAfterSettle(name string)

AddHXTriggerAfterSettle records an HTMX event to fire after the settle step. Populates the HX-Trigger-After-Settle response header.

func (*RenderContext) AddHXTriggerAfterSwap

func (rc *RenderContext) AddHXTriggerAfterSwap(name string)

AddHXTriggerAfterSwap records an HTMX event to fire after the swap step. Populates the HX-Trigger-After-Swap response header. See AddHXTrigger for naming and dedup semantics.

func (*RenderContext) AddHXTriggerWithDetail

func (rc *RenderContext) AddHXTriggerWithDetail(name string, detail any)

AddHXTriggerWithDetail records an event with a JSON-encodable detail payload. The client receives the detail on the event object via htmx:event-name.detail. See AddHXTrigger for dedup and lifecycle semantics.

func (*RenderContext) AddMeta

func (rc *RenderContext) AddMeta(name, content string)

AddMeta adds a <meta name="..." content="..."> tag for this request. Last write wins per name (no duplicate emission). Spills to a heap slice past metaInlineCap.

func (*RenderContext) ClearAssets

func (rc *RenderContext) ClearAssets()

ClearAssets resets the collected CSS/JS dependencies. Called on pool reuse; callers generally don't need it.

func (*RenderContext) ClearFormErrors

func (rc *RenderContext) ClearFormErrors()

ClearFormErrors removes all flash validation errors. Called on pool reuse; callers generally don't need to invoke it directly.

func (*RenderContext) ClearHeadState

func (rc *RenderContext) ClearHeadState()

ClearHeadState resets the title, meta tags, and id sequence. Called on pool reuse; callers generally don't need it.

func (*RenderContext) CloseSuspense

func (rc *RenderContext) CloseSuspense()

CloseSuspense closes the coordinator: the writer drains pending payloads and exits. Workers already running continue; workers that haven't started are skipped because the channel will receive no more readers. Call from defer in the handler that opened the coordinator, after the render walk completes.

Note: CloseSuspense does NOT cancel the coordinator's context — that cancellation is owned by rc.Request.Context() and fires automatically on client disconnect. Cancelling here would kill in-flight workers before they deliver, defeating the point of suspense.

func (*RenderContext) ComponentRegistry

func (rc *RenderContext) ComponentRegistry() *ComponentRegistry

ComponentRegistry returns the active ComponentRegistry, if any.

func (*RenderContext) ComponentState

func (rc *RenderContext) ComponentState() *State

ComponentState returns the per-render state. The state is reset on each new render (via SetRenderState).

func (*RenderContext) ContextDepth

func (rc *RenderContext) ContextDepth() int

ContextDepth returns the current stack depth. Useful for instrumentation / debugging; not used in the hot path.

func (*RenderContext) EffectiveFuncMap

func (rc *RenderContext) EffectiveFuncMap(programDefaults template.FuncMap) template.FuncMap

EffectiveFuncMap returns the FuncMap that should be applied to the Program. Per-request FuncMap (when set) overrides the Program's default. If neither is set, returns nil.

func (*RenderContext) EnsureSuspense

func (rc *RenderContext) EnsureSuspense(w ByteWriter) *suspenseCoordinator

EnsureSuspense lazily starts a suspense coordinator for this RenderContext, bound to w. Returns the coordinator (or the existing one if already started). Nil-safe: a nil receiver or nil writer means suspense is unavailable; the caller should fall back to synchronous rendering.

The coordinator inherits cancellation from rc.Request.Context() when rc.Request is non-nil; otherwise it uses context.Background().

func (*RenderContext) FormErrors

func (rc *RenderContext) FormErrors() map[string]string

FormErrors returns all flash validation errors as a map[string]string (field → message). Returns nil when there are none. Allocates — for display/instrumentation, not the hot path.

func (*RenderContext) GetContext

func (rc *RenderContext) GetContext(key string) any

GetContext scans the cascading-context stack backwards (newest first) and returns the value bound to the nearest matching key, or nil if no binding exists. Returns nil for a nil receiver.

O(n) in the stack depth — at inlineCtxCap the loop is fully cache-resident. Deeper trees still hit cache but pay an extra indirect on the spillover.

func (*RenderContext) GetFormError

func (rc *RenderContext) GetFormError(key string) string

GetFormError returns the flash validation error for a form field, or "" if none was set for this request.

func (*RenderContext) HTMXResponse

func (rc *RenderContext) HTMXResponse() HTMXResponse

HTMXResponse returns the active response-decision struct. Callers may read it for inspection; modifications should go through the Set* helpers above.

func (*RenderContext) HXTriggers

func (rc *RenderContext) HXTriggers() HXTriggers

HXTriggers returns the accumulated trigger set. The router reads this after the render completes to populate response headers. The returned set is owned by the RenderContext; callers must not mutate it after the handler returns.

func (*RenderContext) HXTriggersAfterSettle

func (rc *RenderContext) HXTriggersAfterSettle() HXTriggers

HXTriggersAfterSettle returns the after-settle trigger set.

func (*RenderContext) HXTriggersAfterSwap

func (rc *RenderContext) HXTriggersAfterSwap() HXTriggers

HXTriggersAfterSwap returns the after-swap trigger set.

func (*RenderContext) OOBSink

func (rc *RenderContext) OOBSink() ByteWriter

OOBSink returns the destination for OOB swap output. Falls back to Writer when no explicit sink is configured.

func (*RenderContext) PopContextTo

func (rc *RenderContext) PopContextTo(savedPtr int)

PopContextTo sets the stack pointer back to a saved index. Used by ComponentContext to unwind pushes made during its render frame so bindings don't leak out of scope. Popped slots are zeroed so the GC can reclaim any `any` values the user pushed (without this, they'd stay reachable through the stack slot until the next push overwrites them).

func (*RenderContext) PushContext

func (rc *RenderContext) PushContext(key string, val any)

PushContext pushes (key, val) onto the cascading-context stack. Nested bindings shadow outer ones (reverse-scan lookup).

The first inlineCtxCap pushes land in the inline array on RenderContext itself — zero allocation, persistent across pool reuse. Beyond that, pushes spill into overflowCtx (a heap slice) so genuinely deep trees don't panic. There is no hard cap; the framework never panics on depth.

Prefer ComponentContext.ProvideContext over calling this directly — it auto-pops on the component's render scope exit and gives you true cascading isolation.

func (*RenderContext) RequiresCSS

func (rc *RenderContext) RequiresCSS(href string)

RequiresCSS registers a CSS dependency for this request. The path is emitted by <NANO_ASSETS/> as a <link rel="stylesheet"> tag. Deduplicated: registering the same path twice emits it once, keeping the FIRST occurrence so emission order = first render order (deterministic — the executor walks the tree in order).

Components rendered in loops (a card repeated 50 times) register their dependency once; the head gets one tag, and the browser downloads the asset once.

func (*RenderContext) RequiresJS

func (rc *RenderContext) RequiresJS(src string)

RequiresJS registers a JS dependency for this request. The path is emitted by <NANO_ASSETS/> as a <script defer> tag. Deduplicated, first occurrence wins.

func (*RenderContext) SetComponentRegistry

func (rc *RenderContext) SetComponentRegistry(c *ComponentRegistry)

SetComponentRegistry binds a ComponentRegistry to this context.

func (*RenderContext) SetFormError

func (rc *RenderContext) SetFormError(key, msg string)

SetFormError records a flash validation error for a form field. The error is visible to the component's re-render in the same request (flash semantics: cleared on pool reuse). Writing to the same key twice — last write wins.

Flash lifecycle: the action calls SetFormError for each invalid field, returns ErrValidation, and HandleAction re-renders the component at 200 with the errors readable via GetFormError. The errors do NOT persist across requests — the component re-renders from its own data source; the errors are display state for this one pass.

Prefer ComponentContext.SetFormError inside components; this method is for action closures (which receive rc directly).

func (*RenderContext) SetHXLocation

func (rc *RenderContext) SetHXLocation(url string)

SetHXLocation triggers client-side navigation without a history entry. Full URL required.

func (*RenderContext) SetHXPushURL

func (rc *RenderContext) SetHXPushURL(url string)

SetHXPushURL pushes a URL into the history stack. Pass "true" to push the response URL; pass any other string as the URL to push. Empty clears the decision.

func (*RenderContext) SetHXRedirect

func (rc *RenderContext) SetHXRedirect(url string)

SetHXRedirect triggers a client-side redirect to the given URL. Full URL required.

func (*RenderContext) SetHXRefresh

func (rc *RenderContext) SetHXRefresh()

SetHXRefresh triggers a full client-side page refresh when set to "true". Any other value is treated as "true" by HTMX.

func (*RenderContext) SetHXReplaceURL

func (rc *RenderContext) SetHXReplaceURL(url string)

SetHXReplaceURL replaces the current URL without a history entry. Pass "true" to replace with the response URL; pass any other string as the URL to replace with.

func (*RenderContext) SetHXReselect

func (rc *RenderContext) SetHXReselect(selector string)

SetHXReselect sets the HX-Reselect response header. selector is a CSS selector telling HTMX which part of the response body to swap in. Use when the server returns a wrapper page and only an inner region should land in the DOM.

func (*RenderContext) SetHXResettle

func (rc *RenderContext) SetHXResettle(value string)

SetHXResettle sets the HX-Resettle response header. pass "true" to force scroll reset to the top after the swap, "false" to preserve scroll position across the swap. Empty string clears.

func (*RenderContext) SetHXReswap

func (rc *RenderContext) SetHXReswap(strategy string)

SetHXReswap overrides hx-swap on the triggered element. Examples: "outerHTML", "innerHTML swap:100ms", "none".

func (*RenderContext) SetHXRetarget

func (rc *RenderContext) SetHXRetarget(selector string)

SetHXRetarget overrides hx-target on the triggered element. CSS selector string. Set during the handler; applied by WriteHTMXHeaders.

func (*RenderContext) SetOOBSink

func (rc *RenderContext) SetOOBSink(w ByteWriter)

SetOOBSink redirects HTMX Out-of-Band swap output for components with WithOOB enabled. Pass nil to clear; OOBSink() then falls back to Writer.

Most handlers leave this nil — the main response is the swap — but routers that want to interleave OOB updates with a different stream (e.g. an SSE trailing chunk) can set it here.

func (*RenderContext) SetTitle

func (rc *RenderContext) SetTitle(title string)

SetTitle sets the document <title> for this request. Visible to the layout's <NANO_HEAD/> injector on full page loads (the view renders before the layout in the two-pass pipeline, and the head state is transferred to the layout's context).

On HTMX partial swaps, HTMX hoists <title> tags from the response body automatically — components can write them directly to their output instead.

func (*RenderContext) SetUserData

func (rc *RenderContext) SetUserData(d any)

SetUserData stores the wrapped data so engines can read it back.

func (*RenderContext) SetVariant

func (rc *RenderContext) SetVariant(dim, value string)

SetVariant sets a single variant dimension. Used to drive locale/theme resolution at Part() lookup.

func (*RenderContext) SubmitAsync

func (rc *RenderContext) SubmitAsync(id string, fn func(w ByteWriter, rc *RenderContext, data any) error, boundary ErrorBoundaryFunc, data any)

SubmitAsync spawns a worker goroutine that renders fn(data) into a pooled buffer and submits the result through the coordinator's channel. The worker selects on rc.ctx.Done() so a client disconnect aborts the render before it finishes — no wasted work, no leaked goroutines, no orphan channels.

id is the OOB target id (HTMX hx-swap-oob="true" wrapping). fn is the render function captured by the component definition. data is the per-call props.

boundary is the panic recovery callback. When non-nil, a panic in fn triggers boundary(bufWriter, panicVal) inside the worker; the boundary's output replaces fn's output as the OOB payload. A panic in the boundary itself is swallowed and a generic placeholder OOB is emitted so the page never crashes.

SubmitAsync is a no-op when the coordinator is nil or when the context is already cancelled.

func (*RenderContext) Suspense

func (rc *RenderContext) Suspense() *suspenseCoordinator

Suspense returns the active coordinator, or nil if no async component has been encountered. Useful for callers that want to check whether suspense is in flight.

func (*RenderContext) Title

func (rc *RenderContext) Title() string

Title returns the current document title ("" if unset). Useful for tests and for layouts that want to read it back.

func (*RenderContext) UseId

func (rc *RenderContext) UseId() string

UseId returns a stable, unique identifier for this request, suitable for pairing <label for="x"> with <input id="x">. Returns "nano-1", "nano-2", … — the first 256 come from a precomputed static array (zero allocation); deeper requests fall back to strconv formatting.

Note the difference from React: React's useId is stable per component instance across renders; ours is a per-request sequence — uniqueness (the property the label/input pairing needs) is what's guaranteed, and ids never repeat within a request. The sequence is reset per request (pool reuse).

func (*RenderContext) UserData

func (rc *RenderContext) UserData() any

UserData returns the (possibly wrapped) data the injectors last saw. The Registry uses this when the original data was not a map.

func (*RenderContext) WithDebug

func (rc *RenderContext) WithDebug() *RenderContext

WithDebug enables the Times map so the Debug stage can populate it.

func (*RenderContext) WithFuncMap

func (rc *RenderContext) WithFuncMap(fm template.FuncMap) *RenderContext

WithFuncMap returns rc with the given FuncMap attached. The receiver is returned unchanged for chaining.

func (*RenderContext) WithLayout

func (rc *RenderContext) WithLayout(name string) *RenderContext

WithLayout returns rc with the given layout name attached. The receiver is returned unchanged for chaining.

func (*RenderContext) WithLoader

func (rc *RenderContext) WithLoader(loader SourceFunc) *RenderContext

WithLoader returns rc with the given Loader attached. The receiver is returned unchanged for chaining.

func (*RenderContext) WriteHTMXHeaders

func (rc *RenderContext) WriteHTMXHeaders(w http.ResponseWriter)

WriteHTMXHeaders applies all accumulated HTMX response headers to the response writer in one call. The router invokes this at the end of the handler. Nil-safe.

Headers written:

  • HX-Trigger (from hxTriggers)
  • HX-Trigger-After-Swap (from hxTriggersAfterSwap)
  • HX-Trigger-After-Settle (from hxTriggersAfterSettle)
  • HX-Retarget, HX-Reswap, HX-Push-Url, HX-Redirect, HX-Refresh, HX-Replace-Url, HX-Location

w may be nil (no-op). Headers whose source is empty are skipped so the response carries only what the handler actually set.

func (*RenderContext) WriteHeader

func (rc *RenderContext) WriteHeader(key, value string)

WriteHeader writes h to the underlying writer. Headers are not buffered by ByteWriter; they are pushed directly to the wrapped io.Writer so the response is flushed before the body.

func (*RenderContext) WriteHeadersTo

func (rc *RenderContext) WriteHeadersTo(w io.Writer) error

WriteHeadersTo writes the queued headers to the underlying writer. Called automatically by the ByteWriter the first time bytes are written.

type Slots

type Slots map[string]string

Slots is a map of slot name to children content. Used for multi-slot composition (e.g. Card with header, body, footer).

type SourceFunc

type SourceFunc func(name string) ([]byte, error)

SourceFunc returns the raw template bytes for a given template name. The Registry calls this on cache miss. Implementations are typically fs.FS, embed.FS, or a path-based loader.

func NewFileLoader

func NewFileLoader(root, ext string) SourceFunc

NewFileLoader returns a SourceFunc that reads templates from the given root directory. Names are resolved as root + "/" + name + ext.

func NewMapLoader

func NewMapLoader(templates map[string][]byte) SourceFunc

NewMapLoader returns a SourceFunc that serves templates from an in-memory map. Useful for tests and small embedded sets.

func NewPrefixLoader

func NewPrefixLoader(prefix string, fs interface {
	Open(name string) (io.ReadCloser, error)
}) SourceFunc

NewPrefixLoader returns a SourceFunc that prepends prefix to name and reads the resulting path. Useful for embedding.

type Sourceless

type Sourceless interface {
	Engine
	NoSource()
}

Sourceless is an optional interface an Engine implements to signal that Compile ignores the source bytes — the engine's programs come from elsewhere (e.g. templ components, which are pre-compiled to Go at build time and registered by name). The registry then does NOT require a Loader on the RenderContext for this engine.

type State

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

State is the per-render state shared across components in a single render. It enables React-style state management: a parent can set a value, and a child (rendered later in the same render) can read it. State is reset on each new render.

The state object is accessed via the FuncMap (useState, get, set) or directly via the RenderContext.

func NewState

func NewState() *State

NewState returns an empty State.

func (*State) Clear

func (s *State) Clear()

Clear removes all key-value pairs from the State.

func (*State) Get

func (s *State) Get(key string) (any, bool)

Get returns the value for the given key. Returns nil if the key has not been set. The boolean indicates whether the key was present.

func (*State) Set

func (s *State) Set(key string, value any)

Set sets the value for the given key. Overwrites any previous value.

func (*State) UseState

func (s *State) UseState(key string, initialValue any) any

UseState returns the value for the given key, or initialValue if the key has not been set. The initialValue is stored for subsequent calls. This is the React-style useState hook:

count, _ := state.UseState("count", 0).(int)

On the first call, UseState returns initialValue and stores it. On subsequent calls, UseState returns the stored value.

type Watcher

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

Watcher is an fsnotify-based hot-reload handler. It watches a set of template roots and emits invalidation events to a channel. The program cache subscribes to the channel and evicts matching keys when an event fires.

Hot-reload is enabled by default; production builds can disable it at construction time.

func NewWatcher

func NewWatcher(roots ...string) (*Watcher, error)

NewWatcher returns a Watcher over the given roots. The returned watcher is started; call Close to shut it down.

func (*Watcher) Close

func (w *Watcher) Close() error

Close shuts the watcher down.

func (*Watcher) Disabled

func (w *Watcher) Disabled() bool

Disabled reports whether the watcher is a no-op.

func (*Watcher) Events

func (w *Watcher) Events() <-chan Event

Events returns the channel of invalidation events.

Directories

Path Synopsis
Package nano provides a thin adapter between the nanite router and the nanite-render package.
Package nano provides a thin adapter between the nanite router and the nanite-render package.

Jump to

Keyboard shortcuts

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