Type-safe HTML templating in Zig

(lalinsky.com)

122 points | by lukaslalinsky 5 hours ago

11 comments

  • pjmlp 3 hours ago
    > 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.

    • lukaslalinsky 3 hours ago
      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.
      • pjmlp 2 hours ago
        Fair enough, different life experience then.
  • stefs 1 hour ago
    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.

    • kitd 1 hour ago
      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.
  • fergie 4 hours ago
    W3C: Why can't we just have <includes src="./header.html"> and be done with it?
    • tannhaeuser 3 hours ago
      SGML (ISO 8879), the markup meta-language on which HTML is based, has that, as well as many other things, such as type-checked templating.

      Hence HTML as a mere vocabulary making use of SGML's facilities didn't need to define its own ad-hoc mechanism.

      Btw the <?php ...> construct is also derived from SGML. In SGML terms it's a "processing instruction" rather than an "entity reference", the latter being the basic means for content inclusion.

    • cton 4 hours ago
    • pwdisswordfishq 2 hours ago
    • pautasso 3 hours ago
      Nice idea!

      https://web.dev/articles/imports

      Been there, tried that.

      Introduced in 2014 (Chrome 36) Deprecated 2018 (Chrome 70) Retired 2019 (Chrome 73)

    • jrimbault 4 hours ago
      <?php include "header.html" ?>
      • fergie 4 hours ago
        Yes, but then you need php

        Why is there no direct support for templating in HTML itself?

        • alternatex 2 minutes ago
          It's a markup language for documents. If you enable templating devs are then going to ask for more. Eventually it will end up turing complete. Such is the development arc of a lot of languages.
      • messe 4 hours ago

            <!-- #include file="header.html" -->
        
        https://en.wikipedia.org/wiki/Server_Side_Includes
        • fergie 4 hours ago
          SSIs are not widely supported any more, and were never a part of the HTML standard

          (but yes- I used these a lot like 25 years ago)

          • jonatron 1 hour ago
            From what I can tell, apache mod_include and nginx SSI are still included by default?
    • geoffmunn 3 hours ago
      Can I introduce you to iframes? Clunky but they do exactly what you are asking for.
      • brazukadev 22 minutes ago
        and in a much safer way, sandboxed.
  • weitzj 3 hours ago
    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?

    • lukaslalinsky 2 hours ago
      Fun fact, I actually also had a fork of the Google Closure Templates that added support for Python. I used it briefly. I completely forgot about it, but I remember now I did enjoy using it back then. https://pythonhosted.org/Flask-Soy/
  • Humphrey 4 hours ago
    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.
    • lucasdicioccio 1 hour ago
      I recently built some templating language (rather an intermediary language for documents) for solving an accumulated frustration over the years. Already added into four or five of my projects (for templating html, emails, dashboards, chatbots messages, and some configs).

      The design point "deterministic, hostable, with enough to make hooks from the templates" is what makes it special: https://lucasdicioccio.github.io/tramaj/ .

    • marcle 4 hours ago
      I always thought that the type-safe XML/HTML in Ur/Web was elegant.

      For examples, see http://www.impredicative.com/ur/demo/

    • riffraff 3 hours ago
      There was a brief period where everyone and their dog implemented mustache[0] as a template system, but it died quickly because cross-language templating is not that useful in the end, you seldom need to switch host language but keep the templates.

      0: https://en.wikipedia.org/wiki/Mustache_(template_system)

      • bvrmn 1 hour ago
        I think the craze was about "purity" of mustache as presentation-only template engine. Apparently you need some level of logic to be useful.
    • sorokod 4 hours ago
    • dnautics 4 hours ago
      This is sort of halfway between tsx and Elixir's EEx
    • boxed 3 hours ago
      I think nim also has this.

      As for Django... well.. Django only has text formatting, you can write any garbage and broken html and lots of invalid templates with silent failures there unfortunately.

      • pelme 29 minutes ago
        We created https://htpy.dev to solve this for our Django app. Has been using it in production for multiple years. Works great with Django (or any Python web framework).
      • insin 1 hour ago
        This had me checking if anyone has implemented first-class support for Python JSX components in Django yet, and it could be... sort of... maybe viable with PyJSX? [1]

        It looks like you'd have to implement something equivalent to provider/context functionality yourself if you want it, as well as a simple render()-like helper to render a component tree. I've not tried their VS Code plugin, but it's a good sign that they have one.

        [1] https://github.com/tomasr8/pyjsx

  • orf 2 hours ago
    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...

    • lukaslalinsky 2 hours ago
      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.
      • ioasuncvinvaer 1 hour ago
        > 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.

        • lukaslalinsky 1 hour ago
          It's a different approach to Rust. There, if you cancel a future, it stops polling and runs the Drop interface. You don't get to do more complex cleanup. But, at the same time, because cancellation is not an error that all functions have to propagate, it's structurally not possible to ignore it. Zig, like many other languages, uses a specific error to indicate cancellation. The application can decide what to do with it, it has some benefits. But unlike Python/Java where exceptions can he thrown from anywhere, Zig errors need to be named and that's where to issues comes from, because the Reader/Writer interfaces no longer are generic, they can't pass the concrete errors returned from the implementations, and cancellation is one of the errors that get hidden that way. It's a comprise either way, Zig would need to have first class support for comptime interfaces/traits to solve it better, but that's not going to happen. So in the meantime, there is a dilema how to handle it, but I'm sure it will get solved, or at least best practices will get developed.
          • ioasuncvinvaer 1 hour ago
            I am not really convinced that new best practices will be developed or in included in the standard library because andrew thinks that there isn't an issue.
            • lukaslalinsky 54 minutes ago
              I have a partially written blog post in my head, as I've spent a lot of time thinking about the problem. I'd have preferred if there was a change to the reader/writer interfaces, but even without it, I'm starting to see patterns that work and can be applied to any code to make it safe. People just need to be aware of the problem and that's my goal, although I'm probably failing if my tone is creating the drama, so I'm learning how to communicate it better.
              • ioasuncvinvaer 46 minutes ago
                I don't think you are creating any drama to be honest. I understand how frustrating it must be.

                There is a similar issue in kotlin where a cancellation is marked via a CancellationException but that extends IllegalStateException and thus Exception. Because of that it gets caught and swallowed all the time. This is a constant source of bugs for me at work and google has resorted to a bunch of linting to get around this design mistake.

        • jeltz 1 hour ago
          Cancellation in Rust is problematic too but for other reasons than cancellation in Zig is problematic. Maybe there is just no free lunch here, but hopefully it can be fixed in Zig and Rust.
    • brodo 1 hour ago
      The answer from Andrew sounds like that because there is a history between people from the core team and lalinsky.
    • ptsneves 1 hour ago
      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.

  • _kidlike 3 hours ago
    sounds very similar to jte.gg for Java
  • vogelke 3 hours ago
    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>
  • slopinthebag 5 hours ago
    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" }
                }
             }
          }
       }
    • dnautics 4 hours ago
      > 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.

      • josephg 4 hours ago
        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>
                }
            }
        • vrighter 25 minutes ago
          does it still work if you remove that space before the tag's closing character in this line?

              on:click=move |_| set_count.set(3) >
      • slopinthebag 3 hours ago
        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>
            };
    • asimovDev 4 hours ago
      This is just Twig templates, crazy. It's cool that Rust can do this out of a box
      • slopinthebag 4 hours ago
        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

        • lukaslalinsky 3 hours ago
          I'm not a fan of Rust, the overly flexible macros are actually one reason for it, but if you can indeed get LSP support in a custom syntax like this, that's really awesome. Thank you for the example.
    • skavi 3 hours ago
      but you have invented syntax
    • sroerick 4 hours ago
      That is super cool actually
    • eggsandbeer 4 hours ago
      [dead]
  • thetexaslawdog 4 hours ago
    [flagged]
  • jaxxzi 3 hours ago
    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.
    • chii 3 hours ago
      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).

    • NamlchakKhandro 3 hours ago
      eww gross.