Tài liệu đọc trong lesson
Một lesson (course_concepts) từ 08.09.2026 có thể mang một hoặc nhiều tài liệu đọc giàu định dạng: ảnh, video nhúng, khung nhấn, khối gập, câu hỏi chấm tại chỗ. Người soạn viết chúng trong admin bằng trình soạn thảo TipTap; người học đọc chúng ở chặng Tìm hiểu của trang bài.
Nó KHÔNG thay ba bài viết của máy sinh
course_concept_articles | course_lesson_docs | |
|---|---|---|
| Ai viết | máy sinh (authoring.ts) | người soạn, trong admin |
| Dạng | chữ thuần (body_vi) | TipTap JSON (doc_json) |
| Lớp kiểm | content-rules, quality-audit, assert văn phong | không có, người soạn tự chịu |
| Số lượng | đúng ba (what / confusion / apply) | 0..n, có thứ tự |
Ba bài viết phải giữ dạng chữ thuần để các lớp kiểm còn đọc được. Tài liệu đọc là thứ khác: bài đọc dài kiểu Substack, dựng bối cảnh cho ba lát cắt ngắn phía sau, nên nó hiện trước ba bài viết trong chặng Tìm hiểu.
Ba chỗ phải sửa cùng lúc khi thêm một loại khối
Đây là bẫy chính của tính năng này, và cả ba lần thiếu đều không báo lỗi gì:
| Chỗ | File | Thiếu thì |
|---|---|---|
| Whitelist ở máy chủ | api.conan.school/src/shared/rich-doc.ts | khối bị bỏ khi lưu, người soạn mất công |
| Trình soạn thảo | admin.conan.school/legacy/admin-course-content/src/components/lesson-doc/nodes.ts | không chèn được khối |
| Cách vẽ | packages/rich-doc/RichDoc.tsx | khối biến mất khi đọc |
Tên node và tên attr phải trùng từng chữ ở cả ba.
An toàn: whitelist ở máy chủ, không phải ở editor
sanitizeDoc() chạy trong đường ghi của mọi endpoint admin. Nó là danh sách cho phép, không bao giờ là danh sách cấm: node lạ bị bỏ, attr không khai bị cắt, href chỉ nhận https: và mailto:, src của ảnh chỉ nhận https:, video chỉ nhận YouTube / Vimeo / Cloudflare Stream và được đổi sang đúng URL nhúng.
Trình soạn thảo không phải hàng rào, nó là JavaScript trong trình duyệt người soạn, và một lượt curl tới PATCH /api/admin/lesson-docs/:id bỏ qua nó hoàn toàn.
Phía đọc, RichDoc.tsx dựng React element, không bao giờ dangerouslySetInnerHTML. Hai lớp, không lớp nào dựng HTML từ chuỗi, đúng chỗ đã hỏng với SVG cover lấy từ D1.
Lớp kiểm
auditCourse không đọc course_lesson_docs. Lớp kiểm của tài liệu đọc là một gác riêng:
npm run check:lesson-docsNó dựng lại các migration bằng SQLite, rồi soi từng tài liệu bằng mười luật đếm được, kèm mười mẫu lỗi phải bị bắt và hai mẫu hợp lệ phải được cho qua. Chi tiết và ba quyết định thiết kế đáng giữ: skill lesson-docs, luật 8.
Nháp và xuất bản
Hai cột, không phải một:
draft_json, bản đang soạn. Mọi lượt lưu (kể cả tự lưu sau 2 giây ngừng gõ) chỉ chạm cột này.doc_json, bản người học đọc. ChỉPOST /:id/publishghi vào đây.
learn-overview chỉ trả tài liệu status='published', và câu SELECT của nó không códraft_json trong danh sách cột. Xuất bản một tài liệu rỗng bị chặn ở API (422): một mục có tiêu đề mà bấm vào không có gì là đúng loại hỏng mà hệ vẫn trông như đang chạy.
Ba trạng thái người soạn phải phân biệt được trên màn hình:
| Nhãn | Nghĩa |
|---|---|
| Đang hiển thị | người học đang đọc đúng bản này |
| Có nháp chưa xuất bản | người học vẫn đang đọc bản cũ |
| Nháp, người học chưa thấy | chưa từng xuất bản, ngoài kia không có gì |
Ảnh
Tải lên qua POST /api/admin/lesson-docs/concepts/:conceptId/assets: nén WebP xuống dưới 400 KB (cùng hàm với media_library.ts), đẩy vào R2 EVENT_MEDIA ở lesson-docs/<conceptId>/<id>.webp, ghi sổ course_lesson_doc_assets, tài liệu chỉ giữ URL.
Không bao giờ nhúng base64 vào doc_json. Một tấm ảnh 2 MB thành ~2,7 MB chữ, nằm cùng hàng D1 với nội dung và đi kèm mọi lượt tải trang học, đúng khuôn "loader trả cả cục" đã làm trang chủ www nặng 1,99 MB.
Sổ asset tồn tại vì R2 không trả lời được câu "ảnh này còn ai dùng không". Không có sổ thì mỗi lần xoá tài liệu là để lại rác vĩnh viễn trong bucket.
Hai đường ảnh
| Ảnh của ai | Đi đâu | Sổ |
|---|---|---|
| Người soạn tải lên trong admin | R2 (lesson-docs/<conceptId>/<id>.webp) | course_lesson_doc_assets |
| Hình của tài liệu do agent soạn | com.conan.school/public/lesson-docs/*.svg trong git | git |
Đường thứ hai tồn tại vì không có đường nào đưa ảnh vào R2 qua CI. Tài liệu do agent soạn sống trong git, nên hình của nó phải sống cùng chỗ, để trên R2 thì chạy lại repo ở môi trường khác là tài liệu mất hình. Đừng dùng đường git cho ảnh của người soạn: nó bắt người ta mở PR để thay một tấm ảnh.
Hình trong git vẽ bằng scripts/build-lesson-doc-figures.mjs: màu đọc thẳng từ theme.css lúc chạy, và không chữ trong hình, nghĩa nằm ở alt, thứ RichDoc.tsx in ra thành caption.
npm run figures:build # vẽ lại sau khi đổi token hoặc đổi hình
npm run check:figures # gác trong job `guards` của CIGác bắt bốn kiểu hỏng, cả bốn đều im lặng: đổi token màu mà quên vẽ lại (SVG nằm trong <img> nên biến CSS của trang không với tới nó), sửa tay file SVG, hình mồ côi còn trong git mà bộ vẽ không sinh ra nữa, và URL trong migration trỏ tới hình không tồn tại.
Khối "Hỏi nhanh" gửi kèm đáp án: và đó là cố ý
quickCheck chấm ngay trong trình duyệt, không ghi điểm, không chặn đường ai. Hai cổng chặn thật, quiz chốt của bài và quiz cuối unit, vẫn chấm ở máy chủ và không bao giờ gửi đáp án đi trước. Đừng chép khuôn của quickCheck sang hai cổng đó.
Đường dẫn
| Việc | Chỗ |
|---|---|
| Soạn | admin.conan.school → Content & Learning → Courses & Docs → Lesson Documents |
| API | /api/admin/lesson-docs/* (miền quyền content) |
| Người học đọc | /learn/lesson/:lessonId, chặng Tìm hiểu |
| Bảng | course_lesson_docs, course_lesson_doc_assets (họ không tiền tố) |
| Migration | api.conan.school/migrations/20260916a_lesson_docs.sql |
Xem thêm: skill lesson-docs, content-ops, known-risks, design-system.