# Zig Templating *This is a young project. The syntax is mostly settled, but one breaking change is still under discussion: how block-level control flow is written ([#27]). Feedback is very welcome, especially there.* [#27]: https://github.com/lalinsky/zt/issues/27 A small HTML templating language that compiles to Zig at build-time. Inspired by [Templ], [Zeix] and [JSX]. The idea is to invent as little syntax as possible, just enough to make it possible to interweave real Zig code with HTML elements. Using this approach, the template compiler can stay very simple and delegate Zig code analysis to the real Zig compiler. Templates are transpiled into Zig source files as part of `zig build`, so you can just import them like any other source files in your application. Everything is fully type-checked by the Zig compiler, and there is no overhead at runtime. Output is directly written to a `std.Io.Writer`, so there is no state and no allocations. [JSX]: https://react.dev/learn#writing-markup-with-jsx [Zeix]: https://ziex.dev/ [Templ]: https://templ.guide/ ## Installation zt supports Zig 0.16 and 0.17. Both versions are tested in CI. ```bash zig fetch --save git+https://github.com/lalinsky/zt ``` Configure your `build.zig`: ```zig const std = @import("std"); const zt = @import("zt"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const zt_dep = b.dependency("zt", .{ .target = target, .optimize = optimize, }); // Compile templates (.zt → .zig) const templates = zt.addTemplates(b, zt_dep, &.{ b.path("src/templates/pages.zt"), }); const root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); root_module.addImport("zt", zt_dep.module("zt")); const exe = b.addExecutable(.{ .name = "myapp", .root_module = root_module, }); exe.step.dependOn(templates); b.installArtifact(exe); } ``` The template compiler is built in debug mode by default. To build it optimized, pass `tool_optimize`: ```zig const zt_dep = b.dependency("zt", .{ .target = target, .optimize = optimize, .tool_optimize = .fast, }); ``` ## Usage ```zig const std = @import("std"); const pages = @import("templates/pages.zig"); pub fn main() !void { var buf: [4096]u8 = undefined; var stdout = std.fs.File.stdout().writer(&buf); const w = &stdout.interface; try pages.Home.render(.{ "Welcome" }, w); try w.flush(); } ``` --- ## Example ```zig const Post = @import("../models.zig").Post; const User = @import("../models.zig").User; fn formatDate(ts: i64) []const u8 { // ... } pub templ Layout(title: []const u8) { {title} @Nav()
@children
} pub templ Nav() { } pub templ PostCard(post: Post) {

{post.title}

if (post.subtitle) |subtitle| {

{subtitle}

}
{for (post.tags) |tag| {tag}}
} pub templ HomePage(user: ?User, posts: []const Post) { @Layout("Home") { if (user) |u| {

Welcome back, {u.name}!

} else {

Welcome, guest! Log in

}
for (posts) |post| { @PostCard(post) }
} } ``` --- ## Syntax ### Templates Templates are defined with `templ` and compile to structs with a `render` method: ```zig pub templ Greeting(name: []const u8) {

Hello, {name}!

} ``` ### Zig Code Standard Zig code goes at the top of the file and is passed through unchanged: ```zig const std = @import("std"); const Post = @import("../models.zig").Post; fn formatDate(ts: i64) []const u8 { // ... } pub templ Article(post: Post) {

{post.title}

} ``` --- ## Elements ```zig pub templ Document() { My Page
Content
} ``` All HTML elements must be explicitly closed, but void elements like `` and `` will be rendered correctly without the closing slash. --- ## Attributes **Static** - quoted string values: ```zig
``` Single-quoted values are always static, so `{` has no special meaning inside them. Use them for JSON or JavaScript object literals: ```zig
``` **Dynamic** - Zig expressions in braces: ```zig
``` **Interpolated** - mix static and dynamic parts (double quotes only): ```zig Read more ``` **Boolean** - presence means true: ```zig ``` **Conditional** - `?=` takes a `bool` (or `?bool`) and renders the bare attribute when it is true, nothing otherwise: ```zig ``` A plain dynamic attribute rejects a `bool` at compile time, because `checked="false"` still means checked. For attributes that expect the strings `"true"`/`"false"` (`aria-expanded`, `draggable`, `hx-boost`, ...), pass a string: ```zig ``` **Optional** - attribute omitted when value is null: ```zig Link
``` --- ## Expressions **Escaped output** (default) - safe for user content: ```zig {comment.text} ``` **Raw output** - for trusted HTML only: ```zig
{!article.html_content}
``` ### Custom Formatting A value with a `format` method is written through it, and the output is escaped like any other text (or written as is with `{!...}`): ```zig const Money = struct { cents: u32, pub fn format(self: Money, writer: *std.Io.Writer) std.Io.Writer.Error!void { try writer.print("{d}.{d:0>2} EUR", .{ self.cents / 100, self.cents % 100 }); } }; pub templ Price(price: Money) { {price} } ``` A value with a `formatHtml` method writes HTML that is not escaped, so it must produce safe markup itself: ```zig const Icon = enum { home, search, pub fn formatHtml(self: Icon, writer: *std.Io.Writer) std.Io.Writer.Error!void { try writer.print("", .{self}); } }; ``` `zt.fmt` turns a function that writes to a writer into a value with a `format` method, called with the arguments followed by the writer: ```zig fn dateTime(ts: i64, writer: *std.Io.Writer) std.Io.Writer.Error!void { // ... } pub templ RecipeInfo(recipe: Recipe) {

Created {zt.fmt(dateTime, .{recipe.created_at})}

} ``` --- ## Control Flow ### If/Else Block-level: ```zig if (user.is_admin) { Admin } if (post.subtitle) |subtitle| {

{subtitle}

} ``` Inline: ```zig {if (user.premium) "Pro" else "Free"} {if (error) |msg|
{msg}
} ``` ### For Loops Block-level: ```zig for (rows, 0..) |row, i| { ... } ``` Inline: ```zig ``` ### Switch Block-level: ```zig switch (order.status) { .pending => { Processing }, .shipped => |tracking| { Track package }, else => { Unknown }, } ``` Inline: ```zig {switch (user.role) .admin => Admin, .mod => Mod, else => User} ``` --- ## Components Call other templates with `@`: ```zig pub templ Nav() { } pub templ Page() { @Nav()
Content
} ``` With imports: ```zig const ui = @import("ui.zig"); pub templ Page() { @ui.Button("Click me") } ``` ### Children Parent templates use `@children` to render nested content: ```zig pub templ Card(title: []const u8) {

{title}

@children
} pub templ Page() { @Card("Welcome") {

This appears inside the card body.

} } ``` Nesting works to any depth: ```zig pub templ Page() { @Layout("Home") { @Card("News") {

Latest updates...

} } } ``` ### Components as Parameters Templates can accept `zt.Component` for dynamic composition: ```zig pub templ Modal(title: []const u8, body: zt.Component) { } ``` Create components with `bind`: ```zig const args: templates.Alert.Args = .{ "Something went wrong" }; const alert = templates.Alert.bind(&args); try templates.Modal.render(.{ "Error", alert }, writer); ``` --- ## API Each template generates: - `render(args, writer) !void` - render to a writer - `bind(args) zt.Component` - create a type-erased component - `Args` - the argument tuple type See `examples/` for a runnable project: ```bash cd examples zig build run ```