Routing

Routes are one comptime table in src/app/routes.zig. The server matches the request path against it and renders the matched chain outside-in.

pub const routes: []const verve.Route = &.{
    verve.Route.init("/", renderHome),
    verve.Route.init("/work/:slug", renderWork),
    verve.Route.layout("/app", renderShell, &.{
        verve.Route.init("/dashboard", renderDashboard),
        verve.Route.init("/settings/:section", renderSettings),
    }),
    verve.Route.init("/admin", renderAdmin).protect(adminGuard),
};

Path parameters

:name segments capture into ctx.params; a trailing *rest wildcard captures the remainder:

fn renderWork(ctx: *verve.Context) !*verve.Node {
    const slug = ctx.param("slug") orelse "";
    ...
}

Captured slices point into the request path buffer — dupe into ctx.alloc() if you need them beyond the render.

Nested layouts

Route.layout(path, render, children) declares a layout whose children match relative paths beneath it. The server renders the child first, hands its tree to the layout via ctx.outlet():

fn renderShell(ctx: *verve.Context) !*verve.Node {
    return ctx.main_().children(.{
        ctx.nav().children(.{ ... }),
        ctx.section().children(.{ctx.outlet()}),
    }).build();
}

When the layout path itself is requested (/app), no child matched and ctx.outlet_node is null — check it to render an index. Nesting depth is capped at 8.

Guards

.protect(fn) runs before render. Return null to allow, or a Redirect to bounce:

fn adminGuard(ctx: *verve.Context) ?verve.Redirect {
    const meta = ctx.request_meta orelse return .{ .to = "/login" };
    if (sessionValid(meta)) return null;
    return .{ .to = "/login" };
}

Guards on a layout protect every child beneath it.

Redirects from render

A handler can abort into a redirect — useful after form posts:

return ctx.redirect("/thanks");               // 303 See Other
return ctx.redirectWithStatus("/moved", 301); // explicit status

Active-link highlighting

ctx.el("a").href("/docs")
    .attrFmt("class", "nav-link {s}", .{ctx.activeClass("/docs", "active")})

activeClass compares against the current location (trailing slash normalized) — no prop drilling of the current path.

Client-side links

verve.link(ctx, "/counter", "Counter", .{ .prefetch_on_hover = true }) builds an anchor the client runtime can intercept and prefetch.

Next: Control flow.