← back to /home
the data model · page & lect

One shape for
all your content.

Every piece of content in 0xCMS is a page, and a page's body is a lect — a single, schema-light JSON object that is universal, multi-language, and endlessly extensible. A blueprint describes its shape; the lect is the living document.

draft_pages.lect — mail_list #2185…
{
  "_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 — the page

Everything is a page

A page is the unit of content — an event, an article, a guest, an email template. It carries a little structured metadata in real columns, and all of its actual content in one lect column.

The draft_pages row

uuidstable identity, survives publish & restore
name · slughuman title & URL key
page_typewhich blueprint shapes the lect
page_idparent page (tree structure)
weightsort order among siblings
start · end · tzoptional publish window
lect⟵ the JSON content payload
…_version_idhead of an append-only history

Draft → versioned → published

  • Edits live in draft_pages — private, never public.
  • Every save snapshots the lect into page_versions — roll back to any point.
  • Publishing fans the lect out to live targets (D1, static JSON, plugins).
  • Blocks, nested items and tags are also lects — the same shape, all the way down.

page : lect  ::  record : document

// 02 — the lect

A universal content format

The lect is one JSON object with a handful of conventions. The same shape represents a page, a content block, a repeatable item, or a tag — so the editor, the renderer, and plugins all speak a single language.

_type

Names the blueprint this lect follows — "event", "mail_list", a block type, etc.

value fields  → { lang: value }

Human text, stored as a language map: "name": { "en": "Gala", "zh-hant": "晚宴" }. Resolved per requested language.

@ attributes  → scalars

Language-agnostic settings stored bare: "allow_checkin": "yes". Declared @field in the blueprint.

_pointers  → references

Links to other pages by id: "_pointers": { "event": "2185…" }. Soft relations — no rigid foreign keys.

_blocks  ·  items[]

Ordered nested lects — content blocks (_blocks) and repeatable groups ("session": [ … ]), each with its own _type, _weight, _name.

_modifier · _updated_at

Metadata the CMS keeps alongside your content. Unknown keys are preserved untouched — the format is forward-compatible.

lect of an event page
{
  "_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 — multi-language

Every value is multilingual by default

Text isn't a flat string — it's a map keyed by language. Adding a language is a config line, not a schema migration. A reader asks for a language; the resolver returns it, falling back to the neutral base.

// one field, every language
"name": {
  "mis":     "Launch",   // base / neutral
  "en":      "Launch",
  "zh-hant": "發佈會",
  "zh-hans": "发布会"
}

The mis base language

0xCMS ships with languages: ['mis', 'en', 'zh-hant', 'zh-hans'] and a default of mis — the ISO 639 code for a language-neutral base. Resolution for a requested language returns that language's value, then falls back to mis, then to any value present, so partial translations never render blank.

mis en zh-hant zh-hans + add your own

// 04 — blueprint ⟷ lect

Schema meets document

A blueprint is a tiny declarative array that describes a page type's fields. It is the schema; the lect is the document. The editor reads the blueprint to render the right widgets, and scaffolds an empty lect from it — but the lect itself stays free-form JSON.

blueprint  — the shape (cms-config.ts)
blueprint.event = [
  "@start:date/datetime",
  "*venue",
  "name:text/title",
  "body:richtext/md",
  { session: [ "@start", "name" ] }
]
lect  — a document of that shape
{
  "_type": "event",
  "start": "2026-06-25T19:00",
  "_pointers": { "venue": "2185…" },
  "name": { "mis": "Launch" },
  "body": { "en": "## Hi" },
  "session": [ { "start": "19:00", "name": {…} } ]
}

// blueprint tokens → what they become in the lect

namelocalized value → { lang: value }
@attrscalar attribute → bare value
*refpointer → _pointers.ref
field:renderersame value, custom widget
{ group: [ … ] }repeatable items → array of lects
blockListswhich blocks → _blocks[]

One blueprint shapes many lects. The blueprint guides the editor and seeds defaults, but it never locks the document down: a lect can carry keys the blueprint never mentioned, so a plugin can stash its own data (a sent_edm log, an extra _pointers.event) and it round-trips untouched.

// 05 — extend

Grow the model without migrations

Because content lives in JSON behind a declarative blueprint, extending the model is additive — no ALTER TABLE, no downtime.

Add a field

Append a token to the blueprint array. It shows up in the editor and in new lects instantly — existing lects simply don't have the key yet.

🌐

Add a language

Add a code to languages. Every value field is already a map, so translators just fill a new key — no field is ever single-language.

🎛️

Add a widget

A field:renderer resolves to a Liquid snippet at /snippets/pagefield/<renderer>. Drop in a template to ship a new field type — date pickers, maps, markdown.

🧱

Add a block

Register a block type in blocks and list it in a blockList. Editors stack blocks into _blocks[] — each one a nested lect.

🔗

Relate pages

A *pointer links one page to another by id — group EDMs and guest lists under an event, reference a venue — all soft relations in _pointers.

🧩

Extend via plugins

A plugin Worker contributes whole page types, blocks and field snippets to the effective config over HTTPS — new content shapes with no CMS redeploy.

WHY IT HOLDS UP: schema-light JSON unknown keys preserved one shape: page · block · item · tag additive, migration-free

// see it in the editor

The shape is simple.
The reach is not.

Open the live editor and watch a blueprint render into a multilingual lect.