Skip to content

Data model ​

A document is a JSON object with blocks (required list) and an optional meta object; no other top-level keys. The contract is JSON Schema v1 (schema/document-v1.json).

Block shape ​

Every block has exactly these seven keys:

json
{
    "meta": {},
    "blocks": [
        {
            "id": "b_k3xf92ab",
            "type": "section",
            "enabled": true,
            "data": { "title": "Intro" },
            "settings": { "spacing_top": "lg", "width": "container" },
            "visibility": { "from": null, "until": "2026-12-31T23:59:59+00:00", "sites": null },
            "children": []
        }
    ]
}
KeyNotes
idunique per document, generated as b_ + 8 random alphanumerics
typeblock handle matching ^[a-z0-9_-]+$
enabledfalse = never rendered
data / settingscontent fields / presentation settings (objects)
visibilityexactly from, until (ISO 8601 or null) and sites (site ids or null; a non-empty list hides the block on unknown or unlisted sites)
childrennested blocks, for container blocks
  • Versions, diffs and the media index key on block ids. Copy documents or subtrees with ContentApi::duplicateDocument() / duplicateBlock() so every block gets a fresh id.
  • Nesting is capped at 20 levels (ContentDocument::MAX_DEPTH); a field can lower it with maxDepth.

Normalization vs. strict validation ​

  • ContentDocument::normalize() repairs loose input: missing ids, enabled, data, settings, children and visibility keys. Invalid JSON, a bad type, excess depth and duplicate ids are reported in getErrors().
  • DocumentValidator rejects anything off-contract: unexpected or missing keys, wrong types, malformed visibility.

Every write path (form widget via DocumentSaveGuard, Content::put(), REST PUT /document) normalizes, then validates, and rejects the document on errors from either step. Unknown block types are kept, not rejected.