Nhập dữ liệu cũ vào CMS
Giai đoạn 4 chuyển tin tức, thông báo, playbook từ api.conan.school sang api-cms. Trang này mô tả đường nhập: chụp, sinh migration, kiểm, và làm lại khi cần. Hai bộ đã chạy (17.10.2026, theo kế hoạch):
| Collection | PR | Ảnh chụp | Migration | Số bài, danh mục |
|---|---|---|---|---|
news | 3 | import/news/snapshot.json | 0003_import_news.sql | 15, 12 |
announcements | 5 | import/announcements/snapshot.json | 0004_import_announcements.sql | 20, 6 |
Ba bước, ba file
Sơ đồ dưới lấy tin tức làm ví dụ; thông báo đi đúng đường đó với tên announcements.
api.conan.school (route công khai)
│ scripts/import-legacy/export.mjs news chỉ GET, đúng route www đang gọi
▼
import/news/snapshot.json NGUỒN SỰ THẬT, commit vào git
│ scripts/import-legacy/build-sql.mjs news tất định, không gọi mạng
▼
migrations/0003_import_news.sql Workers Builds áp TRƯỚC wrangler deploy
│
▼
D1 conan-cms ── scripts/import-legacy/verify.mjs news đếm + băm lại qua /public/newsĐường dẫn trong bảng này tính từ api-cms.conan.school/.
| File | Vai trò | Ai ghi |
|---|---|---|
import/news/snapshot.json | Phản hồi nguyên văn của GET /api/public/content/news?limit=500 và từng GET /api/public/content/news/{slug}, kèm fetched_at, api_release (/api/system/diagnostics), kết quả kiểm ảnh bìa, và hai băm mỗi bài | export.mjs, không sửa tay |
scripts/import-legacy/news.mjs | Ánh xạ từng trường (một chỗ khai, dùng ở cả ba script và ở test) | người |
migrations/0003_import_news.sql | INSERT danh mục, bài, và một bản lịch sử import mỗi bài | build-sql.mjs, không sửa tay |
import/announcements/snapshot.json, scripts/import-legacy/announcements.mjs, migrations/0004_import_announcements.sql | Ba file tương ứng của thông báo | như trên |
import/announcements/REVIEW.md | Danh sách 20 thông báo trông như dữ liệu mẫu, cho người soạn rà và gỡ trong CMS; bảng màu danh mục sang token | người (file duy nhất ở đây viết tay) |
Ảnh chụp là nguồn sự thật, không phải D1 cũ và không phải migration. Lý do: hai repo chỉ nối với nhau bằng HTTP, và luật "nội dung vào repo trước, D1 sau" cần một file trong git ghi đúng thứ đã nhập. Ảnh chụp đi qua route công khai nên đã qua mọi bộ lọc www đang thấy (đã xuất bản, đúng trường), tức "không mất gì" nghĩa là "không mất gì www đang hiện".
Hai băm mỗi bài
source_sha256: băm của phản hồi cũ nguyên văn. Dùng để biết bảng cũ có ai ghi thêm sau lượt chụp (export.mjs --check).cms_sha256: băm của phần api-cms trả và www render (slug, locale, title, summary, body, body_format, cover_url, category_slug, pinned, expires_at, meta, published_at, updated_at). Tính hai lần theo hai đường độc lập: từ ảnh chụp quamapEntrylúc chụp, và từGET /public/news/:slugsau khi nhập. Khớp nhau nghĩa là bài www sẽ đọc đúng bằng bài đã chụp.
Băm là sha256 của JSON có khoá xếp theo thứ tự chữ (stableStringify ở common.mjs), chạy được cả ở Node lẫn workerd.
Ánh xạ tin tức
| CMS | Nguồn cũ |
|---|---|
id | id cũ (uuid), giữ nguyên |
slug | giữ nguyên (15/15 hợp luật slug của CMS) |
locale | vi (không bài nào có title_en, excerpt_en, content_en; có thì mapEntry dừng) |
title, summary, body | title, excerpt, content nguyên văn |
body_format | markdown |
cover_url | NULL khi ảnh bìa không tải được lúc chụp (15/15 trả 404, quyết định của chủ repo 17.10.2026) |
category_slug | slug của danh mục nhúng news_categories |
meta | { "author_name": "Conan School" } (giữ cho đủ, www không hiện) |
legacy_ref | legacy_news_articles:<id> |
published_at, created_at, updated_at | giữ nguyên thời điểm, chuẩn hoá +00:00 và micro giây thành toISOString() (.111Z) |
status | published |
Danh mục: 3 gốc (có trong danh sách) cộng 9 con (chỉ có ở danh mục nhúng của từng bài), label_vi = name_vi, label_en = name_en, parent_slug theo parent_id, sort_order giữ, meta.icon = emoji cũ (giữ ở ngày chuyển vì trang tin hôm nay hiện nó; đổi sang icon ở PR 10).
Ánh xạ thông báo
Chỉ 20 bài www hiện: Conan School (x-school-id), scope='school'. Route /api/public/announcements của api.conan.school là route đa trường (conan.media, inside6, Ran cùng đọc), nên bộ nhập chỉ đọc nó và không có gì ở repo này đụng tới nó. 120 bài của ba trường khác và 20 thông báo lớp của Conan School ở lại.
export.mjs announcements đi hết các trang của GET /api/public/announcements?limit=50&page=n (so tổng với pagination.total), rồi từng GET /api/public/announcements/{slug} (mọi trường phải trùng bản ở danh sách), và GET /api/public/announcements/categories (danh mục nhúng ở từng bài phải trùng bản ở đây).
| CMS | Nguồn cũ |
|---|---|
id, slug, title | giữ nguyên (20/20 slug hợp luật) |
locale | vi (title_en, content_en rỗng cả 20; có thì mapEntry dừng) |
body | content nguyên văn (markdown, có emoji trong câu) |
summary | văn bản thuần của content, sinh bằng đúng stripMarkdown của www.conan.school/app/routes/announcements.tsx (chép trong announcements.mjs). Thẻ danh sách của www cắt 150 ký tự từ chính chuỗi đó, nên danh sách của api-cms không cần mang thân bài |
category_slug | slug của danh mục nhúng (route cũ JOIN danh mục: bài không danh mục không bao giờ hiện) |
pinned | is_pinned |
expires_at | expires_at (rỗng cả 20), chuẩn hoá toISOString() |
meta | { "priority": "normal" | "important" | "urgent" } |
legacy_ref | announcements:<id> (bảng không tiền tố, SQL thô) |
published_at, created_at, updated_at | giữ thời điểm, chuẩn hoá toISOString() |
cover_url | NULL (thông báo không có ảnh) |
Thứ tự trong migration là thứ tự route cũ trả: ghim trước, rồi mới nhất trước. api-cms xếp pinned DESC, published_at DESC, id DESC, ra cùng thứ tự (không hai bài nào cùng published_at).
Danh mục: 6 dòng, label_vi = name, label_en rỗng (thông báo chỉ có nhãn tiếng Việt), sort_order = vị trí trong phản hồi /categories (route cũ xếp theo sort_order nhưng không trả cột đó), meta.icon = tên icon cũ (chữ, không phải emoji). Route cũ không trả thời điểm của danh mục, nên created_at/updated_at để mặc định (lúc migration áp).
Màu danh mục là tên token, không phải mã màu. Bảng cũ lưu mã màu và www tô bằng style nội tuyến. CMS lưu meta.color_token, tên một token của packages/design-system/theme.css, qua bảng COLOR_TOKEN_BY_LEGACY_COLOR ở announcements.mjs (khoá là mã màu nguồn, giá trị là tên token; mã lạ thì dừng). build-sql.mjs kiểm mọi tên có trong theme.css (chạy trong npm test), vì một tên gõ sai là một danh mục tô bằng biến CSS không tồn tại mà không báo gì. Bốn trên sáu token cùng giá trị với màu cũ; hai (cam, vàng) lấy token gần nhất, liệt kê ở import/announcements/REVIEW.md. Ngày chuyển www vẫn tô đúng màu cũ qua bảng cầu nối tạm của nó; PR 10 bỏ cầu nối và tô bằng token.
Dữ liệu mẫu. Cả 20 bài được chèn bằng một câu lệnh ngày 10.07.2026 và nêu tên người, đối tác, số tiền. Quyết định của chủ repo: nhập nguyên trạng, đã xuất bản, kèm import/announcements/REVIEW.md để người soạn rà và gỡ trong CMS. Gỡ cả 20 thì /announcements của www trả 503 (có chủ ý, www đọc CMS).
Kiểm
npm testcủa api-cms chạynpm run check:import(build-sql.mjs <collection> --checkcho từng collection đã nhập: migration đã commit phải trùng từng byte với bản sinh lại từ ảnh chụp) rồi vitest.test/import-news.test.tschạy trên D1 cục bộ đã áp mọi migration, kể cả0003: đếm 15/12/15, so thứ tự danh sách, băm lại từng bài và danh mục từ route công khai, kiểm mọi hàng qua lược đồ GHI của api-cms (strict), và chạy lại một câu nhập để thấy nó hỏng vì trùng.test/import-announcements.test.tslàm như thế cho0004(20/6/20), cộng: thứ tự ghim trước, hai trang 12 bài qua con trỏ (đúng hai trang www hiện), mỗi bộ lọc danh mục ra đúng tập bài của route cũ,summarybằngstripMarkdowncủa thân, màu danh mục là tên token (không có#nào trongmeta), và0004xếp sau0003.- Lệnh deploy của Workers Builds chạy cùng
npm testtrước khi áp migration, nên migration lệch ảnh chụp không bao giờ lênconan-cms. - Sau deploy:
node scripts/import-legacy/verify.mjs newsvàverify.mjs announcements(mặc địnhhttps://api-cms.conan.school). Trước khi migration áp, nó thoát 1 với "số bài: api-cms 0": không có bài là lỗi, không phải "chưa có gì".
Làm lại
| Muốn | Làm |
|---|---|
| Biết bảng cũ có ai ghi sau lượt chụp | node scripts/import-legacy/export.mjs <collection> --check (không ghi gì; lệch thì thoát 1 và liệt kê +, -, ~) |
| Chụp lại TRƯỚC khi migration nhập merge | export.mjs <collection>, rồi build-sql.mjs <collection>, rồi npm test; commit cả ba file |
| Nhập thêm SAU khi migration nhập đã áp | Không sửa migration hay ảnh chụp đã nhập. Ảnh chụp mới (file mới) và migration số mới; build-sql --check sẽ đỏ nếu ai sửa file cũ |
| Thử ở máy | npx wrangler d1 migrations apply conan-cms --local --persist-to <thư mục tạm>, npx wrangler dev --local --persist-to <cùng thư mục>, rồi verify.mjs <collection> --base http://localhost:8787 |
Không bao giờ wrangler d1 execute --remote để chèn: chèn từ máy là không ai biết nội dung đó từ đâu ra.
Vì sao INSERT thuần
Không có ON CONFLICT DO NOTHING hay OR IGNORE. CMS đang rỗng ở collection này; trùng (khoá chính id, (collection, slug, locale), legacy_ref, khoá chính danh mục) chỉ xảy ra khi ai đó đã tạo bài cùng slug trong CMS trước lượt nhập. Khi đó migration phải hỏng và chặn luôn deploy (Workers Builds áp migration trước wrangler deploy), thay vì lặng lẽ bỏ một bài mà mọi con số vẫn trông như đúng. Gỡ: xoá bài tạo tay trong CMS rồi chạy lại build.
Khác biệt cố ý so với www hôm nay
Ghi ở mục 4.5 của bản thiết kế, và là danh sách duy nhất phép so của www (PR 4, PR 6) cho qua:
- Ảnh bìa tin: 15 ảnh vỡ thành không có ảnh (thẻ danh sách hiện ô chữ dự phòng ngay từ HTML máy chủ, trang chi tiết bớt một ảnh vỡ,
og:imagerơi về ảnh mạng xã hội của site). - Chuỗi thời điểm chuẩn hoá (cùng thời điểm, khác cách viết).
Thông báo không thêm khác biệt cố ý nào ngoài mục 2: màu danh mục giữ nguyên qua cầu nối, summary ra đúng chữ của thẻ cũ.