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": []
}
]
}| Key | Notes |
|---|---|
id | unique per document, generated as b_ + 8 random alphanumerics |
type | block handle matching ^[a-z0-9_-]+$ |
enabled | false = never rendered |
data / settings | content fields / presentation settings (objects) |
visibility | exactly from, until (ISO 8601 or null) and sites (site ids or null; a non-empty list hides the block on unknown or unlisted sites) |
children | nested 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 withmaxDepth.
Normalization vs. strict validation
ContentDocument::normalize()repairs loose input: missing ids,enabled,data,settings,childrenandvisibilitykeys. Invalid JSON, a badtype, excess depth and duplicate ids are reported ingetErrors().DocumentValidatorrejects anything off-contract: unexpected or missing keys, wrong types, malformedvisibility.
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.