Lập trình mẫu HTML an toàn kiểu (type-safe) trong Zig với thư viện zt

Công nghệ12 tháng 10, 2026·5 phút đọc

Lukáš Lalinský giới thiệu zt — thư viện templating HTML cho Zig lấy cảm hứng từ Templ của Go, giúp biên dịch template thành mã Zig ngay lúc build. Cách tiếp cận này mang lại kiểm tra kiểu ở compile time, tự động escape để chống XSS và không cấp phát bộ nhớ, hứa hẹn đưa lập trình web trở lại với ngôn ngữ hệ thống.

Lập trình web bằng một ngôn ngữ hệ thống như Zig nghe có vẻ lạ, nhưng Lukáš Lalinský đang chứng minh điều đó hoàn toàn khả thi. Bài viết trên blog cá nhân của ông giới thiệu zt — một thư viện templating HTML được chuyển thể từ Templ của Go sang Zig, với mục tiêu giải quyết bài toán sinh HTML an toàn và nhanh chóng.

Từ PHP đến Zig: hành trình tìm kiếm giải pháp templating

Tác giả bắt đầu làm web từ khoảng năm 2000, khi PHP 3.0 vừa ra đời. Ông đã trải qua gần như mọi cách sinh HTML: viết PHP thuần, nối chuỗi bằng Python, dùng Smarty trong PHP, Template Toolkit trong Perl, rồi Jinja/Django. Khi làn sóng SPA và framework JavaScript lên ngôi, ông rời bỏ mảng frontend.

Hai năm trước, ông chuyển sang Zig và xây dựng hệ sinh thái riêng gồm zio và Dusty cho backend API (đầu ra là JSON hoặc MessagePack). Nhận ra rằng nhờ arena allocator, việc phát triển web cũng trở nên dễ quản lý — không cần nhớ giải phóng từng chuỗi — cộng với comptime của Zig cho phép tạo giao diện cấp cao mà không đánh đổi hiệu năng, ông bắt đầu nghĩ lại về web.

Với sự hỗ trợ của htmx hoặc Datastar, thậm chí có thể xây dựng SPA mà không cần chạm đến JavaScript hay TypeScript — điều ông từng tự hứa sẽ không bao giờ làm.

Bốn yêu cầu khắt khe cho thư viện templating

Tuy nhiên, vẫn thiếu một mảnh ghép: templating. Nối chuỗi không phải là lựa chọn, và tác giả không muốn quay lại thời kỳ CGI trước năm 2000. Ông đặt ra bốn yêu cầu:

  • An toàn kiểu (type-safe): mọi đầu vào được định kiểu và kiểm tra ngay lúc biên dịch, không còn lỗi template lúc chạy.
  • Nhanh: không cấp phát, không trạng thái trung gian, ghi thẳng ra std.Io.Writer.
  • Mặc định an toàn: mọi đầu vào được escape tự động để chống XSS.
  • Vẫn có cảm giác viết HTML: không phải một DSL Zig khiên cưỡng, khó chiến đấu.

Không giải pháp nào đáp ứng đủ cả bốn. Bước ngoặt đến khi ông thấy Ziex — chứng minh rằng cách tiếp cận JSX có thể tồn tại ngoài JavaScript. Dù Ziex hướng tới WASM không hợp nhu cầu, cú pháp của nó đã truyền cảm hứng. Sau đó, ông phát hiện Templ của Go và biết chính xác mình muốn xây dựng điều gì.

zt hoạt động như thế nào?

zt là bản chuyển thể của Templ sang Zig. Template được phân tích và biên dịch thành mã Zig ngay trong quá trình zig build. Điều này có hai hệ quả quan trọng: bạn có thể import template trực tiếp từ mã ứng dụng, và ngược lại — template cũng có thể import mã ứng dụng. Nhờ đó, mọi tham số đều được định kiểu đầy đủ. Nếu một giá trị không thể render, trình biên dịch Zig sẽ báo lỗi ngay.

Mã sinh ra khá đơn giản, chỉ ghi vào std.Io.Writer. Bạn có thể ghi trực tiếp ra socket hoặc tích lũy vào buffer tùy ý.

Cú pháp template trông như sau:

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

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


{title}


@children


}

pub templ HomePage(user: ?User) {
  @Layout("Home") {
    if (user) |u| {
      Welcome back, {u.name}!
      Your profile
    } else {
      Welcome, guest!
      Log in
    }
  }
}

Và đây là cách dùng từ mã Zig:

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

const user: User = .{ .id = 1, .name = "Lukas" };
try pages.HomePage.render(.{user}, w);
try w.flush();

Nếu dùng trong Dusty để render trang HTML, chỉ cần một dòng:

try res.render(.html, pages.HomePage, .{user});

An toàn XSS theo mặc định

Mọi giá trị động đều được escape HTML tự động, cả trong văn bản lẫn thuộc tính. Các giá trị trong thuộc tính URL như href hay src còn được kiểm tra scheme nguy hiểm. Nếu thực sự cần xuất HTML thô, bạn phải yêu cầu tường minh bằng cú pháp {!value}, giúp dễ nhận diện trong template.

Trình phân tích cú pháp cũng buộc mọi phần tử phải được đóng đúng cách, tránh sinh ra markup hỏng do vô ý. Tác giả thừa nhận điều này không bảo vệ khỏi mọi thứ, nhưng loại bỏ những nguồn XSS phổ biến nhất.

Lỗi template chính là lỗi trình biên dịch

Điểm tác giả tâm đắc nhất: lỗi template trở thành lỗi biên dịch Zig. Nếu gõ nhầm {u.nmae} thay vì {u.name}, zig build sẽ báo:

error: no field named 'nmae' in struct 'models.User'

Thông báo trỏ tới mã sinh ra, nhưng mỗi dòng đều có comment ghi vị trí trong template gốc. Điều tương tự áp dụng cho phía gọi: nếu truyền chuỗi vào chỗ HomePage mong đợi ?User, mã sẽ không biên dịch.

Ngoài ra, bạn có thể xây dựng component tái sử dụng và truyền chúng như giá trị. Cú pháp @children cho phép tạo layout bao bọc template khác — tương đương kế thừa template ở ngôn ngữ khác. Luồng điều khiển chính là Zig, nên if, for, switch hoạt động như mong đợi, kể cả payload capture.

Tất cả vẫn biên dịch thành một chuỗi lệnh ghi, không cấp phát bộ nhớ.

Một điểm còn bỏ ngỏ

Một phần cú pháp vẫn đang mở. Luồng điều khiển cấp khối được viết bằng Zig thuần, theo mẫu của Templ — dòng bắt đầu bằng if, for hay switch bị hiểu là mã. Điều này khiến câu văn như "if you forgot your password…" đứng riêng một dòng trở thành lỗi phân tích cú pháp, buộc phải bọc trong phần tử hoặc viết dưới dạng biểu thức chuỗi.

Tác giả đang cân nhắc chuyển sang @if, @for, @switch kiểu Razor hoặc Twirl, loại bỏ sự nhập nhằng với cái giá là template hơi rối hơn. Ai có ý kiến có thể để lại bình luận trên GitHub issue của dự án.

Dù đã có người dùng, zt vẫn chưa thực sự trưởng thành. Tác giả hoan nghênh mọi phản hồi qua GitHub issue hoặc kênh Discord của Zig.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗