www đọc CMS
Từ giai đoạn 4, www đọc từng collection biên tập từ một trong hai nguồn: api.conan.school (cũ) hoặc api-cms.conan.school (CMS). Tin tức là collection đầu tiên chuyển (PR 4 của kế hoạch, 17.10.2026), sau khi nhập 15 bài vào conan-cms (PR 3, trang dev/import.md "Nhập dữ liệu cũ"). Thông báo theo sau (PR 6), sau khi nhập 20 bài của Conan School (PR 5): xem Thông báo.
Một hằng số, một chữ để quay lui
www.conan.school/app/lib/editorial-source.ts:
export const EDITORIAL_SOURCE = { news: "cms", announcements: "cms", playbooks: "legacy" }Danh sách (/news), trang chi tiết (/news/:slug) và sitemap (sitemap-library.xml) cùng đọc hằng số này qua app/lib/news-source.ts, nên không có lúc trang đọc nguồn này mà sitemap khai URL của nguồn kia. Quay lui = đổi "cms" thành "legacy" rồi push; nhánh cũ vẫn nằm nguyên trong news-source.ts tới PR 10 (gỡ nhánh cũ cùng file hằng số).
CMS_API_URL khai một chỗ ở app/env.public.ts.
Hai nguồn, một hình dạng
news-source.ts trả cho JSX đúng hình dạng cũ (title/title_en, excerpt/excerpt_en, thumbnail_url, news_categories { icon, name_vi, name_en }...), nên JSX của hai route gần như không đổi:
| www cần | Nguồn cũ | api-cms |
|---|---|---|
Bản /en | pick(lang, vi, en) trên cột *_en | locale=en&fallback=vi; mục có locale: 'en' điền vào *_en, mục rơi về vi để *_en null, nên pick ra cùng kết quả |
viOnly (canonical của /en về bản vi) | !content_en && !title_en | !available_locales.includes('en') |
| Ảnh bìa | thumbnail_url | cover_url |
| Danh mục | news_categories | category nhúng (label_vi, label_en, meta.icon) |
| Thân HTML | looksLikeHtml | body_format === 'html' hoặc looksLikeHtml (giữ cả hai) |
<lastmod> sitemap | published_at | updated_at (lần sửa cuối trong CMS) |
Phản hồi parse bằng hợp đồng zod công khai của packages/cms-contract (publicContracts, chế độ strip) qua fetchSchema ở app/lib/contract.ts, cùng đường với fetchContract: lỗi mạng, 5xx, JSON hỏng, lệch hợp đồng là 503 kèm log contract_mismatch; 404 của trang chi tiết là 404 thật.
api-cms trả 0 tin thì 503, không hiện "chưa có tin" (requireItems, log editorial_empty): nguồn này chỉ bật sau khi dữ liệu nhập đã lên, nên 0 bài nghĩa là PR đổi nguồn lên trước dữ liệu. Giao diện danh sách rỗng là thông điệp của một trang khoẻ, đúng loại hỏng luật SEO cấm.
packages/cms-contract/* phải có trong watch paths của Pages conan-www (thêm tay ở dashboard, Deploy), và scripts/ci/select-apps.mjs build lại www khi gói đó đổi.
Phép so trước và sau
scripts/compare-editorial.mjs lấy HTML của www production (nguồn cũ) và của bản dựng mới, rồi so từng URL:
cd www.conan.school && npm run build && cd ..
node scripts/compare-editorial.mjs news # bản dựng đọc api-cms production
node scripts/compare-editorial.mjs news --cms-api http://localhost:8787 # bản dựng đọc api-cms ở máy
node scripts/compare-editorial.mjs news --new https://www.conan.school # sau deploy: chạy lại với production- Bản dựng chạy ngay trong tiến trình (
createRequestHandlercủa React Router trênwww.conan.school/build/server), không cần server hay Pages preview (preview của repo này đòi đăng nhập Access).--cms-apiđổi host api-cms cho riêng tiến trình đó, để so được trước khi dữ liệu lên production:wrangler d1 migrations apply conan-cms --local --persist-to <dir>rồiwrangler dev --local --persist-to <dir>ởapi-cms.conan.schoolcủa nhánh nhập. - URL:
/news,/en/news, mọi slug x {vi,/en},sitemap.xml,sitemap-pages.xml,sitemap-library.xml. Tin tức không có trang lọc danh mục ở www (route cũ nhận?category=nhưng www chưa bao giờ truyền). - Mỗi URL so riêng: mã trạng thái, thân
<main>,<title>+ mọi<meta>,<link rel=canonical|alternate>, từng khối JSON-LD. Sitemap so từng<url>. Chuẩn hoá: bỏ chú thích, khoảng trắng giữa thẻ,<script>trong<main>(dữ liệu loader tuần tự hoá), tên asset băm không nằm trong phần nào được so. - Tập bài ở hai nguồn phải trùng nhau; thiếu hay thừa một slug là lệch.
- api-cms chưa có bài nào: thoát 2 và nói rõ là dữ liệu chưa nhập, không bao giờ ra "khớp".
- Chạy với
TZ=UTC(script tự đặt): Workers chạy ở UTC, còn Node đọc múi giờ của máy, và một chuỗi ngày không kèm múi giờ lùi một ngày ở Asia/Ho_Chi_Minh.
Khác biệt cố ý
Danh sách DUY NHẤT phép so cho qua (bản thiết kế mục 4.5). Mỗi luật đếm số chỗ nó đã dùng, và báo cáo in con số đó.
| Luật | Cho qua | Vì sao |
|---|---|---|
anh-bia-vo | <img> của ảnh bìa tin mà phép so kiểm thật là 404 ở www; khung ảnh của trang chi tiết; og:image, og:image:secure_url, twitter:image, image của JSON-LD rơi về ảnh mạng xã hội của site | 15/15 ảnh bìa tin vỡ; nhập cover_url = NULL (quyết định của chủ repo). Thẻ danh sách hiện ô chữ dự phòng ngay từ HTML máy chủ thay vì sau onError |
chuoi-thoi-diem | Hai chuỗi ISO cùng một thời điểm (+00:00 và .111073 thành .111Z) | api-cms lưu toISOString(); lộ ra ở <time dateTime>, JSON-LD vốn đã chuẩn hoá |
lastmod-tin | <lastmod> của URL /news/* | Đổi từ published_at sang updated_at; phép so kiểm thêm là giá trị mới bằng đúng ngày updated_at ở api-cms |
Mọi lệch khác làm phép so thoát 1. Phép so được thử ngược: sửa một chữ trong thân một bài và nhãn một danh mục ở D1 cục bộ thì nó báo đúng bốn trang lệch.
Ở trang tin, một thứ giữ nguyên dù trái luật ở ngày chuyển, để PR chuyển chỉ mang một loại thay đổi: emoji làm icon danh mục (meta.icon, hiện ở trang chi tiết). Sửa ở PR 10, cùng hai thứ của thông báo và playbook.
Sau khi PR đổi nguồn merge
Chỉ merge khi dữ liệu nhập đã lên: node api-cms.conan.school/scripts/import-legacy/verify.mjs news khớp, và node scripts/compare-editorial.mjs news (bản dựng của nhánh đọc api-cms PRODUCTION) ra KHỚP.
Pages
conan-wwwbuild xong (dashboard, Deployments, commit của lượt merge). Cầu nối giữ HTML tới 60 giây.Chạy lại phép so với production làm bên mới, và một bản dựng ở nguồn cũ làm bên cũ:
bashgit worktree add --detach ../www-legacy origin/main # trong ../www-legacy: đổi news thành "legacy" ở www.conan.school/app/lib/editorial-source.ts (cd ../www-legacy/www.conan.school && npm ci && npm run build) node scripts/compare-editorial.mjs news --old local --old-build ../www-legacy/www.conan.school --new https://www.conan.schoolPhải ra KHỚP với đúng ba luật khác biệt cố ý.
Mở
/news, một bài, và/en/news/<bài>qua Chrome; xem nguồn trang: không còn<img>trỏ/news/thumbnails/.curl -s https://www.conan.school/sitemap-library.xml | grep -c '/news/'= 15.
Hỏng thì quay lui: "cms" thành "legacy" ở editorial-source.ts, push, Pages build lại (vài phút).
Thông báo
app/lib/announcements-source.ts, cùng khuôn với news-source.ts: /announcements (cả ?page=, ?category=), /announcements/:slug (cả khối bài liên quan) và phần thông báo của sitemap-community.xml cùng đọc EDITORIAL_SOURCE.announcements. Route /api/public/announcements của api.conan.school là route đa trường (conan.media, inside6, Ran cùng đọc): www thôi đọc nó, không ai sửa nó.
| www cần | Nguồn cũ | api-cms |
|---|---|---|
| Danh sách 12 bài/trang | ?page=&limit=12, API đếm total | lấy hết một lượt (limit=500, hôm nay 20 bài), tự cắt trang và tính total_pages như API cũ; còn next_cursor ở trần 500 thì 503, không lặng lẽ thiếu bài |
| Lọc danh mục | ?category= (một slug hay nhiều slug cách dấu phẩy; slug lạ ra rỗng) | lọc ngay trên danh sách đã lấy, đúng ngữ nghĩa đó |
| Thứ tự | is_pinned DESC, published_at DESC | pinned DESC, published_at DESC, id DESC |
| Chữ xem trước ở thẻ | 150 ký tự đầu của stripMarkdown(content) | 150 ký tự đầu của stripMarkdown(summary); summary lúc nhập chính là stripMarkdown(content), và hàm này lặp lại không đổi gì (đã kiểm trên 20 bài) |
| Bài liên quan | ?category=<của bài>&limit=4, bỏ bài đang xem, giữ 3; thẻ hiện 100 ký tự đầu của thân bài | cùng truy vấn ở api-cms, rồi đọc thân từng bài (tối đa 3 lượt) vì danh sách không mang thân |
| Màu danh mục | mã màu category.color, tô bằng style nội tuyến | meta.color_token (tên token), đổi lại đúng mã cũ qua bảng cầu nối tạm LEGACY_COLOR_BY_TOKEN; token chưa có ở bảng thì huy hiệu không tô màu |
viOnly | !title_en && !content_en | !available_locales.includes('en') |
<lastmod> sitemap | published_at | published_at (giữ nguyên, không có luật khác biệt cố ý cho thông báo) |
Loader giờ chỉ gửi trường thẻ đọc: danh sách thôi gửi thân của 12 bài (chữ xem trước cắt ở loader), bài liên quan thôi gửi thân của 3 bài.
Hai chỗ nhánh cms chặt hơn nhánh cũ, có chủ ý: danh mục hỏng thì 503 (nhánh cũ ẩn bộ lọc), khối liên quan hỏng thì 503 (nhánh cũ nuốt lỗi). Cả hai đọc cùng Worker, cùng D1 với danh sách, nên "danh mục hỏng mà danh sách khoẻ" gần như không xảy ra, và một bộ lọc biến mất là đúng loại hỏng trông như khoẻ. api-cms trả 0 thông báo thì /announcements và sitemap-community.xml trả 503 (log editorial_empty); trang chi tiết của slug không có ở api-cms là 404. Người soạn gỡ cả 20 bài mẫu trong CMS cũng ra 503: viết ít nhất một bài thật trước.
Cầu nối màu là trái luật màu có ghi tên, để ngày chuyển không đổi gì nhìn thấy được: JSX ghép mã màu với độ mờ dạng hex (${color}18) nên cần lại đúng mã cũ. PR 10 bỏ bảng và style nội tuyến, tô bằng lớp token; bốn trên sáu danh mục giữ nguyên màu, hai (cam, vàng) đổi nhẹ (api-cms.conan.school/import/announcements/REVIEW.md). Không thêm dòng vào bảng cầu nối.
Phép so của thông báo
node scripts/compare-editorial.mjs announcements --cms-api http://localhost:8787 # trước khi PR 5 deploy
node scripts/compare-editorial.mjs announcements # sau khi PR 5 deployURL: /announcements và /en/announcements, mọi trang ?page=2.. của cả hai ngôn ngữ cộng một trang quá tổng, mọi slug x {vi, /en}, mỗi danh mục x {vi, /en} (?category=), một slug danh mục lạ, hai slug ghép dấu phẩy; sitemap.xml, sitemap-pages.xml, sitemap-community.xml (cả các họ khác trong sitemap con đó, đọc api.conan.school ở cả hai bên). Chỉ luật chuoi-thoi-diem được dùng; <lastmod> của /announcements/* phải giống hệt. Thử ngược: đổi một mã trong bảng cầu nối thì phép so báo 15 trang lệch.
Sau khi PR 6 merge: như tin tức ở mục dưới, với announcements thay cho news (bản dựng nguồn cũ đặt announcements: "legacy"), và curl -s https://www.conan.school/sitemap-community.xml | grep -c '/announcements/' = 20.