0xCMS 入面每一份內容都是一個 page,page 的 body 是一個 lect — 單一、輕 schema 的 JSON object,通用、多語,並且 可無限擴充。blueprint 描述形狀;lect 就是活的文件。
{
"_type": "mail_list",
"_pointers": {
"event": "21857528035279",
"edm": "21844625140640"
},
"allow_checkin": "yes",
"name": { "en": "01 FULL RUN" },
"_modifier": 1,
"_updated_at": "2026-06-24T15:54Z"
}
// 01 — page
Page 是內容單位 — 可以是一個活動、一篇文章、一位嘉賓、一個 email template。少量結構化 metadata 會放在真實欄位,而實際內容全部放在一個 lect 欄位。
draft_pages rowdraft_pages — 私有,絕不公開。page_versions — 可回復到任一時間點。page : lect :: record : document
// 02 — lect
lect 是一個帶有少量慣例的 JSON object。同一種形狀可表示 page、content block、repeatable item 或 tag — 因此 editor、renderer 及 plugins 都講同一種語言。
標示這個 lect 跟隨哪個 blueprint — 例如 "event"、"mail_list"、某個 block type 等。
人類文字以 language map 儲存:"name": { "en": "Gala", "zh-hant": "晚宴" }。系統按 requested language resolve。
與語言無關的設定直接儲存:"allow_checkin": "yes"。在 blueprint 以 @field 宣告。
以 id 連到其他 pages:"_pointers": { "event": "2185…" }。屬於 soft relation,不需要 rigid foreign key。
有序的 nested lects — content blocks(_blocks)及 repeatable groups("session": [ … ]),每個都有自己的 _type、_weight、_name。
CMS 與內容一起保留的 metadata。未知 keys 會原封不動保存 — 格式向前兼容。
{
"_type": "event",
"start": "2026-06-25T19:00",
"_pointers": { "venue": "21857528035279" },
"name": { "mis": "Launch", "zh-hant": "發佈會" },
"body": { "en": "Join us at the edge…" },
"session": [
{
"_weight": 0,
"start": "19:00",
"name": { "en": "Keynote" }
}
],
"_blocks": [
{ "_type": "paragraph", "_weight": 0,
"body": { "en": "## Agenda…" } }
]
}
// 03 — 多語
文字不是單一 flat string,而是按語言為 key 的 map。新增語言只需一行 config,不是 schema migration。讀者要求某語言時,resolver 會回傳該語言,否則 fallback 到中性 base。
// 一個欄位,所有語言
"name": {
"mis": "Launch", // base / neutral
"en": "Launch",
"zh-hant": "發佈會",
"zh-hans": "发布会"
}
mis base language0xCMS 預設提供 languages: ['mis', 'en', 'zh-hant', 'zh-hans'],並以 mis 為 default — 這是 ISO 639 中代表 language-neutral base 的代碼。resolve 指定語言時,會先回傳該語言的 value,再 fallback 到 mis,最後 fallback 到任何已存在 value,所以部分翻譯也不會 render 空白。
// 04 — blueprint ⟷ lect
blueprint 是一個小型 declarative array,用來描述 page type 的欄位。它是 schema;lect 是 document。Editor 讀取 blueprint 來 render 正確 widget,並由它 scaffold 空 lect — 但 lect 本身仍然是自由 JSON。
blueprint.event = [
"@start:date/datetime",
"*venue",
"name:text/title",
"body:richtext/md",
{ session: [ "@start", "name" ] }
]
{
"_type": "event",
"start": "2026-06-25T19:00",
"_pointers": { "venue": "2185…" },
"name": { "mis": "Launch" },
"body": { "en": "## Hi" },
"session": [ { "start": "19:00", "name": {…} } ]
}
// blueprint tokens → 在 lect 中會變成甚麼
一個 blueprint 可塑造 多個 lect。Blueprint 指引 editor 並填入預設值,但不會鎖死 document:lect 可以帶有 blueprint 從未提及的 keys,所以插件可存放自己的資料(例如 sent_edm log、額外 _pointers.event),round-trip 後仍保持不變。
// 05 — 擴充
因為內容存在 declarative blueprint 背後的 JSON,擴充模型只需 additive change — 不用 ALTER TABLE,不用 downtime。
在 blueprint array 加一個 token。它會即時出現在 editor 及新 lect;現有 lect 只是暫時沒有該 key。
在 languages 加一個 code。每個 value field 本來已是 map,譯者只需填入新 key — 沒有欄位會被鎖成單語。
field:renderer 會 resolve 到 /snippets/pagefield/<renderer> 的 Liquid snippet。放入 template 就可推出新 field type — date picker、地圖、markdown。
在 blocks 註冊 block type,並在 blockList 列出。Editor 會把 blocks 疊入 _blocks[] — 每個都是 nested lect。
*pointer 以 id 把一個 page 連到另一個 page — 可把 EDM 及 guest list 歸到活動之下、引用 venue — 全部都是 _pointers 裏的 soft relation。
插件 Worker 可透過 HTTPS 向 effective config 注入完整 page types、blocks 及 field snippets — 新內容形狀 毋須重新部署 CMS。