Phục vụ 12.237 trang luật chỉ trong 0,3 giây với Astro và Zero Client JavaScript

06 tháng 4, 2026·8 phút đọc

Ley Abierta là nền tảng mã nguồn mở lưu trữ toàn bộ luật pháp Tây Ban Nha với hiệu suất vượt trội. Bài viết chia sẻ kiến trúc kỹ thuật sử dụng Astro để tạo trang tĩnh, SQLite cho tìm kiếm toàn văn và AI để phát hiện các luật "omnibus", đạt điểm Lighthouse tuyệt đối.

Phục vụ 12.237 trang luật chỉ trong 0,3 giây với Astro và Zero Client JavaScript

Luật pháp là tài sản công cộng. Việc đọc và tra cứu luật không nên tốn tới 200 euro mỗi tháng.

Đó là lý do tôi xây dựng Ley Abierta, một nền tảng mã nguồn mở lập chỉ mục cho mọi đạo luật của Tây Ban Nha từ năm 1835 đến nay. Con số hiện tại là 12.237 đạo luật với 42.000 commit Git theo dõi mọi lần sửa đổi. Điểm hiệu suất Lighthouse: 100/100.

Dưới đây là cách hệ thống này hoạt động.

Vấn đề cần giải quyết

Công báo chính thức của Tây Ban Nha (BOE) công bố luật pháp dưới dạng XML. Nếu bạn muốn lấy văn bản hợp nhất của một đạo luật (đã bao gồm các sửa đổi), bạn chỉ có hai lựa chọn:

  1. Đọc trực tiếp trên trang web BOE (chúc bạn may mắn khi điều hướng giao diện của nó).
  2. Trả tiền cho các dịch vụ như Westlaw hay Aranzadi.

Không có nguồn dữ liệu nào miễn phí, có thể tìm kiếm và kiểm soát phiên bản (version-controlled). Vì vậy, tôi quyết định tự xây dựng một cái.

Tổng quan kiến trúc

Dữ liệu được luân chuyển qua quy trình sau:

BOE API → Pipeline (Bun) → Git repo (Markdown) → Astro (SSG) → Cloudflare Pages → SQLite + FTS5 → Elysia API → Hetzner Docker

Cùng một dữ liệu được lưu trữ ở ba nơi, mỗi nơi phục vụ một mục đích khác nhau:

LayerFormatMục đích
JSON cache12.231 tệp JSONNguồn gốc của Pipeline
Git repoMarkdown + YAML frontmatterDễ đọc cho con người, kiểm soát phiên bản
SQLite14 bảng + chỉ mục FTS5Truy vấn nhanh, tìm kiếm toàn văn

Website hoàn toàn tĩnh. API xử lý tìm kiếm và các tính năng động (cảnh báo email, phát hiện luật omnibus). Hai thành phần này được triển khai độc lập.

Tối ưu hóa hiệu suất với Astro: 12.000 trang, một lần build

Mỗi đạo luật là một tệp Markdown trong một kho Git công khai (leyabierta/leyes). Tại thời điểm build, Astro sẽ checkout repo này và xử lý mỗi tệp như một entry của bộ sưu tập nội dung.

Metadata được lưu trong phần frontmatter:

---
title: "Ley 35/2006, de 28 de noviembre, del Impuesto sobre la Renta de las Personas Físicas"
rank: "ley"
status: "vigente"
published_at: "2006-11-29"
jurisdiction: "es"
materias: ["IRPF", "Hacienda Pública", "Impuestos"]
reforms_count: 47
---

Astro tạo một trang HTML tĩnh cho mỗi entry. Quá trình build mất khoảng 45-60 giây cho tất cả 12.231 trang bằng tính năng queued rendering của Astro 6.1.1 với độ tương tranh 4-worker.

Kết quả đầu ra là HTML thuần túy trên CDN của Cloudflare. Không có gì để hydrate, không có gì để phân tích trên máy khách. Thời gian tải cơ bản chỉ là độ trễ của kết nối mạng.

Performance:  100
FCP:          0.3s
LCP:          0.7s
TBT:          0ms

Quy trình Pipeline hàng ngày

Mỗi buổi sáng, một workflow trên GitHub Actions sẽ tự động chạy:

  1. Khám phá các đạo luật mới từ BOE API.
  2. Tải XML song song (6 workers, có giới hạn tốc độ).
  3. Phân tích metadata, các điều khoản và lịch sử sửa đổi.
  4. Commit mỗi đạo luật dưới dạng tệp Markdown, sử dụng ngày đăng thực tế làm ngày commit.
  5. Đẩy lên repo leyes.
  6. Kích hoạt rebuild Astro nếu có bất kỳ thay đổi nào.

Vào Chủ Nhật, hệ thống sẽ chạy kiểm tra lại toàn bộ để bắt các cập nhật cho các luật hiện có. Các ngày trong tuần chỉ xử lý tăng thêm (incremental) các ấn bản mới.

Vấn đề trước năm 1970

Git lưu trữ ngày dưới dạng Unix timestamps. Đạo luật cũ nhất trong cơ sở dữ liệu từ năm 1835, tức là trước kỷ nguyên Unix.

Giải pháp của tôi: ngày commit được đặt thành 1970-01-02 (ngày an toàn sớm nhất), nhưng ngày công bố thực tế nằm trong YAML frontmatter và một Git trailer tùy chỉnh (Source-Date: 1835-05-24). Web và API luôn sử dụng ngày thực. Lịch sử Git hiển thị ngày giữ chỗ.

Vấn đề này ảnh hưởng đến khoảng 334 đạo luật. Tuy không hoàn hảo, nhưng nó bảo toàn mô hình "một commit cho mỗi sửa đổi" giúp lệnh git diff hoạt động trên toàn bộ kho luật.

Những "kỳ quặc" của BOE API (Kinh nghiệm xương máu)

Một số điều mà tài liệu sẽ không nói với bạn:

  • Header Accept: application/json trả về lỗi 400 trên endpoint /texto. Bạn bắt buộc phải parse XML.
  • Tham số limit=-1 bị giới hạn âm thầm ở 10.000 kết quả. Luôn sử dụng phân trang với offset rõ ràng.
  • Endpoint /analisis chỉ trả về một tập con các chủ đề. Để lấy danh sách đầy đủ, bạn cần scrape thẻ meta ELI từ phiên bản HTML.
  • Luật của các khu vực sử dụng ID từ các bản tin vùng (BOA, BOJA, DOGV), không phải BOE. Phân quyền phải được trích xuất từ mẫu URL ELI (/eli/es-pv/ → Xứ Basque).

Tìm kiếm toàn văn với SQLite FTS5

Tìm kiếm cần nhanh và không phân biệt dấu (ví dụ: "politica" phải khớp với "política"). Tiện ích mở rộng FTS5 của SQLite xử lý việc này một cách tự nhiên.

Chỉ mục tìm kiếm bao gồm tiêu đề luật, văn bản đầy đủ và các thẻ thân thiện với công dân:

CREATE VIRTUAL TABLE norms_fts USING fts5(
  norm_id UNINDEXED,
  title,
  content,
  citizen_tags
);

Truy vấn sử dụng phương pháp hai bước: các kết quả khớp với tiêu đề sẽ được xếp hạng cao hơn nội dung. Kết quả được phân trang bằng bộ lọc ID theo từng phần để tránh giới hạn biến của SQLite trên các tập kết quả lớn (chia thành các phần 5.000 mục).

API (Elysia chạy trên Bun) cung cấp tính năng này dưới dạng REST endpoint với các bộ lọc cho cấp bậc, trạng thái, phân quyền và danh mục chủ đề. Tài liệu Swagger có sẵn tại api.leyabierta.es/swagger.

Phát hiện luật Omnibus bằng AI

Luật "omnibus" gộp nhiều chủ đề không liên quan vào một văn bản luật duy nhất. Chính phủ thường sử dụng chúng để lén lút đưa ra các biện pháp không được ủng hộ qua sự kiểm duyệt của công chúng. Tại Tây Ban Nha, việc này diễn ra thường xuyên. Ví dụ như một cải cách thuế bị giấu trong một sắc lệnh thiên tai. Không ai theo dõi việc này, nên tôi đã xây dựng một công cụ phát hiện.

Cách phát hoạt động

  1. Nếu một đạo luật chạm đến 15+ danh mục chủ đề khác nhau (sau khi lọc các từ chung như "Public Administration"), đánh dấu nó là omnibus.
  2. Trích xuất cấu trúc của luật (tiêu đề, chương, điều) và gửi đến Gemini Flash.
  3. Mô hình tạo ra nhãn, tiêu đề, tóm tắt, số lượng điều và một boolean sneaked_in (lén lút) cho mỗi chủ đề.

Cờ sneaked_in là phần thú vị nhất. Nó bắt được những chủ đề không liên quan gì đến tiêu đề chính thức của luật. Ví dụ như quy định năng lượng bị chôn vùi trong một cập nhật an sinh xã hội.

{
  "topic_label": "Energía (medida encubierta)",
  "headline": "New renewable energy requirements",
  "article_count": 8,
  "sneaked_in": true
}

Chi phí: Khoảng 0,01 USD/ngày sử dụng Gemini Flash qua OpenRouter.

Kết quả được cung cấp qua API, hiển thị trên trang /omnibus và có sẵn dưới dạng nguồn cấp RSS.

Thông báo Email

Người dân có thể đăng ký các chủ đề họ quan tâm. Khi một luật ảnh hưởng đến những chủ đề đó được sửa đổi, họ sẽ nhận được email với bản tóm tắt bằng ngôn ngữ đơn giản.

Hệ thống dựa trên sự kiện (event-driven):

  1. Cron hàng ngày tạo tóm tắt AI cho các cải cách mới.
  2. Khớp chủ đề của người đăng ký với các chủ đề cải cách.
  3. Gửi qua Resend (email giao dịch).
  4. Theo dõi trong notified_reforms để tránh trùng lặp.

Double opt-in (xác nhận đăng ký hai bước) sử dụng các liên kết xác thực được ký HMAC. Không cần xác thực, đăng ký được quản lý bằng token email.

Những điều tôi sẽ làm khác nếu bắt đầu lại

Sử dụng SQLite ngay từ đầu. Tôi đã dành vài tuần để truy vấn repo Git trực tiếp trước khi chấp nhận rằng Git không phải là một cơ sở dữ liệu. git log --grep không thể thay thế cho câu lệnh WHERE materia = 'IRPF' AND status = 'vigente'.

Tôi cũng không nên tin tưởng vào tài liệu BOE API. Chúng thiếu sót và ở một số nơi sai hoàn toàn. Việc bắt đầu bằng việc scrape các endpoint và tìm ra hành vi thực tế ngay từ đầu đã tiết kiệm được rất nhiều thời gian.

Một vấn đề của Astro: bộ sưu tập nội dung với hơn 12.000 tệp sẽ "ăn" hết bộ nhớ của bạn trong quá trình build nếu không cẩn thận. Tính năng queued rendering trong Astro 6 đã khắc phục điều này, nhưng tôi đã mất vài buổi chiều vì lỗi OOM (thiếu bộ nhớ) trước khi tìm ra giải pháp.

Hãy thử trải nghiệm

Nếu bạn có ý tưởng, phát hiện lỗi hoặc muốn thích ứng hệ thống này cho luật pháp của quốc gia mình, mình rất hoan nghênh các issue và PR.


Tôi là Alex, một lập trình viên độc lập đến từ Tây Ban Nha. Bạn có thể tìm thấy tôi trên LinkedIn.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗