A minimal, zero-dependency templating engine written in Zig.
zemplate leverages Zig’s comptime capabilities to build a function graph for efficient template rendering. Templates can access nested fields, iterate over values, evaluate conditionals, and render string literals — all with zero runtime reflection.
Originally created for my portfolio site, zemplate became its own project because the model is flexible and extensible.
-
Scope
Every template has a root scope derived from the provided context struct.
Loops and conditional blocks create child scopes.- Scope is resolved by leading periods:
.= current scope..= parent scope- more periods walk upward
- Appending a field accesses that scope’s field (
..field)
- Scope is resolved by leading periods:
-
Statements vs Expressions
- Expressions:
{| <expression> |}
Used to render values into the output. - Statements:
||zz <statement> zz||
Used for control flow such as loops and conditionals.
- Expressions:
-
Truthiness & Optionals
- Optional fields (
?T) may be used directly inifstatements nullevaluates as false- Booleans behave as expected
- Optional fields (
-
String Literals & Comparisons
- Single-quoted string literals are supported inside statements :
'example' - Equality and comparison operators may be used in conditionals
- Single-quoted string literals are supported inside statements :
-
JSON Serialization
{| .field json |}serializes a field to JSON- serialization options are passed to the
renderfunction
More complete examples can be found in
tests/rendering.zigandtests/templating.zig
const MyStruct = struct { field: []const u8 };
var tmpl = try zemplate.Template(MyStruct).init(
allocator,
.{ .field = "World" },
);
defer tmpl.deinit();
const render = try tmpl.render(
\\ Hello {|.field|}!
, .{});
// Output: "Hello World!"slices and arrays are iterable and will be rendered item-by-item:
var tmpl = try zemplate.Template(MyStruct).init(
allocator,
.{ .field = "World" },
);
defer tmpl.deinit();
const render = try tmpl.render(
\\ ||zz for .field zz||
\\ {|.|}
\\ ||zz endfor zz||
, .{});
// Output:
// W
// o
// r
// l
// dconst Test = struct {
opt: ?[]const u8,
};
var tmpl = try zemplate.Template(Test).init(
allocator,
.{ .opt = "optional" },
);
defer tmpl.deinit();
const render = try tmpl.render(
\\ ||zz if .opt zz||
\\ {|.|}
\\ ||zz endif zz||
, .{});
// Output:
// optionalconst Test = struct {
value: u32,
inner: struct { text: []const u8 },
};
var tmpl = try zemplate.Template(Test).init(
allocator,
.{ .value = 42, .inner = .{ .text = "inner string" } },
);
defer tmpl.deinit();
const render = try tmpl.render(
\\ ||zz if .value == 42 zz||
\\ {|.value|} == 42
\\ ||zz endif zz||
\\ ||zz if .inner.text == 'inner string' zz||
\\ {|.inner.text|} == 'inner string'
\\ ||zz endif zz||
, .{});const Nested = struct { inner: []const u8 };
const TestStruct = struct { field: Nested };
var tmpl = try zemplate.Template(TestStruct).init(
allocator,
.{ .field = .{ .inner = "World" } },
);
defer tmpl.deinit();
const render = try tmpl.render(
\\ Hello {|.field.inner|}!
, .{});
// Output: "Hello World!"const Test = struct { field: struct { key: u32 } };
var tmpl = try zemplate.Template(Test).init(
allocator,
.{ .field = .{ .key = 42 } },
);
defer tmpl.deinit();
const render = try tmpl.render(
\\ Hello {| .field json |}!
, .{});
// Output: "Hello {\"key\":42}!"- Associate templates with any struct
- Basic string interpolation
- JSON rendering
- For loops
- If statements
- String literals
- Performance optimization