Bỏ qua điều hướng, tới nội dung chính
Thanh niên Bắc NinhTỉnh đoàn Bắc Ninh
← Về trang chủTài liệu kỹ thuật

Kiến trúc hệ thống

Trang chủ là một trang tĩnh, giới thiệu nền tảng “Thanh niên Bắc Ninh” và dẫn người dùng sang Zalo Mini App. Tài liệu này mô tả kiến trúc của trang chủ và cách nó kết nối với các phần còn lại của hệ thống.

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
F2Lọc 4 tin việc làm mẫu theo Tất cả / Toàn thời gian / Bán thời gian
F3Phầ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
F4Nú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
F5Thanh thao tác cố định ở đáy chỉ hiện khi màn hình rộng từ 760px trở xuống
F6Cô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ínhMục tiêuLý do
Tốc độLCP < 2.5s, CLS < 0.1 trên điện thoại tầm trung, mạng 4GNgười dùng chủ yếu mở trang từ Zalo trên điện thoại
Dung lượngJavaScript 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à LatinTrang tĩnh, chỉ có ba tương tác nhỏ
Phụ thuộc lúc chạyKhô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àngTrang 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ậpKhông cần backend
Trình duyệtSafari iOS, Chrome Android, webview trong Zalo, trình duyệt desktop hiện hànhKê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útTrang giới thiệu, ít khi thay đổi

Ràng buộc

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 = ./dist

Mã đượ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"
     └───────────────────────┘◀──┘

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ìnhMáy bật giảm chuyển độngKết quả
khôngcókhôngchạy
cóbất kỳbất kỳdừng
bất kỳkhôngbấ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ầnKhi nào cóCách nối dự kiến
Link Mini AppSau 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ã QRCùng lúc với linkẢnh SVG tĩnh, tạo ra khi build
Email, địa chỉ, điện thoạiTỉnh đoàn cung cấpSửa index.html
Tin hoạt độngKhi có nguồn tinXem §9
Việc làm thậtKhi backend có API công khaiXem §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ênHeaderLý do
/assets/*public, max-age=31536000, immutableMỗi lần đổi nội dung, tên file cũng đổi theo
/fonts/*như trênFont không đổi
/index.htmlpublic, max-age=0, must-revalidateNgườ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=()

8. Môi trường, CI/CD và vận hành

Môi trườngCách tạoDùng cho
Localnpm run devPhát triển và chạy test
PreviewMỗ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
ProductionMerge vào main → wrangler deployTrang 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àoVấn đềHướng xử lý
Tin hoạt động cập nhật hằng tuầnSử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ậtDữ liệu thay đổi liên tục, cần luôn mớiWorker script gọi API công khai của backend, lưu cache 5–15 phút
Nhiều trangMộ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ơnMeta, Open Graph, dữ liệu cấu trúcThê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ệtTá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ụcChọnThay vìLợi íchHạn chế
FrameworkTypeScript thuầnReactFile JavaScript nhỏ, chạy nhanh trên điện thoại cấu hình thấp, không phụ thuộc thư việnPhả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
HostingWorkers static assetsPagesMột nền tảng dùng được cho cả trang tĩnh và Worker sau nàyCó ít tài liệu và ví dụ hơn Pages
FontTự host, subsetGoogle FontsKhông gọi tới bên thứ ba, đặt CSP chặt được, tải nhanh hơnTự quản lý file và giấy phép
Nội dungTĩnh trong repoCMSKhông cần thêm hạ tầng, mọi thay đổi được duyệt qua PRCán bộ không tự sửa nội dung được
Chuyển độngCSS keyframesGSAPKhông cần thư viện, tự dừng khi tab bị ẩnKhó 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ăngHiện thông báo “Chưa hoàn thiện”Ẩn nútNgườ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.