{"id":48490598,"url":"https://github.com/lalinsky/zt","last_synced_at":"2026-04-07T11:32:35.110Z","repository":{"id":338316453,"uuid":"1153966770","full_name":"lalinsky/zt","owner":"lalinsky","description":"HTML template language that compiles to Zig","archived":false,"fork":false,"pushed_at":"2026-02-26T09:39:19.000Z","size":149,"stargazers_count":10,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-26T14:54:05.484Z","etag":null,"topics":["html","template","web-development","zig","zig-package"],"latest_commit_sha":null,"homepage":"","language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lalinsky.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-09T21:46:50.000Z","updated_at":"2026-02-26T09:12:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/lalinsky/zt","commit_stats":null,"previous_names":["lalinsky/zt"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/lalinsky/zt","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lalinsky%2Fzt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lalinsky%2Fzt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lalinsky%2Fzt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lalinsky%2Fzt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lalinsky","download_url":"https://codeload.github.com/lalinsky/zt/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lalinsky%2Fzt/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31511645,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T03:10:19.677Z","status":"ssl_error","status_checked_at":"2026-04-07T03:10:13.982Z","response_time":105,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["html","template","web-development","zig","zig-package"],"created_at":"2026-04-07T11:32:33.350Z","updated_at":"2026-04-07T11:32:35.099Z","avatar_url":"https://github.com/lalinsky.png","language":"Zig","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Zig Templating\n\n*This is still an experimental project. Feedback is welcome, but use with caution.*\n\nA small HTML templating language that compiles to Zig at build-time.\nInspired by [Templ], [Zeix] and [JSX].\n\nThe idea is to invent as little syntax as possible, just enough to make it possible to interweave real Zig code with HTML elements.\nUsing this approach, the template compiler can stay very simple and delegate Zig code analysis to the real Zig compiler.\n\nTemplates are transpiled into Zig source files as part of `zig build`, so you can just import them like any other\nsource files in your application. Everything is fully type-checked by the Zig compiler, and there is no\noverhead at runtime. Output is directly written to a `std.Io.Writer`, so there is no state and no allocations.\n\n[JSX]: https://react.dev/learn#writing-markup-with-jsx\n[Zeix]: https://ziex.dev/\n[Templ]: https://templ.guide/\n\n## Installation\n\n```bash\nzig fetch --save git+https://github.com/lalinsky/zt\n```\n\nConfigure your `build.zig`:\n\n```zig\nconst std = @import(\"std\");\nconst zt = @import(\"zt\");\n\npub fn build(b: *std.Build) void {\n    const target = b.standardTargetOptions(.{});\n    const optimize = b.standardOptimizeOption(.{});\n\n    const zt_dep = b.dependency(\"zt\", .{\n        .target = target,\n        .optimize = optimize,\n    });\n\n    // Compile templates (.zt → .zig)\n    const templates = zt.addTemplates(b, zt_dep, \u0026.{\n        b.path(\"src/templates/pages.zt\"),\n    });\n\n    const root_module = b.createModule(.{\n        .root_source_file = b.path(\"src/main.zig\"),\n        .target = target,\n        .optimize = optimize,\n    });\n    root_module.addImport(\"zt\", zt_dep.module(\"zt\"));\n\n    const exe = b.addExecutable(.{\n        .name = \"myapp\",\n        .root_module = root_module,\n    });\n    exe.step.dependOn(templates);\n\n    b.installArtifact(exe);\n}\n```\n\n## Usage\n\n```zig\nconst std = @import(\"std\");\nconst pages = @import(\"templates/pages.zig\");\n\npub fn main() !void {\n    var buf: [4096]u8 = undefined;\n    var stdout = std.fs.File.stdout().writer(\u0026buf);\n    const w = \u0026stdout.interface;\n\n    try pages.Home.render(.{ \"Welcome\" }, w);\n    try w.flush();\n}\n```\n\n---\n\n## Example\n\n```zig\nconst Post = @import(\"../models.zig\").Post;\nconst User = @import(\"../models.zig\").User;\n\nfn formatDate(ts: i64) []const u8 {\n    // ...\n}\n\npub templ Layout(title: []const u8) {\n    \u003c!DOCTYPE html\u003e\n    \u003chtml\u003e\n        \u003chead\u003e\n            \u003ctitle\u003e{title}\u003c/title\u003e\n            \u003clink rel=\"stylesheet\" href=\"/style.css\" /\u003e\n        \u003c/head\u003e\n        \u003cbody\u003e\n            @Nav()\n            \u003cmain\u003e\n                @children\n            \u003c/main\u003e\n        \u003c/body\u003e\n    \u003c/html\u003e\n}\n\npub templ Nav() {\n    \u003cnav\u003e\n        \u003ca href=\"/\"\u003eHome\u003c/a\u003e\n        \u003ca href=\"/about\"\u003eAbout\u003c/a\u003e\n    \u003c/nav\u003e\n}\n\npub templ PostCard(post: Post) {\n    \u003carticle class=\"card\"\u003e\n        \u003ch2\u003e\u003ca href=\"/posts/{post.id}/{post.slug}\"\u003e{post.title}\u003c/a\u003e\u003c/h2\u003e\n        \u003ctime\u003e{formatDate(post.created_at)}\u003c/time\u003e\n        if (post.subtitle) |subtitle| {\n            \u003cp class=\"subtitle\"\u003e{subtitle}\u003c/p\u003e\n        }\n        \u003cdiv class=\"tags\"\u003e\n            {for (post.tags) |tag| \u003cspan class=\"tag\"\u003e{tag}\u003c/span\u003e}\n        \u003c/div\u003e\n    \u003c/article\u003e\n}\n\npub templ HomePage(user: ?User, posts: []const Post) {\n    @Layout(\"Home\") {\n        if (user) |u| {\n            \u003cp\u003eWelcome back, {u.name}!\u003c/p\u003e\n        } else {\n            \u003cp\u003eWelcome, guest! \u003ca href=\"/login\"\u003eLog in\u003c/a\u003e\u003c/p\u003e\n        }\n        \u003csection class=\"posts\"\u003e\n            for (posts) |post| {\n                @PostCard(post)\n            }\n        \u003c/section\u003e\n    }\n}\n```\n\n---\n\n## Syntax\n\n### Templates\n\nTemplates are defined with `templ` and compile to structs with a `render` method:\n\n```zig\npub templ Greeting(name: []const u8) {\n    \u003ch1\u003eHello, {name}!\u003c/h1\u003e\n}\n```\n\n### Zig Code\n\nStandard Zig code goes at the top of the file and is passed through unchanged:\n\n```zig\nconst std = @import(\"std\");\nconst Post = @import(\"../models.zig\").Post;\n\nfn formatDate(ts: i64) []const u8 {\n    // ...\n}\n\npub templ Article(post: Post) {\n    \u003carticle\u003e\n        \u003ctime\u003e{formatDate(post.created_at)}\u003c/time\u003e\n        \u003ch1\u003e{post.title}\u003c/h1\u003e\n    \u003c/article\u003e\n}\n```\n\n---\n\n## Elements\n\n```zig\npub templ Document() {\n    \u003c!DOCTYPE html\u003e\n    \u003chtml\u003e\n        \u003chead\u003e\n            \u003ctitle\u003eMy Page\u003c/title\u003e\n            \u003clink rel=\"stylesheet\" href=\"/style.css\" /\u003e\n        \u003c/head\u003e\n        \u003cbody\u003e\n            \u003cdiv\u003e\n                Content\n            \u003c/div\u003e\n            \u003cimg src=\"avatar.png\" /\u003e\n        \u003c/body\u003e\n    \u003c/html\u003e\n}\n```\n\nAll HTML elements must be explicitly closed, but void elements like `\u003cimg\u003e` and `\u003clink\u003e` will be rendered correctly without the closing slash.\n\n---\n\n## Attributes\n\n**Static** - quoted string values:\n\n```zig\n\u003cdiv class=\"container\" id=\"main\"\u003e\u003c/div\u003e\n```\n\n**Dynamic** - Zig expressions in braces:\n\n```zig\n\u003cdiv class={className} data-id={item.id}\u003e\u003c/div\u003e\n```\n\n**Interpolated** - mix static and dynamic parts:\n\n```zig\n\u003ca href=\"/posts/{post.id}/{post.slug}\"\u003eRead more\u003c/a\u003e\n```\n\n**Boolean** - presence means true:\n\n```zig\n\u003cinput type=\"checkbox\" checked disabled /\u003e\n```\n\n**Optional** - attribute omitted when value is null:\n\n```zig\n\u003ca href={item.url}\u003eLink\u003c/a\u003e\n\u003cdiv class={if (isActive) \"active\" else null}\u003e\u003c/div\u003e\n```\n\n---\n\n## Expressions\n\n**Escaped output** (default) - safe for user content:\n\n```zig\n\u003cspan\u003e{comment.text}\u003c/span\u003e\n```\n\n**Raw output** - for trusted HTML only:\n\n```zig\n\u003cdiv\u003e{!article.html_content}\u003c/div\u003e\n```\n\n---\n\n## Control Flow\n\n### If/Else\n\nBlock-level:\n\n```zig\nif (user.is_admin) {\n    \u003cspan class=\"badge\"\u003eAdmin\u003c/span\u003e\n}\n\nif (post.subtitle) |subtitle| {\n    \u003ch2\u003e{subtitle}\u003c/h2\u003e\n}\n```\n\nInline:\n\n```zig\n\u003cspan\u003e{if (user.premium) \"Pro\" else \"Free\"}\u003c/span\u003e\n{if (error) |msg| \u003cdiv class=\"error\"\u003e{msg}\u003c/div\u003e}\n```\n\n### For Loops\n\nBlock-level:\n\n```zig\n\u003cul\u003e\n    for (items) |item| {\n        \u003cli\u003e{item.name}\u003c/li\u003e\n    }\n\u003c/ul\u003e\n\nfor (rows, 0..) |row, i| {\n    \u003ctr class={if (i % 2 == 0) \"even\" else \"odd\"}\u003e...\u003c/tr\u003e\n}\n```\n\nInline:\n\n```zig\n\u003cnav\u003e{for (links) |link| \u003ca href={link.url}\u003e{link.title}\u003c/a\u003e}\u003c/nav\u003e\n```\n\n### Switch\n\nBlock-level:\n\n```zig\nswitch (order.status) {\n    .pending =\u003e {\n        \u003cspan class=\"yellow\"\u003eProcessing\u003c/span\u003e\n    },\n    .shipped =\u003e |tracking| {\n        \u003ca href={tracking}\u003eTrack package\u003c/a\u003e\n    },\n    else =\u003e {\n        \u003cspan\u003eUnknown\u003c/span\u003e\n    },\n}\n```\n\nInline:\n\n```zig\n{switch (user.role) .admin =\u003e \u003cb\u003eAdmin\u003c/b\u003e, .mod =\u003e \u003ci\u003eMod\u003c/i\u003e, else =\u003e \u003cspan\u003eUser\u003c/span\u003e}\n```\n\n---\n\n## Components\n\nCall other templates with `@`:\n\n```zig\npub templ Nav() {\n    \u003cnav\u003e...\u003c/nav\u003e\n}\n\npub templ Page() {\n    @Nav()\n    \u003cmain\u003eContent\u003c/main\u003e\n}\n```\n\nWith imports:\n\n```zig\nconst ui = @import(\"ui.zig\");\n\npub templ Page() {\n    @ui.Button(\"Click me\")\n}\n```\n\n### Children\n\nParent templates use `@children` to render nested content:\n\n```zig\npub templ Card(title: []const u8) {\n    \u003cdiv class=\"card\"\u003e\n        \u003ch2\u003e{title}\u003c/h2\u003e\n        \u003cdiv class=\"body\"\u003e\n            @children\n        \u003c/div\u003e\n    \u003c/div\u003e\n}\n\npub templ Page() {\n    @Card(\"Welcome\") {\n        \u003cp\u003eThis appears inside the card body.\u003c/p\u003e\n    }\n}\n```\n\nNesting works to any depth:\n\n```zig\npub templ Page() {\n    @Layout(\"Home\") {\n        @Card(\"News\") {\n            \u003cp\u003eLatest updates...\u003c/p\u003e\n        }\n    }\n}\n```\n\n### Components as Parameters\n\nTemplates can accept `zt.Component` for dynamic composition:\n\n```zig\npub templ Modal(title: []const u8, body: zt.Component) {\n    \u003cdiv class=\"modal\"\u003e\n        \u003ch2\u003e{title}\u003c/h2\u003e\n        @body\n    \u003c/div\u003e\n}\n```\n\nCreate components with `bind`:\n\n```zig\nconst args: templates.Alert.Args = .{ \"Something went wrong\" };\nconst alert = templates.Alert.bind(\u0026args);\ntry templates.Modal.render(.{ \"Error\", alert }, writer);\n```\n\n---\n\n## API\n\nEach template generates:\n\n- `render(args, writer) !void` - render to a writer\n- `bind(args) zt.Component` - create a type-erased component\n- `Args` - the argument tuple type\n\nSee `examples/` for a runnable project:\n\n```bash\ncd examples\nzig build run\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flalinsky%2Fzt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flalinsky%2Fzt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flalinsky%2Fzt/lists"}