Rendering & Context

Every route handler receives a *verve.Context — the per-request hub for building the page. It owns an arena allocator (wiped at end of request), the matched path params, the URL, the head accumulator, and the element factories.

The node builder

Element factories return *Node so chains read like the HTML they produce. .build() finalizes a chain and surfaces any allocation error accumulated along the way:

return ctx.div().class("card")
    .children(.{
        ctx.h1("Title"),
        ctx.p().text("Body copy."),
        ctx.a("/docs", "A link"),
    })
    .build();

Dedicated factories: div span p h1–h4 a button input form ul ol li img br hr label code pre nav main_ section header footer article aside strong em small textarea select option link meta title style script. Anything else goes through the escape hatch ctx.el("dialog").

Common chain methods

MethodEmits
.class("a b")class="a b"
.attr("k", "v")arbitrary attribute
.attrFmt("k", "{d}", .{n})formatted attribute (arena-allocated)
.text("…") / .textInt(n) / .textFmt(...)escaped text content
.children(.{ a, b, c })append children (tuple of *Node)
.bind("name")z-bind="name" — reactive text binding
.onClick("export_name")z-on-click — wasm export dispatch
.id("x") / .href(...) / .src(...) / .type_(...) / .name(...) / .value(...)the usual suspects
.raw(bytes)verbatim bytes — caller guarantees safety

Text set via .text/textFmt is HTML-escaped by the renderer; URLs in markdown are sanitized. .raw and ctx.scriptInline are the explicit unsafe escape hatches.

Context essentials

ctx.alloc()              // request arena — no manual frees
ctx.param("slug")        // captured :slug path segment
ctx.location             // ?*Location — path, query, fragment
ctx.assetHref("a.css")   // cache-busted /public URL from the manifest
ctx.activeClass(href, "active")  // nav highlighting
ctx.outlet()             // nested-route child placeholder (layouts)
ctx.redirect("/login")   // sentinel node → 303 response
ctx.errorBoundary(inner, fallback)  // swap subtree on builder error

Special node forms

ctx.raw(bytes)        // fragment, verbatim bytes (sitemap.xml, OG SVG…)
ctx.textNode("plain") // bare escaped text, no wrapper element
ctx.template("name", inner) // <template data-vt="name"> for client cloning

Pair ctx.raw with .contentType("application/xml") for non-HTML routes.

The page shell

One component owns the <html> skeleton. It drains the head accumulator (see Head management), then emits the body and the bridge script:

pub fn page(ctx: *const verve.Context, body: *verve.Node) !*verve.Node {
    try ctx.setTitleIfUnset("My app");
    var aw: std.Io.Writer.Allocating = .init(ctx.alloc());
    try ctx.head.?.render(&aw.writer);
    return ctx.el("html").children(.{
        ctx.el("head").children(.{
            ctx.raw(aw.written()),
            ctx.link("stylesheet", try ctx.assetHref("site.css")),
        }),
        ctx.el("body").children(.{ body, ctx.script("/verve.js") }),
    }).build();
}

The server calls components.page for its own 404/error responses, so keep that export.

Errors

Builder chains never try mid-chain: allocation failure poisons the node and the error surfaces at .build(). ctx.errorBoundary(inner, fallback) substitutes a fallback subtree when inner accumulated an error.

Next: Routing.