Deemwar Monads & Abstractions

Catch template typos at build time

Source: Type-safe HTML templating in Zig, by Lukáš Lalinský

In Jinja, Django or Smarty templates, a misspelled field shows up only when the page renders. zt, Lukáš Lalinský’s adaptation of Go’s Templ to Zig, turns each template into Zig code during zig build. A typo becomes a compiler error.

1 typo fails the build
0 allocations per render
18:31 template line in the error

The problem

Templates in dynamic languages check nothing until they run. Write {u.nmae} instead of {u.name} and you learn about it from a broken page, not from your build. The author wanted the opposite: “all inputs typed and checked at compile time.”

How zt checks a template

  1. 1

    Write the template

    pages.zt

    The template looks like HTML with Zig expressions in braces. It imports your app types, for example User from models.zig, so every parameter has a type.

  2. 2

    Transpile during zig build

    zt

    zt parses the template and writes Zig code that only writes to a std.Io.Writer.

  3. 3

    Compile

    the Zig compiler

    The compiler checks the generated code against your structs. If something cannot be rendered, the build stops.

  4. fail → the error names the field and the template line · fix pages.zt and build again
  5. 4

    Render

    your app

    Call pages.HomePage.render(.{ user }, w). Output goes straight to a socket or a buffer.

The template and your types meet in one compile step.

The typo, verbatim

Misspell name as nmae and zig build prints:

src/templates/pages.zig:45:43: error: no field named 'nmae' in struct 'models.User'
    try zt.writeEscaped(writer, u.nmae); // pages.zt:18:31
                                 ^~~~
src/models.zig:1:18: note: struct declared here
pub const User = struct {
                 ^~~~~~

The error points at the generated code, but each generated line carries a comment with the location in the original template: pages.zt:18:31. The calling side is checked too: pass a string where HomePage expects a ?User and it won’t compile.

Safe by default

Escaped outputEvery dynamic value is HTML-escaped, in text and in attributes.
Raw HTML on requestOnly {!value} outputs raw HTML, so it is easy to spot.
URL attributesValues in href or src are checked for dangerous schemes.
Closed tagsThe parser requires every element to be closed, so markup cannot break.
No allocationsTemplates compile to a sequence of writes to std.Io.Writer.

The limits, in the author’s words

LimitWhat it means
“It won’t protect you from everything”It removes the most common sources of XSS, not all of them.
“Not very mature yet”Several people use it; the author asks for feedback.
Lines starting with ifA line starting with if, for or switch parses as code, so text like “if you forgot your password…” on its own line is a parse error. He is considering @if, @for, @switch, as in Razor or Twirl.

Try this on your stack

Read the source

Type-safe HTML templating in Zig, Lukáš Lalinský, 11 October 2026.

Discussion on Hacker News.

Sources: the error text, the 18:31 location and the no-allocation claim come from the post above.