Ankit Patial 5ce319b335 Fix mixin expansion in conditionals and include resolution in extends
- mixin.zig: expandNode and expandNodeWithArgs now recurse into
  node.consequent and node.alternate for Conditional nodes
- view_engine.zig: process includes and collect mixins from child
  template before extracting blocks in processExtends

This fixes mixin calls inside if/else blocks not being rendered
in compiled templates.
2026-01-30 22:24:27 +05:30
2026-01-17 20:07:55 +05:30
2026-01-28 19:38:59 +05:30

Pugz

A Pug template engine written in Zig. Templates are parsed and rendered with data at runtime.

Features

  • Pug syntax (tags, classes, IDs, attributes)
  • Interpolation (#{var}, !{unescaped})
  • Conditionals (if, else if, else, unless)
  • Iteration (each, while)
  • Template inheritance (extends, block, append, prepend)
  • Includes
  • Mixins with parameters, defaults, rest args, and block content
  • Comments (rendered and unbuffered)
  • Pretty printing with indentation

Installation

Add pugz as a dependency in your build.zig.zon:

zig fetch --save "git+https://github.com/ankitpatial/pugz#main"

Then in your build.zig:

const pugz_dep = b.dependency("pugz", .{
    .target = target,
    .optimize = optimize,
});

exe.root_module.addImport("pugz", pugz_dep.module("pugz"));

Usage

ViewEngine

The ViewEngine provides file-based template management for web servers.

const std = @import("std");
const pugz = @import("pugz");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Initialize once at server startup
    var engine = pugz.ViewEngine.init(.{
        .views_dir = "views",
    });
    defer engine.deinit();

    // Per-request rendering with arena allocator
    var arena = std.heap.ArenaAllocator.init(allocator);
    defer arena.deinit();

    const html = try engine.render(arena.allocator(), "pages/index", .{
        .title = "Hello",
        .name = "World",
    });

    std.debug.print("{s}\n", .{html});
}

Inline Templates

For simple use cases or testing, render template strings directly:

const html = try pugz.renderTemplate(allocator,
    \\h1 Hello, #{name}!
    \\ul
    \\  each item in items
    \\    li= item
, .{
    .name = "World",
    .items = &[_][]const u8{ "one", "two", "three" },
});

With http.zig

const pugz = @import("pugz");
const httpz = @import("httpz");

var engine: pugz.ViewEngine = undefined;

pub fn main() !void {
    engine = pugz.ViewEngine.init(.{
        .views_dir = "views",
    });
    defer engine.deinit();

    var server = try httpz.Server(*Handler).init(allocator, .{}, handler);
    try server.listen();
}

fn handler(_: *Handler, _: *httpz.Request, res: *httpz.Response) !void {
    res.content_type = .HTML;
    res.body = try engine.render(res.arena, "pages/home", .{
        .title = "Hello",
        .user = .{ .name = "Alice" },
    });
}

Compiled Templates (Maximum Performance)

For production deployments, pre-compile .pug templates to Zig functions at build time. This eliminates parsing overhead and provides type-safe data binding.

Step 1: Update your build.zig

const std = @import("std");
const pugz = @import("pugz");

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

    const pugz_dep = b.dependency("pugz", .{
        .target = target,
        .optimize = optimize,
    });

    // Add template compilation step
    const compile_templates = pugz.compile_tpls.addCompileStep(b, .{
        .name = "compile-templates",
        .source_dirs = &.{"views/pages", "views/partials"},
        .output_dir = "generated",
    });

    // Templates module from compiled output
    const templates_mod = b.createModule(.{
        .root_source_file = compile_templates.getOutput(),
    });

    const exe = b.addExecutable(.{
        .name = "myapp",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
            .imports = &.{
                .{ .name = "pugz", .module = pugz_dep.module("pugz") },
                .{ .name = "templates", .module = templates_mod },
            },
        }),
    });

    // Ensure templates compile before building
    exe.step.dependOn(&compile_templates.step);

    b.installArtifact(exe);
}

Step 2: Use compiled templates

const templates = @import("templates");

fn handler(res: *httpz.Response) !void {
    res.content_type = .HTML;
    res.body = try templates.pages_home.render(res.arena, .{
        .title = "Home",
        .name = "Alice",
    });
}

Template naming:

  • views/pages/home.pugtemplates.pages_home
  • views/pages/product-detail.pugtemplates.pages_product_detail
  • Directory separators and dashes become underscores

Benefits:

  • Zero parsing overhead at runtime
  • Type-safe data binding with compile-time errors
  • Template inheritance (extends/block) fully resolved at build time

Current limitations:

  • each/if statements not yet supported in compiled mode
  • All data fields must be []const u8

See examples/demo/ for a complete working example.


ViewEngine Options

var engine = pugz.ViewEngine.init(.{
    .views_dir = "views",           // Root directory for templates
    .extension = ".pug",            // File extension (default: .pug)
    .pretty = false,                // Enable pretty-printed output
});
Option Default Description
views_dir "views" Root directory containing templates
extension ".pug" File extension for templates
pretty false Enable pretty-printed HTML with indentation

Memory Management

Always use an ArenaAllocator for rendering. Template rendering creates many small allocations that should be freed together after the response is sent.

var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();

const html = try engine.render(arena.allocator(), "index", data);

Documentation


Benchmarks

Same templates and data (src/tests/benchmarks/templates/), MacBook Air M2, 2000 iterations, best of 5 runs.

Benchmark Modes

Mode Description
Pug.js Node.js Pug - compile once, render many
Prerender Pugz - parse + render every iteration (no caching)
Cached Pugz - parse once, render many (like Pug.js)
Compiled Pugz - pre-compiled to Zig functions (zero parse overhead)

Results

Template Pug.js Prerender Cached Compiled
simple-0 0.8ms 23.1ms 132.3µs 15.9µs
simple-1 1.5ms 33.5ms 609.3µs 17.3µs
simple-2 1.7ms 38.4ms 936.8µs 17.8µs
if-expression 0.6ms 28.8ms 23.0µs 15.5µs
projects-escaped 4.6ms 34.2ms 1.2ms 15.8µs
search-results 15.3ms 34.0ms 43.5µs 15.6µs
friends 156.7ms 34.7ms 739.0µs 16.8µs
TOTAL 181.3ms 227.7ms 3.7ms 114.8µs

Compiled templates are ~32x faster than cached and ~2000x faster than prerender.

Run Benchmarks

# Pugz (all modes)
zig build bench

# Pug.js (for comparison)
cd src/tests/benchmarks/pugjs && npm install && npm run bench

Development

zig build test    # Run all tests
zig build bench   # Run benchmarks

License

MIT

Description
No description provided
Readme MIT 2.2 MiB
Languages
Zig 74.1%
JavaScript 11.9%
Pug 9.3%
HTML 4.7%