Template Syntax
The zzz template engine uses a Mustache-inspired syntax with double and triple curly braces. All templates are parsed at compile time, so syntax errors produce compiler diagnostics rather than runtime failures.
Delimiters at a glance
Section titled “Delimiters at a glance”| Syntax | Purpose | HTML escaped |
|---|---|---|
{{variable}} | Output a value | Yes |
{{{variable}}} | Output a value without escaping | No |
{{! comment }} | Comment (no output) | — |
{{#if cond}}...{{/if}} | Conditional block | — |
{{#if cond}}...{{else}}...{{/if}} | Conditional with else | — |
{{#each items}}...{{/each}} | Loop over a slice | — |
{{#with expr}}...{{/with}} | Scope into a nested struct | — |
{{> partial}} | Include a partial template | — |
{{{{raw}}}}...{{{{/raw}}}} | Raw block (no tag processing) | No |
Variable interpolation
Section titled “Variable interpolation”Use double braces to output a value. The value is automatically HTML-escaped to prevent XSS:
<h1>{{title}}</h1><p>Written by {{author}}</p>try ctx.render(PageTemplate, .ok, .{ .title = "My Post", .author = "Alice",});Output:
<h1>My Post</h1><p>Written by Alice</p>HTML escaping
Section titled “HTML escaping”Double-brace variables escape the following characters:
| Character | Escaped to |
|---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
For example, if content is <script>alert('xss')</script>, then {{content}} renders as:
<script>alert('xss')</script>Raw (unescaped) output
Section titled “Raw (unescaped) output”Use triple braces when you need to output trusted HTML without escaping:
<div class="content">{{{body_html}}}</div>Only use {{{...}}} for values you control (such as pre-rendered HTML from your own code). Never use it for user-supplied input.
Integer values
Section titled “Integer values”Both string ([]const u8) and integer fields are supported. Integer values are automatically formatted to their decimal string representation:
<p>Count: {{count}}</p>try ctx.render(Tmpl, .ok, .{ .count = @as(u32, 42) });// Output: <p>Count: 42</p>Dot notation
Section titled “Dot notation”Access nested struct fields using dot-separated paths:
<p>{{user.name}} ({{user.email}})</p>try ctx.render(Tmpl, .ok, .{ .user = .{ .name = "Alice", .email = "alice@example.com" },});// Output: <p>Alice (alice@example.com)</p>Dot paths can be arbitrarily deep: {{profile.address.city}} resolves @field(@field(@field(data, "profile"), "address"), "city") at comptime.
Conditionals
Section titled “Conditionals”Basic if
Section titled “Basic if”Render a block only when a condition is truthy:
{{#if show_banner}} <div class="banner">Welcome back!</div>{{/if}}if / else
Section titled “if / else”Provide an alternative when the condition is falsy:
{{#if logged_in}} <span>Welcome, {{username}}!</span>{{else}} <a href="/login">Log in</a>{{/if}}Truthiness rules
Section titled “Truthiness rules”The template engine evaluates truthiness based on the Zig type of the field:
| Type | Truthy when |
|---|---|
bool | true |
?T (optional) | Not null |
[]const T (slice) | len > 0 |
| Any other type | Always truthy |
This means you can use optionals and slices directly in conditions without converting to bool:
{{#if items}} <p>You have items in your cart.</p>{{else}} <p>Your cart is empty.</p>{{/if}}const empty: []const Item = &.{};try ctx.render(Tmpl, .ok, .{ .items = empty });// Renders the else branch because the slice is emptyIterate over a slice with {{#each}}. Inside the loop body, the context switches to each element:
<ul>{{#each items}} <li>{{name}} - {{price}}</li>{{/each}}</ul>const Item = struct { name: []const u8, price: []const u8 };const items = [_]Item{ .{ .name = "Widget", .price = "$9.99" }, .{ .name = "Gadget", .price = "$19.99" },};try ctx.render(Tmpl, .ok, .{ .items = @as([]const Item, &items),});Output:
<ul> <li>Widget - $9.99</li> <li>Gadget - $19.99</li></ul>The collection field must be a slice ([]const T) or a pointer to an array. Inside the loop body, all tags resolve against the element type T rather than the root data struct.
With blocks
Section titled “With blocks”The {{#with}} block changes the scope to a nested field, reducing repetition when accessing deeply nested data:
{{#with user}} <p>{{name}} -- {{email}}</p>{{/with}}This is equivalent to:
<p>{{user.name}} -- {{user.email}}</p>{{#with}} also works with dot paths:
{{#with profile.address}} <p>{{city}}, {{state}}</p>{{/with}}Comments
Section titled “Comments”Comments produce no output. Use them for documentation inside templates:
{{! This section shows the user profile }}<div class="profile"> {{name}}</div>The comment content (everything between {{! and }}) is discarded during rendering.
Raw blocks
Section titled “Raw blocks”Raw blocks pass their content through without any template processing. This is useful when you need to include literal curly braces in your output, such as client-side template syntax or code examples:
{{{{raw}}}} This {{will not}} be processed as a variable. Neither will {{{this}}} or {{#if anything}}.{{{{/raw}}}}Output:
This {{will not}} be processed as a variable. Neither will {{{this}}} or {{#if anything}}.Only {{{{raw}}}} is supported as the block name for raw blocks.
Compile-time safety
Section titled “Compile-time safety”Because templates are parsed at compile time, you get immediate feedback on errors:
- Unclosed tags —
{{namewithout}}produces a compile error - Unclosed blocks —
{{#if show}}without{{/if}}produces a compile error - Missing fields — referencing
{{nonexistent}}with a data struct that lacks that field produces a compile error - Type mismatches — using
{{#each}}on a non-slice field produces a compile error - Unresolved partials —
{{> name}}without providing the partial source produces a compile error
Next steps
Section titled “Next steps”- Layouts and Partials — define reusable layouts and include partial templates
- Pipes and Helpers — transform values with built-in pipe filters
- htmx Integration — use templates with htmx for partial rendering