Type-safe HTML templating in Zig

15 hours ago (lalinsky.com)

> Two years ago, I picked up Zig, and I’d never have imagined I’d be considering web development in such a low-level language.

This is kind of funny remark, given that the author started in 2000 in Web development, and with PHP 3.0.

I started in 1997, our distributed systems class had exercises to write CGIs in Perl and C.

Between 1999 and 2001, most Web development at my employer was in Tcl and C, ASP was naturally a mix of VB and C++ COM components, and there was also the fun to write all those mod_something Apache extensions, and IIS ISAPI modules, naturally in C and C++.

Ah, Microsoft was also shipping ATLServer, because who wouldn't want to use ATL for Web development, while Borland/Inprise was shipping similar stuff for Delphi and C++ Builder.

PHP itself, in 3.0, it was still the time that it hardly had much functionality builtin, so it was quite common to write extension modules in C.

So it is kind of ironic to read coming back to the way things were, before the slow scripting languages takevover in the 2010's, is something no one thought about.

  • For me web was always about these scripting languages. Even in the CGI days, I learned to use Perl just enough to do a simple web form. I kind of associated the domain with GC languages, as this kind of work usually involves many temporary strings and dictionaries. In fact, the first compiled language I ever used for a web project was Go, and even that was due to the fact that it has garbage collector.

  • Google probably still has a large chunk of web development happening in C++. Probably less than Java but still lots of code.

I kinda like kotlinx.html in Kotlin; it's an eDSL for writing HTML in actual Kotlin. So I can put break points in my template code. It's a tiny bit slower than string-with-some-interpolation type of template libraries; but it's so little that it's a price I'm willing to pay. Also there's more impact on compile times than with string-with-some-interpolation type of template libraries.

The typesafety you get is what you already have in Kotlin, and there a little extra in that you cannot nest certain HTML-elements in certain other elements (adhering to the HTML spec). You can come up with your own elements and attributes, but the ones that are in the HMTL-spec are usually available in kotlinx.html as well (and thus can be auto-completed).

A large part of this library is generated from the actual HTML spec.

Claude Code seems to have not problem writing it (I barely write templates myself these days).

https://github.com/kotlin/kotlinx.html

https://dzone.com/articles/modern-type-safe-template-engines...

  • I wish Kotlin was given more attention by JetBrains than the AI nonsense they keep trying to build into, its clear they are going to lose in the AI space at this rate, but Kotlin has so much more potential and its nearly ten years old at this point? Oh its 15 whoops!

I love this design, especially as it takes advantage of the Zig build chain; I expect re-builds will be fast enough that you essentially get JS-style hot reloading in the dev cycle. Now we just need https://github.com/air-verse/air for Zig (Ziex has one built into its CLI, but that's only for Ziex)...

Looking forward to seeing some benchmarks!

W3C: Why can't we just have <includes src="./header.html"> and be done with it?

> Then I saw Ziex and I was amazed. It never occurred to me that the JSX approach could be done outside of JavaScript.

It never occurred to me, either! This is really neat! How does it work?

EDIT: Never mind, seems like it's a custom file format that gets compiled to Zig code. Still quite neat but unfortunately you lose all the IDE support.

  • There are `rsx` macros in Rust that work with a regular `cargo build` and retain IDE support.

    • That's because Zig explicitly doesn't want any kind of Rust or C style macros, so anything like this has to live in a build script most IDEs won't run. Maybe one day there will be a Zig IDE that takes advantage of the fast compiler.

      I think there is a way to do syntax extensions cleanly, with a special block type like `'{ }` that gets tokenized and passed to a comptime function skipping the parser. But one of Zig's main selling points is no hidden control flow and they want to keep it that way.

i'm also using a "type safe" templating engine (for kotlin, as a gradle plugin) of my own design that goes the other way - instead of embedding a model class into the template, a template class is generated that takes all the values as parameters.

i.e. MyTemplate.tpl.html

    {if $foo}hello world, {$myname}!{else}goodbye, world.{/if}

which creates a kotlin class that can be used like this:

    MyTemplate(
      foo = true,
      myname = "bar"
    ).toString()

the biggest upside for me is that this way there's full, free IDE support, i don't need to write extra view models if the mapping isn't 1:1 and performance is pretty good because in the end there's no runtime parsing/compiling/interpreting except for the baseline JDK stuff (though there are even faster engines).

https://codeberg.org/h34tnet/tempolin

warning: so far i'm the only user though, so don't use it in production. also, i'm working on some improvements right now which haven't been pushed yet.

  • Nice work. This is a good approach IMHO, and (if you need validation) also the one adopted by Templ in Go, which has a 'generate' command to do this.

How does zt compare to zmpl, another popular zig templating engine?

https://github.com/jetzig-framework/zmpl

  • Zmpl wants you to build up a JSON-like data tree, and templates work using those values. That unfortunately means you are back to dealing with runtime errors. Minor thing, but they also want to render the template into an allocated string, rather than writing it to std.Io.Writer directly. And then there is the syntax, I prefer the Go Templ approach.

Thank you for the article. I remember using Google Soy templates roughly 10 years ago. I think this is similar to what is used now when you make Helm templates for example or Go Templates. It has the html escaping, JavaScript escaping etc built in.

Is there maybe a Zig wrapper for Google soy around?

I love the idea! I could imagine something similar created for other languages too. Perhaps written in a way that a template could be reused in, let's say Django, and a JS React frontend. With all the type-safety that tsx provides.

I've never quite got the deal with 'template engines'. I have always just done normal programming - in whatever language I'm using - and interspersed outputting raw string literals to create the html.

  • and this leads to html injection attacks when there are small errors in your escaping.

    Using a template engine means you systematically ensure that values are escaped (at least, that's one of the reasons i would choose an engine over raw dogging the html).

The linked thread here[1] is… illuminating. The Zig stdlib seems pretty buggy, and the much applauded IO interfaces seem to be struggling with cancellation. The BDFL doesn’t seem to have much to say other than “brush your teeth”

1. https://ziggit.dev/t/making-it-harder-to-swallow-error-cance...

  • No, that's not the best description. The Io interface is actually one of the best cancellation stories out there. If you combine it with defer/errdefer, you get safer cancellation guarantees than you have in async Rust (you can do async "drop" for example). The only problematic part is the interaction between the Reader/Writer interface and the Io interface, with regards to error handling.

    • > The only problematic part is the interaction between the Reader/Writer interface and the Io interface, with regards to error handling.

      The fact that error handling suffers when combining two of the base interfaces of the standard library is pretty rough though? I don't think rust has that particular problem.

      11 replies →

  • The answer from Andrew sounds like that because there is a history between people from the core team and lalinsky.

    • Yeah, sounds like they're on good terms. The top replier to the GP disagrees with the GP's mischaracterization.

  • Not a zig person, but the post you linked has a good counter example on why the proposal for adding cancellation to the interface is bad: You would be forcing cancellation semantics on interfaces that might not want nor be able to provide one. Forcing a new method because the libraries are not well designed does not seem right for me. Maybe another solutions is available, but that is not one.

    I get the pain, but the answer from Scott Redig totally makes sense. I am not sure what is worse: Libraries swallowing cancelled() or libraries lying that they support cancelled.

    • The point is libraries are agnostic. You as a library user provide io transport and loop implementation which supports/not supports cancelation. And you as as library user handle cancelation.

      > Libraries swallowing cancelled() or libraries lying that they support cancelled.

      For me former is obviously worse.

If you can suppress your gag reflex, M4 can do some of what you want. sinclude(...) will include a file and be silent in case of failure. I did something like this for user-login history because I couldn't use PHP or Perl:

    me% cat index.m4
    <!DOCTYPE html>
    <html>
      <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Better user history</title>
        <link rel="stylesheet" href="local.css">
      </head>

      <body>
          <h3>Users</h3>
          <ul>
            <li><em>By username</em></li>
            <ul>
    sinclude(`byuser.m4')dnl
            </ul>
            <li><em>By last login date</em></li>
            <ul>
    sinclude(`bydate.m4')dnl
            </ul>
          </ul>
    sinclude(`footer.m4')dnl
      </body>
    </html>

    me% cat byuser.m4
    <li><a href="byuser/A-L">A-L</a></li>
    <li><a href="byuser/M-Z">M-Z</a></li>

    me% cat bydate.m4
    <li><a href="bydate/2025">2025</a></li>
    <li><a href="bydate/2026">2026</a></li>

    me% cat footer.m4
    <p><em>Last update: syscmd(`date | tr -d "\n"')</em>

    me% m4 index.m4 > index.htm

    me% cat index.htm
    <!DOCTYPE html>
    <html>
      <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Better user history</title>
        <link rel="stylesheet" href="local.css">
      </head>

      <body>
          <h3>Users</h3>
          <ul>
            <li><em>By username</em></li>
            <ul>
    <li><a href="byuser/A-L">A-L</a></li>
    <li><a href="byuser/M-Z">M-Z</a></li>
            </ul>
            <li><em>By last login date</em></li>
            <ul>
    <li><a href="bydate/2025">2025</a></li>
    <li><a href="bydate/2026">2026</a></li>
            </ul>
          </ul>
    <p><em>Last update: Mon Oct 12 03:08:32 EDT 2026</em>
      </body>
    </html>

one thing i love about rust's macros is that you can accomplish this without needing a separate file type or inventing syntax

   #[component]
   pub fn HomePage(user: Option<User>) -> Element {
      rsx! {
         Layout { 
            title: "Home",
            if let Some(u) = user {
               h1 { "Welcome back, {u.name}!" }
               a { href: "/users/{u.id}", "Your profile" }
            } else {
               h1 { "Welcome, guest!" }
               a { href: "/login", "Log in" }
            }
         }
      }
   }

  • > you can accomplish this

    That's not HTML though, you have to learn a whole new dsl that must conform to rust parser rules. So, not the same thing at all.

    • You can do actual html. This is Leptos, a Solidjs like rust library:

          #[component]
          fn App() -> impl IntoView {
              let (count, set_count) = signal(0);
              view! {
                  <button
                      on:click=move |_| set_count.set(3) >
                      "Click me”</button>
                  <p>{move || count.get() * 2}</p>
              }
          }

      1 reply →

    • i prefer it, but you can also get html if you want

      eg

         html! {
             <div onclick=|_ev: web_sys::MouseEvent| {}>
                You can type text right into the elements
                { component }
                { text_var }
             </div>
          };

  • This is just Twig templates, crazy. It's cool that Rust can do this out of a box

    • yep and you get rust's lsp support too, autoformatting, etc.

      this is rsx, but there are other macros that look just like html. i prefer this though, it's much more compact and readable imo

      2 replies →