# 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) {
}
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| {
}
}
```
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) {
{title}
@body
}
```
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
```