Skip to content

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):

CollectionPRẢnh chụpMigrationSố bài, danh mục
news3import/news/snapshot.json0003_import_news.sql15, 12
announcements5import/announcements/snapshot.json0004_import_announcements.sql20, 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/.

FileVai tròAi ghi
import/news/snapshot.jsonPhả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àiexport.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.sqlINSERT danh mục, bài, và một bản lịch sử import mỗi bàibuild-sql.mjs, không sửa tay
import/announcements/snapshot.json, scripts/import-legacy/announcements.mjs, migrations/0004_import_announcements.sqlBa file tương ứng của thông báonhư trên
import/announcements/REVIEW.mdDanh 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 tokenngườ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 qua mapEntry lúc chụp, và từ GET /public/news/:slug sau 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 ​

CMSNguồn cũ
idid cũ (uuid), giữ nguyên
sluggiữ nguyên (15/15 hợp luật slug của CMS)
localevi (không bài nào có title_en, excerpt_en, content_en; có thì mapEntry dừng)
title, summary, bodytitle, excerpt, content nguyên văn
body_formatmarkdown
cover_urlNULL 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_slugslug của danh mục nhúng news_categories
meta{ "author_name": "Conan School" } (giữ cho đủ, www không hiện)
legacy_reflegacy_news_articles:<id>
published_at, created_at, updated_atgiữ nguyên thời điểm, chuẩn hoá +00:00 và micro giây thành toISOString() (.111Z)
statuspublished

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).

CMSNguồn cũ
id, slug, titlegiữ nguyên (20/20 slug hợp luật)
localevi (title_en, content_en rỗng cả 20; có thì mapEntry dừng)
bodycontent nguyên văn (markdown, có emoji trong câu)
summaryvă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_slugslug 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)
pinnedis_pinned
expires_atexpires_at (rỗng cả 20), chuẩn hoá toISOString()
meta{ "priority": "normal" | "important" | "urgent" }
legacy_refannouncements:<id> (bảng không tiền tố, SQL thô)
published_at, created_at, updated_atgiữ thời điểm, chuẩn hoá toISOString()
cover_urlNULL (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 test của api-cms chạy npm run check:import (build-sql.mjs <collection> --check cho 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.ts chạ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.ts làm như thế cho 0004 (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ũ, summary bằng stripMarkdown của thân, màu danh mục là tên token (không có # nào trong meta), và 0004 xếp sau 0003.
  • Lệnh deploy của Workers Builds chạy cùng npm test trước khi áp migration, nên migration lệch ảnh chụp không bao giờ lên conan-cms.
  • Sau deploy: node scripts/import-legacy/verify.mjs news và verify.mjs announcements (mặc định https://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ốnLàm
Biết bảng cũ có ai ghi sau lượt chụpnode 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 mergeexport.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 đã ápKhô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áynpx 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:

  1. Ả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:image rơi về ảnh mạng xã hội của site).
  2. 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ũ.

Tài liệu nội bộ của Conan CMS.