zynagent-first application framework

Views#

zyn renders ztl HTML and XML .ez templates from src/views:

try action.render("root.html", .{ .time = "12:34" }, .{});

Rendering views#

Every application build discovers and compiles every .ez file below src/views, including files used only through @include, and generates view metadata for Zig's compile-time analysis. An invalid view or include therefore fails the application build even when no route renders it directly.

Action.render and Hook.render require a comptime-known template name. The named template must exist, and the params struct must contain exactly the template's top-level params. Missing and unexpected fields are compile errors. Framework globals config, locals, flash, and children are excluded from that params contract.

Relative template names use the runtime filesystem's path semantics during compile-time validation. Current-directory components are accepted, so "./root.html" and "pages/./show.html" match their canonical view metadata.

Template data#

Template params are available as ztl variables such as <%= @time %>. Every view and layout also receives the root zyn.zon value as @config, the current request's action.locals fields as @locals, and the ordered application-shaped flash entries as @flash:

<p>Signed in as <%= @locals['user_id'] %>.</p>
<% foreach (@flash) |entry| { -%>
    <p data-kind="<%= entry["kind"] %>"><%= entry["message"] %></p>
<% } %>

Fields with values that ztl cannot represent are omitted from @locals and do not prevent other templates from rendering. Integers must fit in an i64. Flash entries retain the kind and message types declared by app.Flash; an unsupported flash value fails only a template which accesses @flash.

Escaping and trusted markup#

Ordinary interpolation escapes HTML and XML metacharacters. Use <%= safe value %> only for trusted markup, never for user-controlled content. Escaping does not make a value safe in JavaScript, CSS, or URL contexts.

Ordinary interpolation escapes flash values in HTML text and attribute contexts.

Partials and layouts#

Templates can include partials beneath src/views with ztl's @include and pass values to them:

<% @include("heading.html.ez", %{title: @title}) %>

Include paths are relative to src/views. Absolute paths and any path containing a .. component are rejected.

Add layouts with route defaults. A layout renders its child through <%= safe @children %>; multiple layouts accumulate and nest from route-module defaults through mounts and per-route defaults.

Scoped CSS#

HTML templates can declare component styles in a static <style> element that is a direct child of the template:

<article class="card">
    ...
</article>

<style>
    .card { animation: appear 150ms ease-out; }
    .card :global(a.button) { text-decoration: none; }
    :global { body.dialog-open { overflow: hidden; } }
    @media (width > 40rem) { .card { display: grid; } }
    @keyframes appear { from { opacity: 0; } }
</style>

The block is extracted into the application stylesheet and scoped to that template with a stable data-zyn-s marker. :global(...) leaves its anchored selector unscoped and :global { ... } leaves a complete rule list unscoped. Keyframe names and their animation references are scoped too. Scoping recurses through @media, @supports, @container, and @layer. The marker attribute is reserved for Zyn.

Use <style inline> for an explicitly inline, unscoped style element. Other attributes and template expressions are not accepted on managed style blocks. XML templates are not transformed. See framework/assets for the stylesheet, url(...), import, fingerprint, and serving contract.

Generating URLs#

Application URLs#

Templates generate application URLs with url(literal_pattern, map):

<a href="<%= url(
    '/articles/:id?page=:page#comments',
    %{id: @article_id, page: @page}
) %>">Article comments</a>

The pattern and map must be literals. Map keys must exactly match the named placeholders across the path, query, and fragment. Values are evaluated when the view renders and must be non-empty strings or integers. Zyn encodes path values as path segments, query values as query components, and fragment values as fragment components.

The application build validates the path portion against the flattened route table. Dynamic patterns and wildcard routes are rejected; literal queries and fragments are supported and do not participate in route matching. Query and fragment placeholder names begin with a letter, _, or @, followed by letters, digits, or _. Percent-encode a literal colon followed by such an identifier as %3A.

Identifier keys follow ztl syntax, including %{@id: value} for a parameter named @id. Escaped <%% delimiters remain literal template text. Ordinary HTML interpolation escapes static query separators as &amp; in rendered attributes without changing the generated URL's query semantics.

Storage URLs#

Templates generate public and time-limited stored-content URLs without receiving read or write access to storage:

<img src="<%= storage_url(@avatar_blob_id) %>" alt="Avatar">
<a href="<%= temporary_storage_url(@document_blob_id, 900) %>">Download</a>

storage_url(blob_id) requires configured public delivery. temporary_storage_url(blob_id, lifetime_seconds) requires a positive bounded integer lifetime. Neither helper checks whether the key exists. Keys and lifetimes are evaluated when the template renders.

Validation failures#

An action can render the same page after an expected failure by selecting the response status at the render call:

const std = @import("std");
const zyn = @import("zyn");

const CreatePayload = struct {
    name: []const u8,
};

pub fn index(action: *zyn.Action) !zyn.Response {
    return action.render("names/index.html", .{}, .{});
}

pub fn create(action: *zyn.Action) !zyn.Response {
    const payload = try action.expect_payload(CreatePayload);
    const name = std.mem.trim(u8, payload.name, " \t\r\n");
    if (name.len == 0) {
        return action.render(
            "names/index.html",
            .{},
            .{ .status = .unprocessable_entity },
        );
    }

    // Create the name.
    return .redirect(.see_other, "/names");
}

XML views#

An XML template such as hello.xml.ez can be rendered as try action.render("hello.xml", params, .{}). It receives the XML content type and uses configured .xml layouts while ignoring inherited HTML layouts.