I’ve done a lot of web betterment complete the years. I started about the twelvemonth 2000, fair following PHP 3.0 was released and started taking complete the web world. I’ve used all kinds of approaches for generating HTML: raw PHP, cord chain in Python, Smarty templates in PHP, Template Toolkit in Perl, and Jinja/Django templates in Python. Then, as the earth moved on to SPAs and JavaScript frameworks, I stopped operating on frontends.
Two years ago, I picked up Zig, and I’d never have imagined I’d be considering web betterment in specified a low-level language. But as I was construction my own ecosystem alongside zio and Dusty, I realized it’s not that far-fetched. I built these libraries for API backends, anywhere the output is JSON or MessagePack, but afterward I saw that alongside appropriate use of arena allocators, equal web betterment is really manageable, since you don’t need to recall to deallocate all the strings that are typically involved. And alongside Zig’s comptime, you can build extremely high-level interfaces without losing the achievement benefits. And alongside the assistance of htmx or Datastar, you can equal build SPAs without touching JavaScript/TypeScript, which I promised myself I’d never do.
That said, one item was motionless missing: templating. String chain was not an option, I didn’t desire to go rear to the pre-2000 CGI days. I had a few requirements for a templating library:
- It have to be type-safe, alongside all inputs typed and checked at compile time. I’ve dealt alongside energetic languages adequate in my life, and I don’t desire to agreement alongside runtime template errors anymore.
- It have to be fast. No allocations, no intermediate state, fair compose the output to std.Io.Writer directly.
- It needs to have harmless defaults. I don’t desire to agreement alongside XSS anymore, so all inputs have to be liberated by default.
- It should motionless awareness akin penning HTML. I didn’t desire a Zig-based DSL I’d have to combat constantly.
I couldn’t discover any resolution that would fulfill all these requirements. Then I saw Ziex and I was amazed. It never occurred to me that the JSX method could be done exterior of JavaScript. The developer cognition Nurul Huda managed to build in Zig was fair unimaginable to me. It didn’t fit my needs directly, since I didn’t desire a WASM framework, but the syntax and his method to integrating Zig code into templates gave me several ideas. Then, following additional research, I discovered Templ for Go, and it was beautiful apparent to me what I wanted to build.
So, let me current zt (I’m not fine at naming, sorry). It’s an adaptation of Templ to Zig. The templates are parsed and transpiled to Zig code during zig build. That method you can import them immediately from your app code, but also, perchance equal additional importantly, the templates can import your app code. As a result, all parameters can be completely typed. If item can’t be rendered, the Zig compiler volition inform you so. The generated code is fairly simple, all it does is compose to a std.Io.Writer. You can compose immediately to a socket or accumulate the output in a buffer, your choice. Here is what a template looks like:
const User = @import("../models.zig").User; pub templ Layout(title: []const u8) { <!DOCTYPE html> <html> <head> <title>{title}</title> </head> <body> @children </body> </html> } pub templ HomePage(user: ?User) { @Layout("Home") { if (user) |u| { <h1>Welcome back, {u.name}!</h1> <a href="/users/{u.id}">Your profile</a> } else { <h1>Welcome, guest!</h1> <a href="/login">Log in</a> } } }
And current is how you would use it from Zig code:
const std = @import("std"); const User = @import("models.zig").User; const pages = @import("templates/pages.zig"); pub fn main(init: std.process.Init) !void { var buf: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buf); const w = &stdout.interface; const user: User = .{ .id = 1, .name = "Lukas" }; try pages.HomePage.render(.{user}, w); try w.flush(); }
If you desire to use it inner Dusty to render an HTML page, you fair do this:
try res.render(.html, pages.HomePage, .{user});
As a flank note, the chief snippet complete has a extremely non-obvious bug in error handling if you use the identical form location std.Io cancellation matters. I’ll likely compose a blog article concerning it, but if you are interested, inspect out this Ziggit thread. The render function in Dusty that wraps the std.Io.Writer handling solves it.
All energetic values are HTML-escaped by default, the two in content and in attributes, and values in URL attributes akin href or src are additionally checked for hazardous schemes. If you really need to output raw HTML, you have to ask for it explicitly alongside {!value}, so it’s uncomplicated to place in the template. The parser additionally requires all elements to be correctly closed, so you can’t accidentally create damaged markup. It won’t defend you from everything, but it removes the most average sources of XSS.
What I akin the most is that template errors are fair Zig compiler errors. If you create a typo in the template complete and compose {u.nmae} alternatively of {u.name}, zig build volition inform you:
src/templates/pages.zig:45:43: error: no site 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 to the generated code, but all row has a comment alongside the location in the first template. The identical applies to the calling side, if you try to continue a cord anywhere HomePage expects a ?User, it won’t compile.
Beyond the basics shown here, you can build reusable components and continue them about as values, and alongside @children you can build layouts that cover another templates, which covers what you’d use template bequest for in another languages. Control stream is fair Zig, so if, for and toggle activity as you’d expect, including payload captures. All of it motionless compiles downward to a sequence of writes, without any allocations.
You can have a appearance at the TODO example in Dusty. It’s a uncomplicated htmx-based web app that showcases this library, affirmative another parts of Dusty that are needed for construction a energetic SPA.
One part of the syntax is motionless open. Block-level authority stream is written as plain Zig, following the example of Templ, so a row starting alongside if, for or toggle is parsed as code. That method content akin “if you forgot your password…” on its own row is a parse error, and you have to cover it in an component or compose it as a cord expression. I’m considering switching to @if, @for and @switch, akin to Razor or Twirl, which would eliminate the ambiguity, at the disbursal of slightly noisier templates. If you have an opinion on this, delight depart a comment in this GitHub issue.
Although multiple group are already using it, the project is not extremely mature yet, so I greeted any feedback. You can open a GitHub matter if you discover any problems, or ping me on the Zig Discord.