Trình soạn thảo Markdown (Milkdown) trong KDIGI CMS
Cập nhật lần cuối:

KDIGI CMS soạn nội dung bằng Markdown thông qua Milkdown: bạn viết trong giao diện trực quan, hệ thống lưu CommonMark kèm GFM, và REST trả về HTML đã làm sạch ở đúng khóa gốc. Trình soạn thảo có slash menu, chọn liên kết nội bộ bằng @, dán từ Google Docs hoặc Word, shortcode và tìm kiếm thay thế.
Tóm tắt nhanh
- Nguồn sự thật là Markdown (trường
contentMd), không phải HTML hay JSON của trình soạn thảo.- Slash menu
/: tiêu đề 1–6, danh sách, to-do, khối mã, trích dẫn, đường kẻ, liên kết, bảng, ảnh và shortcode.@chọn liên kết nội bộ từ bài viết, trang, sản phẩm và landing cùng site.- Dán từ Docs/Word được chuyển sang GFM; ảnh nhúng tự tải lên thư viện media.
- HTML đầu ra được làm sạch bằng
sanitize-html; script và iframe không bao giờ lọt ra.
Markdown là gì?
Markdown là ngôn ngữ đánh dấu nhẹ: bạn viết văn bản thuần với ký hiệu đơn giản (## cho tiêu đề, **đậm**, - cho danh sách, [chữ](url) cho liên kết) rồi công cụ chuyển thành HTML. KDIGI CMS dùng CommonMark kèm phần mở rộng GFM (bảng, danh sách việc cần làm, gạch ngang chữ), nên một bài viết là tệp văn bản đọc được ở mọi nơi.
Vì sao KDIGI CMS chọn Markdown?
Markdown là định dạng văn bản thuần, dễ so sánh phiên bản, dễ di chuyển và không bị khóa vào một trình soạn thảo. Với CMS headless, đây cũng là lợi thế: frontend nhận HTML sạch ở khóa quen thuộc, còn nếu cần bạn vẫn có thể lấy Markdown gốc để render theo cách riêng. Mô hình lưu trữ nằm trong hàm markdownHtmlField() (src/fields/markdown-html-field.ts), trả về hai trường cho mỗi nội dung:
| Trường | Kiểu | Vai trò |
|---|---|---|
{name}Md (ví dụ contentMd) |
textarea, hiển thị trong admin |
Nguồn sự thật, CommonMark + GFM mà biên tập viên viết |
{name} (ví dụ content) |
richText (Lexical), ẩn khỏi admin |
Lưu trữ cũ; hook afterRead của nó là nơi biến {name}Md thành HTML cho REST |
Hiện nay mô hình này áp dụng cho posts, pages, products (content và notice), taxonomies, shortcodes và khối content của landings (bodyMd). Câu trả lời FAQ của collection faqs là một textarea Markdown đơn, chuyển HTML bằng cùng đường ống.
Giao diện soạn thảo
Trình soạn thảo là Milkdown (lõi Crepe) và được tải bằng next/dynamic với ssr: false vì cần API của trình duyệt. Trường chỉ lưu Markdown thuần, thành phần giao diện không bao giờ sinh HTML. Các tính năng sẵn có của Crepe như BlockEdit, ImageBlock, Latex và Toolbar bị tắt, được thay bằng thành phần KDIGI: slash menu, block handle, thanh công cụ nổi khi chọn văn bản và hộp thoại chọn media.
Ngoài ra còn có thanh phác thảo (outline) bên cạnh, tìm và thay thế (Ctrl/Cmd+F, Ctrl/Cmd+Shift+H) và bảng trợ giúp phím tắt (Ctrl/Cmd+/) liệt kê mọi phím tắt cùng quy tắc nhập Markdown.

Slash menu
Gõ / để mở menu lệnh. Menu chia ba nhóm:
| Nhóm | Lệnh |
|---|---|
| Cơ bản | Văn bản, tiêu đề 1–6, danh sách gạch đầu dòng, danh sách đánh số, to-do, khối mã, trích dẫn, đường kẻ |
| Phổ biến | Liên kết, bảng, ảnh (mở hộp thoại media) |
| Snippet | Các shortcode của site; chỉ hiện khi site có ít nhất một. Gõ /snippet hoặc /shortcode để liệt kê |
Danh sách Snippet được tải một lần mỗi lần mở trình soạn thảo từ /api/shortcodes (tối đa 100, sắp theo tiêu đề), chỉ gồm shortcode của đúng site đang sửa. Chọn một shortcode sẽ chèn [sc name="..."/].

Chọn liên kết nội bộ bằng @
Gõ @ ở đầu một từ (không phải trong địa chỉ email) để tìm trong posts, pages, products và landings của cùng site theo tiêu đề hoặc slug, không phân biệt dấu (debounce 150 ms, 8 kết quả mỗi collection). Nhấn Enter để chèn liên kết Markdown gồm tiêu đề và đường dẫn tương đối của tài liệu; Escape đóng menu và giữ lại chuỗi @truy-vấn đã gõ.
- Đường dẫn là đường dẫn tương đối của site, lấy từ hàm
publicPathFor(): sản phẩm thành/product/{slug}, còn bài viết, trang và landing thành/{slug}. Nếu frontend của bạn dùng route khác, chỉ cần đổi hàm đó. - Mặc định chỉ liệt kê tài liệu đang công khai (
status = publishedvàpublishedAttrống hoặc đã qua). Công tắc bản nháp/đã hẹn giờ cho phép chọn cả phần còn lại, nhưng khi chèn sẽ hỏi xác nhận vì tiêu đề và slug sẽ lộ khi trang lên sóng. - Tìm kiếm dùng phiên đăng nhập của chính bạn nên quy tắc truy cập collection vẫn được áp dụng.
Liên kết nội bộ là yếu tố SEO quan trọng; xem thêm SEO và JSON-LD, nơi bộ chấm điểm kiểm tra tối thiểu một liên kết nội bộ Markdown.
Dán nội dung và ảnh
- Ảnh dán hoặc kéo thả được tải lên
/api/mediavà chèn dạng. Với Payload 3, các trườngalt(bắt buộc),foldervàtenantphải nằm trong phần JSON_payloadcủa multipart, nếu không request bị trả về 400 — client upload tự viết của bạn cần làm giống vậy. - Dán giàu định dạng (Google Docs, Word, trang web): HTML trong clipboard được chuyển sang GFM bằng
src/lib/html-to-gfm.ts, ảnh nhúng dạngdata:được tải lên, và toàn bộ được chèn như một bước hoàn tác. Văn bản thuần và Markdown được dán nguyên bản. - Lệnh
/imagemở hộp thoại media với ba tab: Tải lên, Thư viện và URL, giới hạn trong site của tài liệu. Tab URL tải lại ảnh từ xa về kho lưu trữ quaPOST /api/media/from-urlcó chống SSRF.
Chi tiết về thư viện xem thư viện media.
Shortcode: nội dung dùng lại theo site
Shortcode là đoạn Markdown (bảng giá, chính sách, đoạn quảng bá) lưu trong collection shortcodes và nhúng vào bất kỳ trường Markdown nào:
[sc name="pet-policy" /]
[sc name="pet-policy"][/sc]
Quy tắc mở rộng, theo src/lib/shortcodes/expand-shortcodes.ts:
| Quy tắc | Hành vi |
|---|---|
| Kích hoạt | Chỉ khi Markdown chứa chuỗi [sc |
| Phạm vi | Chỉ shortcode của cùng site với tài liệu đang đọc |
| Ngôn ngữ | Đọc theo req.locale |
| Lồng nhau | Tối đa 3 cấp; sâu hơn cho ra chuỗi rỗng |
| Vòng lặp | Tên đã có trên đường mở rộng hiện tại cho ra chuỗi rỗng |
| Tên không có hoặc rỗng | Cho ra chuỗi rỗng |
| Kết quả | Được bao bằng dòng trống để bảng và danh sách parse đúng |
name khớp [a-z0-9-]+, được chuẩn hóa khi lưu (chữ thường, bỏ dấu, ký tự khác thành -) và là duy nhất trong từng site. Shortcode đọc công khai được, nên đừng lưu dữ liệu riêng tư trong đó. Vì trình soạn thảo ghi [ thành \[, thẻ gõ trong editor được lưu ở dạng escape và vẫn được nhận diện.
API trả gì cho frontend?
Hook afterRead chỉ chạy với REST, và có thứ tự ưu tiên rõ ràng:
Trạng thái {name}Md |
Giá trị trả về cho {name} |
|---|---|
| Chuỗi không rỗng | Mở rộng shortcode (nếu có [sc), rồi markdownToHtml(), rồi chèn kích thước ảnh: một chuỗi HTML |
'' (rỗng) |
null — Markdown là nguồn quyết định và nói "không có nội dung" |
null/undefined |
Dự phòng cho dữ liệu cũ: chuyển giá trị Lexical sang HTML |
Việc phân biệt '' với null là chủ ý: một tài liệu đã lưu từ khi chuyển sang Markdown vẫn phải xóa được nội dung của nó.
| Kênh đọc | content trả về |
|---|---|
REST (/api/posts?...) |
Chuỗi HTML đã render và làm sạch |
| GraphQL | JSON Lexical thô, không render, không mở rộng shortcode |
| Local API trong hook, script, endpoint | JSON Lexical thô |
Nếu endpoint hay script của bạn cần HTML, hãy đọc {name}Md rồi tự gọi expandShortcodesInMarkdown() và markdownToHtml(). Xem Headless API.
Làm sạch HTML
markdownToHtml() render bằng markdown-it (với html: true, breaks: false, linkify: true) rồi cho qua sanitize-html, đây là ranh giới bảo mật. Thẻ cho phép gồm h1–h6, p, br, hr, strong, em, del, s, code, pre, blockquote, ul, ol, li, a, img, các thẻ bảng, div, span, figure và figcaption. <script>, <style>, <iframe>, thuộc tính style và trình xử lý sự kiện không bao giờ ra tới đầu ra.
Kích thước ảnh, không còn giật bố cục
Ảnh Markdown không có kích thước nên ảnh tải lười dễ co về 0×0 cho đến khi tải xong. Sau khi render, injectImageDimensions() thêm width và height vào mọi <img> chưa có, khi src khớp url hoặc externalUrl của một tài liệu media cùng site. Bản đồ URL → kích thước được tải theo site và cache 5 phút; Markdown đã lưu không bị sửa. Nhờ đó trang giảm dịch chuyển bố cục (CLS).
Gallery WordPress được giữ nguyên
Gallery hai cột của WordPress sau khi di chuyển vẫn được bảo toàn: contentMd giữ khối HTML div.gallery.gallery-columns-2 (figure.gallery-item, figcaption.gallery-caption), và trình soạn thảo coi nó là một khối chỉ đọc nên sửa đoạn văn xung quanh không làm hỏng gallery. Xem chuyển từ WordPress.
Bản địa hóa
Cấu hình localization có vi (mặc định) và en với fallback. Với localized: true, cả {name}Md và {name} đều được địa hóa nên HTML theo locale của request (?locale=en). Khi chuyển ngôn ngữ, trình soạn thảo được dựng lại sau một khoảng ngắn để các phím gõ cuối được lưu vào ngôn ngữ cũ. Đây là bản địa hóa nội dung, tách biệt với ngôn ngữ giao diện admin.
Thêm một trường nội dung Markdown mới
Dành cho nhà phát triển mở rộng CMS:
- Dùng
markdownHtmlField('name', { localized: true })trong collection thay vì khai báo trườngrichText. - Tạo migration bằng
pnpm payload migrate:create; migration thêm cộtname_mdvàname. - Frontend đọc HTML từ
namequa REST và Markdown từnameMd. - Trong hook và script, đọc
nameMdrồi tự render, đừng dựa vàonamengoài REST. - Nếu cần shortcode, bảo đảm tài liệu có giá trị
tenant, nếu không thẻ sẽ giữ nguyên dạng chữ.
Quy trình viết một bài đề xuất
- Gõ
/để chọn tiêu đề H2, H3 và cấu trúc bài. - Dán nội dung nháp từ Google Docs; kiểm tra lại bảng và ảnh sau chuyển đổi.
- Dùng
@chèn liên kết nội bộ tới ít nhất vài bài liên quan. - Chèn shortcode cho phần lặp lại như bảng giá hay chính sách.
- Đặt SEO và từ khóa chính ở tab SEO, xem SEO và JSON-LD.
- Lưu nháp, xem lịch sử phiên bản và đăng qua quy trình biên tập.
Tài liệu kỹ thuật đầy đủ: Soạn thảo Markdown trên docs.opencms.kdigi.vn. Quay lại tổng hợp tính năng.
Câu hỏi thường gặp
KDIGI CMS dùng trình soạn thảo nào?
Có dán được nội dung từ Google Docs hoặc Word không?
API trả về Markdown hay HTML?
Shortcode trong KDIGI CMS dùng để làm gì?
HTML thô trong Markdown có an toàn không?
Đọc tiếp
Thư viện media
Quản lý ảnh theo thư mục, sửa ảnh trực tiếp, nhập ảnh từ URL có chống SSRF, lưu trên ổ đĩa hoặc Cloudflare R2, xuất WebP để trang tải nhanh hơn.
SEO và JSON-LD
Trường SEO, mẫu title kiểu RankMath, đồ thị JSON-LD, ảnh OG dự phòng, kiểm tra SEO, gợi ý internal link và AI SEO tùy chọn ngay trong trình soạn thảo.
Headless API
Lấy nội dung qua REST hoặc GraphQL, Markdown chuyển sang HTML ở khóa gốc, hợp đồng revalidate cho ISR và ví dụ tích hợp Next.js chạy được ngay.
Quy trình biên tập
Bảng điều khiển biên tập, lịch nội dung, bản nháp và xuất bản, lịch sử phiên bản có khôi phục, thùng rác. Giúp nhóm nội dung làm việc nhất quán.