Xây dựng một trang web danh bạ nghe có vẻ đơn giản cho đến khi bạn thấy mình phải kết nối cơ sở dữ liệu, các lớp bộ nhớ đệm (caching layers) và các framework front-end phản ứng (reactive) chỉ để hiển thị một thứ về cơ bản là một mục lục được biên soạn kỹ lưỡng. Gần đây tôi đã xây dựng Social Tools List, một trang web để so sánh các phần mềm mạng xã hội. Mục tiêu của tôi là đưa nó lên mạng thật nhanh, giữ cho nó chạy nhanh và tránh việc phải duy trì cơ sở hạ tầng cho những nội dung vốn chỉ thay đổi khi tôi thêm hoặc cập nhật một công cụ. Tôi đã quyết định chọn một stack ưu tiên tĩnh (static-first stack): Astro để tạo trang web, TypeScript cho dữ liệu có cấu trúc, và Cloudflare Workers để triển khai. Kết quả là một trang web tải tức thì, chi phí lưu trữ gần như bằng không và không cần quản trị cơ sở dữ liệu.

Tại sao hướng tiếp cận ưu tiên tĩnh (Static-First) lại hợp lý cho một trang danh bạ

Nhiều ứng dụng web mặc định sử dụng server rendering hoặc kiến trúc single-page vì chúng mang lại cảm giác là những lựa chọn hiện đại và an toàn. Nhưng không phải trang web nào cũng nhận dữ liệu đầu vào động từ người dùng trong mỗi yêu cầu. Social Tools List là một nguồn tài nguyên thiên về đọc (read-heavy). Dữ liệu so sánh chỉ thay đổi khi tôi đẩy một bản cập nhật, chứ không phải khi khách truy cập nhấn làm mới trang. Việc render HTML trước giúp loại bỏ nhu cầu truy vấn cơ sở dữ liệu tại edge, biên dịch template tức thời (on the fly), hay gánh nặng hydration trong trình duyệt. Tôi tạo trang web tại thời điểm build, triển khai các tệp tĩnh và để một worker nhẹ nhàng xử lý phần bao bọc (wrapper). Điều này giúp giữ thời gian phản hồi ở mức thấp và loại bỏ hoàn toàn một nhóm các lỗi runtime.

Lưu trữ dữ liệu trong TypeScript, thay vì cơ sở dữ liệu

Tôi không sử dụng cơ sở dữ liệu. Mỗi công cụ trong danh bạ được định nghĩa dưới dạng một đối tượng TypeScript với slug, tên, tên miền và một mảng các quy trình làm việc (workflows) được hỗ trợ. Một mục điển hình trông như thế này:

{
  slug: 'buffer',
  name: 'Buffer',
  domain: 'buffer.com',
  workflows: ['scheduling', 'analytics']
}

Việc lưu trữ dữ liệu bằng cùng một ngôn ngữ dùng để xây dựng trang web mang lại hai lợi ích tức thì. Thứ nhất, các pull request trở thành các đợt kiểm duyệt nội dung. Khi tôi thêm một công cụ, phần diff sẽ hiển thị chính xác các trường và giá trị, và đồng nghiệp có thể phát hiện lỗi đánh máy hoặc sai tên miền mà không cần phải học cách sử dụng giao diện CMS. Thứ hai, trình biên dịch TypeScript sẽ thực thi cấu trúc (shape) của mọi bản ghi. Nếu tôi quên thêm slug hoặc viết sai khóa workflow, quá trình build sẽ thất bại trước khi dữ liệu lỗi kịp xuất hiện trên trang.

Một cơ sở dữ liệu sẽ kéo theo các vấn đề về migration, chuỗi kết nối (connection strings), chiến lược bộ nhớ đệm và các quy trình sao lưu. Đối với một danh bạ chỉ có vài trăm mục mà tôi tự duy trì thủ công, sự cồng kềnh đó hoàn toàn là gánh nặng. Dữ liệu tĩnh trong các module TypeScript là lựa chọn đúng đắn và tiết kiệm nhất cho mô hình này. Phần "đúng đắn" rất quan trọng. Nó không chỉ là về việc tiết kiệm tiền; mà là về việc loại bỏ các lớp trừu tượng (abstraction layers) vốn chỉ để giải quyết những vấn đề mà tôi không hề gặp phải.

Để Astro đảm nhận việc định tuyến (routing) và render

Astro tạo ra một trang HTML cho mỗi công cụ từ một route động duy nhất. Tôi định nghĩa một component layout, và Astro sẽ tự động tạo ra metadata, các tiêu đề và dữ liệu có cấu trúc cho mọi mục. Vì cùng một tập dữ liệu được dùng để vận hành trang chủ, các trung tâm danh mục workflow và các trang chi tiết riêng lẻ, nên sẽ không có chuyện một thẻ (card) trên trang chủ hiển thị mô tả khác với chính trang chi tiết đó. Trong các thiết lập CMS truyền thống, bạn thường thấy sự sai lệch (drift): API trả về một phiên bản, bộ nhớ đệm trả về một phiên bản khác, và render phía client lại là phiên bản thứ ba. Việc tạo trang tĩnh từ một nguồn sự thật duy nhất (single source of truth) sẽ ngăn chặn điều đó.

Kiến trúc island (island architecture) của Astro cũng giúp dễ dàng thêm các thành phần tương tác nhỏ mà không làm "nhiễm độc" toàn bộ trang bằng JavaScript. Trang web được xuất bản dưới dạng HTML tĩnh, và chỉ có script lọc (filtering script) là thực hiện hydration tại một góc cụ thể của DOM. Không có runtime của framework nào bao bọc toàn bộ tài liệu. Astro cũng coi metadata của trang là một yếu tố quan trọng hàng đầu (first-class concern). Mỗi trang công cụ đều có thẻ title và meta description riêng được trích xuất trực tiếp từ bản ghi TypeScript, vì vậy tôi không cần một plugin riêng biệt hay một thư viện quản lý head.

Lọc dữ liệu mà không cần framework

Việc tìm kiếm và lọc thường thôi thúc các nhà phát triển cài đặt React, Vue hoặc một thư viện quản lý trạng thái (state management) nặng nề. Tôi đã kháng cự lại điều đó. Astro render toàn bộ danh sách dưới dạng HTML thuần trên server. Một đoạn script vanilla JavaScript nhỏ, chưa đến 1 kilobyte, sẽ chạy trong trình duyệt và chuyển đổi thuộc tính display của các mục trong danh sách dựa trên thẻ workflow hoặc một chuỗi văn bản khớp.

Việc gửi toàn bộ markup nghe có vẻ không hiệu quả nếu bạn có nền tảng về phát triển dựa trên API. Nhưng hãy xem xét chi phí vận hành của một cách tiếp cận động điển hình. Trình duyệt tải xuống một gói JavaScript, hydrate một cây thành phần, gọi một endpoint, chờ đợi JSON, và sau đó mới hiển thị các hàng. Đối với một danh mục liệt kê ít hơn một trăm công cụ, quy trình đó chậm hơn và kém tin cậy hơn việc ẩn các thẻ div đã có sẵn trong tài liệu. Script của tôi gắn các trình lắng nghe sự kiện vào các nút lọc, đọc thuộc tính data-workflow trên mỗi hàng và đặt các mục không khớp thành hidden. Thao tác này chỉ mất vài mili giây.

Vì danh sách đã có sẵn trong HTML ban đầu, trang web vẫn có thể sử dụng được mà không cần JavaScript. Các trình thu thập dữ liệu của công cụ tìm kiếm có thể thấy mọi liên kết và mọi mô tả. Người dùng trên mạng chậm hoặc sử dụng trình chặn script vẫn nhận được toàn bộ danh mục. Việc lọc là một sự cải tiến, không phải là một rào cản.

Sitemaps và Robots dưới dạng Code

Sitemaps và robots.txt không phải là những phần bổ sung được viết tay sau cùng. Chúng là các Astro routes sử dụng cùng một tập dữ liệu và các URL helpers như