Thông tin về Cloudflare trong tài liệu này (giới hạn gói, file _headers, link xem trước, khôi phục bản cũ) cập nhật tới giữa năm 2026. Hãy kiểm tra lại tài liệu mới nhất của Cloudflare trước khi làm theo.
1. Yêu cầu
Yêu cầu chức năng (theo spec đã duyệt)
| # | Yêu cầu |
|---|---|
| F1 | Ở bề rộng 390px và 1440px, màn hình đầu tiên hiện đủ tiêu đề, lợi ích và nút “Mở Mini App trên Zalo” mà không cần cuộn |
| F2 | Lọc 4 tin việc làm mẫu theo Tất cả / Toàn thời gian / Bán thời gian |
| F3 | Phần “Hành trình điểm thưởng” chạy lặp lại, có nút tạm dừng và tiếp tục, hiện dạng tĩnh khi máy bật chế độ giảm chuyển động |
| F4 | Nút chưa nối chức năng hiện thông báo “Chưa hoàn thiện” kèm lý do. Thông báo tự ẩn sau 4 giây hoặc khi người dùng bấm đóng |
| F5 | Thanh thao tác cố định ở đáy chỉ hiện khi màn hình rộng từ 760px trở xuống |
| F6 | Công cụ axe không báo lỗi trợ năng nào ở mức serious hoặc critical |
Yêu cầu phi chức năng
| Thuộc tính | Mục tiêu | Lý do |
|---|---|---|
| Tốc độ | LCP < 2.5s, CLS < 0.1 trên điện thoại tầm trung, mạng 4G | Người dùng chủ yếu mở trang từ Zalo trên điện thoại |
| Dung lượng | JavaScript dưới 15 KB sau khi nén gzip. Chỉ nạp các độ đậm font thật sự dùng, cắt gọn còn bộ ký tự tiếng Việt và Latin | Trang tĩnh, chỉ có ba tương tác nhỏ |
| Phụ thuộc lúc chạy | Không tải gì từ bên thứ ba: không Google Fonts, không CDN, không công cụ thống kê dùng cookie | Để trang nhanh hơn, bảo vệ quyền riêng tư và chạy ổn định trong trình duyệt của Zalo |
| Sẵn sàng | Trang phục vụ từ mạng máy chủ của Cloudflare, không có máy chủ riêng nào có thể bị sập | Không cần backend |
| Trình duyệt | Safari iOS, Chrome Android, webview trong Zalo, trình duyệt desktop hiện hành | Kênh truy cập thực tế |
| Bảo trì | Một lập trình viên mới đọc hiểu toàn bộ mã trong 30 phút | Trang giới thiệu, ít khi thay đổi |
Ràng buộc
- Chưa có backend, hệ thống quản lý nội dung hay dữ liệu thật. Mọi nội dung là nội dung tĩnh, dữ liệu mẫu đều có nhãn.
- Trang chạy trên Cloudflare và chỉ có tiếng Việt.
- Áp dụng quality contract ở mức Full, profile
web. - Chỉ deploy hoặc tạo tài nguyên trên Cloudflare khi chủ dự án đã đồng ý.
2. Kiến trúc tổng thể
BUILD TIME RUN TIME
┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ GitHub repo │ │ Điện thoại / webview Zalo / desktop │
│ src/ (TS, CSS) public/ (font, SVG) │ │ │
│ spec/homepage.feature │ │ GET / ──────────┐ │
└──────────────┬───────────────────────┘ │ GET /assets/* ──┤ │
│ push / PR └────────────────────┼─────────────────┘
▼ ▼
┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ GitHub Actions │ │ Cloudflare edge │
│ lint · typecheck · unit · coverage │ │ Workers static assets │
│ axe · acceptance (playwright-bdd) │ │ index.html (cache ngắn) │
│ mutation (Stryker) · secrets · deps │ │ assets/*.hash.js|css|woff2 │
│ ── merge main ──▶ vite build │ │ (cache 1 năm) │
│ wrangler deploy ───┼─────▶│ _headers: CSP, bảo mật │
└──────────────────────────────────────┘ └──────────────────────────────────────┘
Không có Worker script, database, hay lời gọi API lúc chạy.
Mọi tương tác (lọc, tạm dừng, thông báo) chạy hoàn toàn trên trình duyệt.Vì sao chọn Workers static assets thay vì Pages: Cloudflare đang khuyến nghị dự án mới dùng Workers. Với một trang tĩnh, hai cách này như nhau. Chọn Workers thì sau này có thể thêm một đoạn Worker script nhỏ (ví dụ chuyển hướng /app sang link Mini App) mà không phải chuyển nền tảng.
3. Cấu trúc mã
├── index.html # khung trang, markup ngữ nghĩa, nội dung tĩnh
├── src/
│ ├── main.ts # điểm vào: khởi tạo 3 tính năng, không chứa logic
│ ├── data/content.ts # MỌI dữ liệu: việc làm, các bước, nhãn lọc, thông báo
│ ├── features/
│ │ ├── jobs-filter.ts # logic thuần filterJobs() + gắn DOM
│ │ ├── reward-journey.ts # trạng thái chuyển động + IntersectionObserver
│ │ └── notice.ts # máy trạng thái thông báo + gắn DOM
│ ├── styles/
│ │ ├── tokens.css # màu, font, bo góc; ghi độ tương phản trong comment
│ │ └── main.css
│ └── vite-env.d.ts
├── public/
│ ├── fonts/ # Phudu, Be Vietnam Pro (woff2, subset) + giấy phép OFL
│ └── _headers # header bảo mật & cache
├── spec/homepage.feature # kịch bản đã duyệt → acceptance
├── tests/
│ ├── unit/ # logic thuần: filterJobs, notice reducer
│ ├── component/ # DOM thật + Testing Library + vitest-axe
│ └── e2e/ # playwright-bdd, 390px và 1440px
└── wrangler.jsonc # assets.directory = ./distMã được tách làm hai lớp. Phần xử lý logic không đụng tới DOM, nên dễ viết unit test và mutation test cho kết quả đáng tin. Phần gắn với DOM viết ngắn gọn và được kiểm bằng component test. Nội dung chính nằm sẵn trong HTML, để trang vẫn đọc được nếu JavaScript bị lỗi. Chỉ dữ liệu cần lọc hoặc hiển thị lặp lại mới đặt trong content.ts.
4. Mô hình dữ liệu
type JobType = 'full' | 'part';
interface Job {
title: string; // "Kỹ thuật viên bảo trì máy"
where: string; // "[Tên doanh nghiệp], Từ Sơn": chỗ chưa có dữ liệu để trong ngoặc vuông
type: JobType;
isSample: true; // bắt buộc true cho tới khi có dữ liệu thật
}
type JobFilter = 'all' | JobType;
interface JourneyStep { title: string; text: string } // đúng 3 bước, có thứ tự
type NotReadyTarget = 'mini-app' | 'email' | 'privacy' | 'news';
const NOT_READY: Record<NotReadyTarget, string>; // lý do hiển thịTrường isSample được khai báo cố định là true. Khi chuyển sang dữ liệu thật, lập trình viên phải đổi kiểu này, và trình biên dịch sẽ báo mọi chỗ đang hiện nhãn “dữ liệu mẫu” để sửa, không bỏ sót chỗ nào.
5. Hành vi và trạng thái
Thông báo “Chưa hoàn thiện”
show(target) show(target2)
Hidden ───────────────▶ Shown(msg) ─────────────────▶ Shown(msg2) (hẹn giờ đặt lại)
▲ │ │
│ hết 4 giây │ │ bấm "Đóng thông báo"
└───────────────────────┘◀──┘- Chỉ dùng một bộ hẹn giờ. Mỗi lần gọi
showsẽ huỷ hẹn giờ trước đó, nên thông báo cũ không tự ẩn nhầm thông báo mới. - Vùng
role="status" aria-live="polite"luôn có sẵn trong DOM, chỉ thay nội dung bên trong.
Phần chuyển động
Ba điều kiện sau quyết định phần chuyển động chạy hay dừng:
| Người dùng bấm tạm dừng | Đang hiện trên màn hình | Máy bật giảm chuyển động | Kết quả |
|---|---|---|---|
| không | có | không | chạy |
| có | bất kỳ | bất kỳ | dừng |
| bất kỳ | không | bất kỳ | dừng (để không tốn tài nguyên khi người dùng không nhìn thấy) |
| bất kỳ | bất kỳ | có | hiện tĩnh, thanh tiến trình hiện đầy |
Chuyển động viết bằng CSS keyframes và animation-play-state. JavaScript chỉ bật hoặc tắt một thuộc tính trên khối cha. Nếu người dùng đã bấm tạm dừng, cuộn ra khỏi phần này rồi cuộn lại thì chuyển động vẫn phải giữ trạng thái dừng.
Lọc việc làm
filterJobs(jobs, filter) là hàm thuần, chỉ trả về danh sách đã lọc. Phần giao diện vẽ lại danh sách và cập nhật aria-pressed trên ba nút lọc.
6. Giao tiếp bên ngoài
Khi chạy, trang không gọi API nào. Ở giai đoạn demo, đây là lựa chọn có chủ ý.
| Phần | Khi nào có | Cách nối dự kiến |
|---|---|---|
| Link Mini App | Sau khi Zalo duyệt ứng dụng | Đổi nút thành link <a href>. Có thể thêm đường dẫn /app chuyển hướng 302, để sau này đổi link Mini App mà không phải build lại trang |
| Mã QR | Cùng lúc với link | Ảnh SVG tĩnh, tạo ra khi build |
| Email, địa chỉ, điện thoại | Tỉnh đoàn cung cấp | Sửa index.html |
| Tin hoạt động | Khi có nguồn tin | Xem §9 |
| Việc làm thật | Khi backend có API công khai | Xem §9 |
Mỗi khi một phần trong bảng trên có thông tin thật: đổi nút thành link, xoá dòng tương ứng khỏi bảng Examples trong spec, rồi gửi duyệt lại spec.
7. Phân phối, cache và bảo mật
Cache
| Tài nguyên | Header | Lý do |
|---|---|---|
/assets/* | public, max-age=31536000, immutable | Mỗi lần đổi nội dung, tên file cũng đổi theo |
/fonts/* | như trên | Font không đổi |
/index.html | public, max-age=0, must-revalidate | Người xem thấy bản mới ngay sau khi deploy |
Header bảo mật
/*
Content-Security-Policy: default-src 'self'; img-src 'self' data:; font-src 'self';
style-src 'self'; script-src 'self'; frame-ancestors 'none'; base-uri 'self';
form-action 'none'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()- CSP đặt chặt được vì trang không tải tài nguyên từ bên ngoài. Muốn thêm nguồn mới (công cụ thống kê, ảnh từ CDN) thì phải sửa CSP cho riêng nguồn đó.
- Vite có thể chèn CSS trực tiếp vào HTML. Nếu CSP chặn phần này, cấu hình build để xuất CSS ra file riêng, không thêm
'unsafe-inline'. - Sau khi đặt
frame-ancestors 'none', kiểm tra lại trang vẫn mở bình thường trong trình duyệt của Zalo.
8. Môi trường, CI/CD và vận hành
| Môi trường | Cách tạo | Dùng cho |
|---|---|---|
| Local | npm run dev | Phát triển và chạy test |
| Preview | Mỗi PR tải lên một phiên bản riêng (wrangler versions upload) | Xem và duyệt trước khi merge, chạy test e2e |
| Production | Merge vào main → wrangler deploy | Trang thật |
Quy trình: PR phải qua hết các bước kiểm tra → merge vào main → build → deploy. Bước deploy cần hai secret do chủ dự án tạo: CLOUDFLARE_API_TOKEN (chỉ cần quyền sửa Workers) và CLOUDFLARE_ACCOUNT_ID.
Quay lại bản cũ: chạy wrangler rollback để về phiên bản trước, hoặc revert commit rồi để quy trình tự deploy lại. Trang không lưu dữ liệu nên quay lại bản cũ không làm mất dữ liệu.
Theo dõi: nếu Tỉnh đoàn cần số lượt xem, dùng Cloudflare Web Analytics (không dùng cookie), và khai báo thêm vào CSP cùng chính sách quyền riêng tư. Đặt một bước kiểm tra định kỳ xem trang / còn chạy. Trang không có backend nên không cần cảnh báo phức tạp hơn.
9. Lượng truy cập và mở rộng
Trang tĩnh phục vụ từ mạng máy chủ của Cloudflare chịu được lượng truy cập của cả tỉnh. Kể cả lúc đông người vào nhất (có sự kiện lớn, link được chia sẻ trong các nhóm Zalo), chi phí chủ yếu chỉ là băng thông tải file tĩnh.
| Khi nào | Vấn đề | Hướng xử lý |
|---|---|---|
| Tin hoạt động cập nhật hằng tuần | Sửa HTML bằng tay mỗi tuần sẽ tốn công và dễ sai | (a) viết tin bằng file Markdown trong repo, build lại khi merge; (b) dùng headless CMS và build hook; (c) dùng Worker đọc tin từ API backend và lưu cache. Nên làm (a) trước. |
| Hiện việc làm thật | Dữ liệu thay đổi liên tục, cần luôn mới | Worker script gọi API công khai của backend, lưu cache 5–15 phút |
| Nhiều trang | Một index.html không đủ | Chuyển sang công cụ tạo trang tĩnh (ví dụ Astro), giữ nguyên token và CSS |
| Cần SEO tốt hơn | Meta, Open Graph, dữ liệu cấu trúc | Thêm ngay khi có nội dung thật, kèm ảnh Open Graph để link hiện đẹp khi chia sẻ trên Zalo và Facebook |
| Thêm ngôn ngữ | Hiện chỉ có tiếng Việt | Tách chữ ra file riêng theo từng ngôn ngữ. Chưa làm cho tới khi có nhu cầu |
10. Vị trí trong toàn hệ thống
┌──────────────┐ link/QR ┌──────────────────┐ API ┌──────────────────────┐
│ HOMEPAGE │──────────▶ │ Zalo Mini App │────────▶ │ Backend (HubCom) │
│ (tài liệu │ │ thanh niên │ │ việc làm, voucher, │
│ này) │ └──────────────────┘ │ check-in, quiz, │
└──────────────┘ ┌──────────────────┐ API │ phân quyền địa bàn │
▲ │ Cổng quản trị │────────▶ │ │
│ (tương lai: │ cán bộ, DN │ └─────────┬────────────┘
│ đọc API công khai)└──────────────────┘ │ API
└─────────────────────────────────────────────────┐ ┌────────▼────────────┐
└─│ AI (Thái Sơn) │
│ chưa có đặc tả │
└─────────────────────┘Hiện tại trang chủ chỉ có link dẫn sang Mini App. Trang không có đăng nhập, không lưu dữ liệu người dùng và không gọi backend. Nhờ vậy, trang chủ deploy được riêng, không phải chờ các phần khác của dự án còn đang dở (API AI, cây địa bàn, Zalo duyệt Mini App).
11. Đánh đổi đã chọn
| Hạng mục | Chọn | Thay vì | Lợi ích | Hạn chế |
|---|---|---|---|---|
| Framework | TypeScript thuần | React | File JavaScript nhỏ, chạy nhanh trên điện thoại cấu hình thấp, không phụ thuộc thư viện | Phải tự viết phần hiển thị danh sách; khó mở rộng nếu trang lớn dần thành ứng dụng |
| Hosting | Workers static assets | Pages | Một nền tảng dùng được cho cả trang tĩnh và Worker sau này | Có ít tài liệu và ví dụ hơn Pages |
| Font | Tự host, subset | Google Fonts | Không gọi tới bên thứ ba, đặt CSP chặt được, tải nhanh hơn | Tự quản lý file và giấy phép |
| Nội dung | Tĩnh trong repo | CMS | Không cần thêm hạ tầng, mọi thay đổi được duyệt qua PR | Cán bộ không tự sửa nội dung được |
| Chuyển động | CSS keyframes | GSAP | Không cần thư viện, tự dừng khi tab bị ẩn | Khó làm hiệu ứng nhiều bước phức tạp nếu sau này cần |
| Nút chưa nối chức năng | Hiện thông báo “Chưa hoàn thiện” | Ẩn nút | Người xem thấy đủ các tính năng sắp có | Phải nhớ gỡ từng thông báo khi có link thật |
Lưu ý: bản đang chạy khác tài liệu này ở một điểm. Trang dựng bằng Next.js (xuất ra file tĩnh) thay vì TypeScript thuần với Vite. Phần hosting thì đúng như mô tả: Workers static assets, deploy bằng wrangler deploy.