An open API service indexing awesome lists of open source software.

https://github.com/lalinsky/zt

HTML template language that compiles to Zig
https://github.com/lalinsky/zt

html template web-development zig zig-package

Last synced: 2 months ago
JSON representation

HTML template language that compiles to Zig

Awesome Lists containing this project

README

          

# Zig Templating

*This is still an experimental project. Feedback is welcome, but use with caution.*

A small HTML templating language that compiles to Zig at build-time.
Inspired by [Templ], [Zeix] and [JSX].

The idea is to invent as little syntax as possible, just enough to make it possible to interweave real Zig code with HTML elements.
Using this approach, the template compiler can stay very simple and delegate Zig code analysis to the real Zig compiler.

Templates are transpiled into Zig source files as part of `zig build`, so you can just import them like any other
source files in your application. Everything is fully type-checked by the Zig compiler, and there is no
overhead at runtime. Output is directly written to a `std.Io.Writer`, so there is no state and no allocations.

[JSX]: https://react.dev/learn#writing-markup-with-jsx
[Zeix]: https://ziex.dev/
[Templ]: https://templ.guide/

## Installation

```bash
zig fetch --save git+https://github.com/lalinsky/zt
```

Configure your `build.zig`:

```zig
const std = @import("std");
const zt = @import("zt");

pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

const zt_dep = b.dependency("zt", .{
.target = target,
.optimize = optimize,
});

// Compile templates (.zt → .zig)
const templates = zt.addTemplates(b, zt_dep, &.{
b.path("src/templates/pages.zt"),
});

const root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
root_module.addImport("zt", zt_dep.module("zt"));

const exe = b.addExecutable(.{
.name = "myapp",
.root_module = root_module,
});
exe.step.dependOn(templates);

b.installArtifact(exe);
}
```

## Usage

```zig
const std = @import("std");
const pages = @import("templates/pages.zig");

pub fn main() !void {
var buf: [4096]u8 = undefined;
var stdout = std.fs.File.stdout().writer(&buf);
const w = &stdout.interface;

try pages.Home.render(.{ "Welcome" }, w);
try w.flush();
}
```

---

## Example

```zig
const Post = @import("../models.zig").Post;
const User = @import("../models.zig").User;

fn formatDate(ts: i64) []const u8 {
// ...
}

pub templ Layout(title: []const u8) {



{title}



@Nav()

@children



}

pub templ Nav() {

Home
About

}

pub templ PostCard(post: Post) {

{post.title}



if (post.subtitle) |subtitle| {

{subtitle}


}

{for (post.tags) |tag| {tag}}


}

pub templ HomePage(user: ?User, posts: []const Post) {
@Layout("Home") {
if (user) |u| {

Welcome back, {u.name}!


} else {

Welcome, guest! Log in


}

for (posts) |post| {
@PostCard(post)
}

}
}
```

---

## Syntax

### Templates

Templates are defined with `templ` and compile to structs with a `render` method:

```zig
pub templ Greeting(name: []const u8) {

Hello, {name}!


}
```

### Zig Code

Standard Zig code goes at the top of the file and is passed through unchanged:

```zig
const std = @import("std");
const Post = @import("../models.zig").Post;

fn formatDate(ts: i64) []const u8 {
// ...
}

pub templ Article(post: Post) {


{post.title}



}
```

---

## Elements

```zig
pub templ Document() {



My Page




Content




}
```

All HTML elements must be explicitly closed, but void elements like `` and `` will be rendered correctly without the closing slash.

---

## Attributes

**Static** - quoted string values:

```zig


```

**Dynamic** - Zig expressions in braces:

```zig


```

**Interpolated** - mix static and dynamic parts:

```zig
Read more
```

**Boolean** - presence means true:

```zig

```

**Optional** - attribute omitted when value is null:

```zig
Link


```

---

## Expressions

**Escaped output** (default) - safe for user content:

```zig
{comment.text}
```

**Raw output** - for trusted HTML only:

```zig

{!article.html_content}

```

---

## Control Flow

### If/Else

Block-level:

```zig
if (user.is_admin) {
Admin
}

if (post.subtitle) |subtitle| {

{subtitle}


}
```

Inline:

```zig
{if (user.premium) "Pro" else "Free"}
{if (error) |msg|

{msg}
}
```

### For Loops

Block-level:

```zig


    for (items) |item| {
  • {item.name}

  • }

for (rows, 0..) |row, i| {
...
}
```

Inline:

```zig
{for (links) |link| {link.title}}
```

### Switch

Block-level:

```zig
switch (order.status) {
.pending => {
Processing
},
.shipped => |tracking| {
Track package
},
else => {
Unknown
},
}
```

Inline:

```zig
{switch (user.role) .admin => Admin, .mod => Mod, else => User}
```

---

## Components

Call other templates with `@`:

```zig
pub templ Nav() {
...
}

pub templ Page() {
@Nav()
Content
}
```

With imports:

```zig
const ui = @import("ui.zig");

pub templ Page() {
@ui.Button("Click me")
}
```

### Children

Parent templates use `@children` to render nested content:

```zig
pub templ Card(title: []const u8) {


{title}



@children


}

pub templ Page() {
@Card("Welcome") {

This appears inside the card body.


}
}
```

Nesting works to any depth:

```zig
pub templ Page() {
@Layout("Home") {
@Card("News") {

Latest updates...


}
}
}
```

### Components as Parameters

Templates can accept `zt.Component` for dynamic composition:

```zig
pub templ Modal(title: []const u8, body: zt.Component) {


}
```

Create components with `bind`:

```zig
const args: templates.Alert.Args = .{ "Something went wrong" };
const alert = templates.Alert.bind(&args);
try templates.Modal.render(.{ "Error", alert }, writer);
```

---

## API

Each template generates:

- `render(args, writer) !void` - render to a writer
- `bind(args) zt.Component` - create a type-erased component
- `Args` - the argument tuple type

See `examples/` for a runnable project:

```bash
cd examples
zig build run
```