Type-safe HTML templating in Zig
In 2000, the author embarked on their web development journey, having witnessed the rise of PHP 3.0. Over the years they explored varying techniques for HTML generation, including raw PHP, string concatenation in Python, Smarty templates in PHP, Template Toolkit in Perl, and Jinja/Django templates in Python. However, the author's focus shifted towards JavaScript frameworks and SPAs (Single Page Applications) as the web landscape evolved, ultimately steering away from front-end development.
Two years ago, with the introduction of Zig, the author found themselves considering web development in a low-level language. This led to the creation of several libraries for API backends, where the output was in JSON or MessagePack format. Through the use of arena allocators, the author discovered that even web development could be manageable without the need to manually deallocate all the strings typically involved.
The author then observed that with Zig's comptime feature, high-level interfaces could be built without sacrificing performance benefits. This realization, combined with the help of htmx or Datastar, enabled the author to build SPAs without resorting to JavaScript/TypeScript. Despite this progress, the author was still in need of a templating solution that met their requirements.
After discovering Ziex, the author was captivated by its JSX-like approach, which could be implemented outside of JavaScript. The developer experience provided by Nurul Huda in Zig was truly remarkable. However, the author's initial requirements did not align perfectly with Ziex, since they did not intend to build a WASM framework. Nonetheless, the syntax and integration of Zig code into templates inspired the author to develop their own solution.
The result is zt, a Zig adaptation of Templ. The templates in zt are parsed and transpiled to Zig code during the zig build process. This allows templates to be imported directly from application code and, more importantly, for templates to access the application code. Consequently, all parameters can be fully typed, ensuring that if something cannot be rendered, the Zig compiler will provide an error message.
The generated code in zt is straightforward, primarily responsible for writing to a std.Io.Writer. This can be done directly to a socket or accumulated in a buffer, depending on the preference. Below are examples of a template and its usage from Zig code:
Template:
```html
<div>
<h1>{title}</h1>
<p>{description}</p>
</div>
```
Usage in Zig code:
```zig
const html = render(&template, &User { .title = "Example", .description = "This is an example template." });
```
When using zt within Dusty for rendering an HTML page, the following code snippet can be utilized:
```zig
const html = render(&template, &User { .title = "Example", .description = "This is an example template." });
dusty.render(html);
```
One notable feature of zt is its handling of dynamic values. By default, all dynamic values are HTML-escaped in both text and attributes to prevent Cross-Site Scripting (XSS) attacks. However, if raw HTML is required, it can be explicitly output using {!value}. Furthermore, the parser enforces proper element closure, eliminating the possibility of producing broken markup. Although it cannot protect against all potential vulnerabilities, zt effectively removes the most common sources of XSS.
The error handling in zt is particularly advantageous, as template errors manifest as Zig compiler errors. If a typo is made in the template, such as {u.nmae} instead of {u.name}, the error is reported in the generated code. Crucially, comments are added to the generated code, indicating the original template's location. This applies to both the template side and the side where the template is invoked.
Moreover, control flow statements like if, for, and switch in zt function identically to their Zig counterparts, providing a familiar experience while ensuring type safety.
Although the syntax for block-level control flow in zt is currently written as plain Zig, the author is considering a switch to @if, @for, and @switch, similar to Razor or Twirl. This change would resolve any ambiguity and improve template readability, albeit at the cost of slightly noisier templates. The author invites feedback on this potential change in the GitHub issue.
Written by urgent.news from Hacker News's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.