Lập trình viên viết lại toàn bộ website bằng Swift, không còn dùng Astro
Sindre Sorhus đã chuyển toàn bộ website cá nhân từ Astro sang Swift, dùng cú pháp gần giống SwiftUI để tạo HTML, CSS và JavaScript. Nhờ compiler kiểm tra mọi liên kết, style và dữ liệu, trang web trở nên nhỏ gọn, nhanh và dễ bảo trì hơn.

Website cá nhân của lập trình viên Sindre Sorhus trước đây được xây dựng bằng Astro. Giờ đây, toàn bộ trang — từng trang, từng component, từng đoạn style — đều được viết bằng Swift.
Điều thú vị là người dùng cuối vẫn chỉ nhận được HTML, CSS thuần và một chút JavaScript. Thay đổi nằm ở cách website được tạo ra, và ở mức độ thoải mái khi làm việc với nó.
Vì sao lại là Swift?
Tác giả viết Swift cả ngày và yêu thích ngôn ngữ này. Anh không còn phải chuyển đổi qua lại giữa các ngôn ngữ và công cụ khi đi từ ứng dụng sang website.
Nhưng lý do thực sự nằm ở compiler. Một website gồm rất nhiều thứ nhỏ phải khớp với nhau: liên kết, tên class, style, script và dữ liệu của hơn 60 ứng dụng. Trong một stack web điển hình, chúng chỉ gặp nhau trên trình duyệt, và một sai sót sẽ biến thành một trang hỏng. Ở đây, phần lớn chúng gặp nhau ngay tại compiler.
Đổi tên một trang, và mọi liên kết tới nó trong code sẽ không còn biên dịch được.
Cú pháp trông như SwiftUI
Một component là một struct với thuộc tính body:
struct BackLink: HTML {
let app: App
var body: some HTML {
a(.href(app.path)) {
"Quay lại \(app.title)"
}
.style {
$0
.secondaryText()
.fontWeight(.medium)
.hoverColor(.primaryText)
}
}
}
Phần HTML đến từ thư viện Elementary. Phần còn lại — style, route và nội dung — là framework nhỏ do chính tác giả viết thêm.
Ngay cả phần tô màu cú pháp hiển thị trên trang cũng do Swift đảm nhiệm. Quá trình build chạy code qua parser swift-syntax của chính Swift rồi tô màu từng token, nên màu luôn chính xác và không cần JavaScript.
Style là các modifier
Mỗi style được gắn trực tiếp lên phần tử, giống như modifier trong SwiftUI. Khối .style { … } ở trên sẽ sinh ra một class dạng back-link-1x2y3z4, được tạo từ tên file và một hash của style.
Cách làm này mang lại hai lợi ích:
- Style không thể rò rỉ sang phần tử khác
- Khi kiểm tra HTML, bạn biết ngay nó đến từ file nào
Các kiểu hiển thị dùng chung và biến thể được định nghĩa bằng enum, nên chúng tự động gợi ý khi gõ, và một lỗi chính tả sẽ khiến code không biên dịch được.
Badge(title: "Mới", kind: .new)
Mỗi trang chỉ nhận CSS của những component mà nó hiển thị. Bên dưới là CSS hiện đại: cascade layers, light-dark(), relative colors, scroll-driven animations và container units. Không dùng preprocessor, cũng không dùng Tailwind.
Biến CSS có kiểu dữ liệu
Custom property trong CSS vốn chỉ là chuỗi ký tự. Ở đây, chúng có kiểu hẳn hoi:
static let color = StyleVariable("--overflow-menu-color")
// Menu dùng nó, kèm giá trị mặc định
.color(OverflowMenu.color.value(default: .link))
// Phần tử cha gán giá trị
.setting(OverflowMenu.color, to: .bodyText)
Gán một giá trị độ dài vào biến màu sẽ không biên dịch được. Với RegisteredProperty, trang còn sinh ra rule @property, cho phép trình duyệt chạy animation — ví dụ như hiệu ứng gradient trên trang chủ.
Đáp ứng và dễ truy cập
Breakpoint và media query đều có kiểu, và các media query kết hợp được với nhau bằng &&:
Style()
.grid(columns: 2)
.from(.tablet) {
$0.gridColumns(3)
}
.media(.coarsePointer) {
$0.padding(.rootEm(0.75))
}
.media(.hover && .finePointer && .allowsMotion) {
$0.hover {
$0.setting(titleLight, to: .percent(35))
}
}
Đoạn cuối chính là vệt sáng chạy theo con trỏ trên tiêu đề ở trang danh sách ứng dụng. Nó chỉ xuất hiện khi dùng chuột hoặc touchpad, và chỉ khi người dùng cho phép hiệu ứng chuyển động.
Màu sắc mang tính ngữ nghĩa, ví dụ .secondaryText. Mỗi màu có giá trị cho chế độ sáng và tối, đồng thời màu chữ, liên kết và đường kẻ sẽ đậm hơn khi người dùng bật chế độ tương phản cao trong cài đặt hệ thống. Các component được hưởng toàn bộ điều này miễn phí.
Liên kết
Liên kết hoạt động giống hệt trong SwiftUI. Các trang là thành viên tĩnh, nên .apps sẽ tự động gợi ý, và một trang không tồn tại sẽ khiến code không biên dịch được. Sau khi build, mọi liên kết và ID trong HTML sinh ra cũng được kiểm tra lại.
Script
Một component có hành vi là một custom element. Swift render HTML, còn script chính là class của phần tử đó. Script nhỏ nằm ngay trong file Swift, script lớn hơn đặt trong file .js bên cạnh. Mỗi script chỉ tác động lên phần tử của chính nó, nên không thể làm hỏng thứ khác trên trang. Không cần framework, không cần bundler.
Nhỏ và nhanh
Một bài blog chỉ nặng khoảng 14 KB sau khi nén: 6,6 KB HTML, 5,4 KB CSS và 1,8 KB JavaScript. Một trang ứng dụng, ví dụ trang của Lungo, nặng khoảng 29 KB trước khi tính đến ảnh chụp màn hình.
Ngoài một stylesheet dùng chung nhỏ, mỗi trang chỉ tải style và script của các component mà nó dùng. Không có web font, và ảnh nằm dưới màn hình đầu tiên được tải theo cơ chế lazy. Trên Chrome, chỉ cần đưa con trỏ lên một liên kết là trang đã được tải trước trong nền, nên thường mở ra tức thì.
Nội dung như dữ liệu
Mỗi ứng dụng là một file Markdown với metadata có kiểu:
@Frontmatter
struct Frontmatter {
@NonEmpty var title: String
@NonEmpty var subtitle: String
@CalendarDay var publicationDate: Date
var platforms: [Platform]
var isPaid = false
}
@Frontmatter là một macro Swift ghi phần decoder ngay tại thời điểm biên dịch. @NonEmpty từ chối giá trị rỗng, còn @CalendarDay chuyển '2017-07-26' thành Date và loại bỏ những ngày không tồn tại như '2026-02-30'.
Một lỗi — như nền tảng không xác định hay khóa viết sai chính tả — sẽ báo lỗi kèm tên file và số dòng, đôi khi còn gợi ý "Có phải bạn muốn viết…".
Quá trình build còn lấy xếp hạng, giá và đánh giá của từng ứng dụng từ App Store. Lệnh swift run website check sẽ bắt những gì compiler không thể phát hiện, chẳng hạn lỗi chính tả, hay một trang ghi ứng dụng cần macOS 14 trong khi App Store ghi macOS 15.
Niềm vui khi làm việc
Cảnh báo cũng khiến build thất bại, và hàng trăm bài kiểm thử sẽ xác minh những gì hệ thống kiểu không thể làm — ví dụ mọi class đều có rule CSS tương ứng. Nhờ vậy, tác giả có thể thực hiện những thay đổi lớn mà không sợ hỏng.
Mọi thứ đều tự động gợi ý, và việc refactor chỉ đơn giản là đổi tên. Code đọc như SwiftUI, không còn mớ class hỗn độn và không phải ghi nhớ selector trong đầu. Một component giữ markup, style và script ở cùng một chỗ, và khi xóa nó, CSS lẫn JavaScript cũng biến mất theo.
Làm việc với website giờ thú vị chẳng kém gì làm việc với các ứng dụng.
Mã nguồn mở
Mã nguồn của dự án đã được mở.
Một điều nữa
Bài kiểm tra căng thẳng nhất cho toàn bộ hệ thống này là trang chủ của tác giả từ năm 1999 — phiên bản trang web cá nhân thời đó, với sổ lưu bút, một game người ngoài hành tinh, một con kỳ lân ảo và cả máy phát nhạc MIDI. Trang này có tới 81 custom element, tất cả đều dùng chung component và style với phần còn lại của website.
Với các lập trình viên Việt Nam đang tìm cách tối ưu quy trình phát triển web, hướng tiếp cận này đáng để tham khảo: đưa càng nhiều kiểm tra về phía compiler thì càng ít lỗi lọt ra tới người dùng cuối.