JSON structure
Read a saved page and recognise every node.
In the Architecture doc you met the page hierarchy as a concept. Now you'll meet it as a JSON file — and learn to read one without flinching. Every saved page is one of these shapes; every theme sample shipped under master/sample/ is one of these shapes; the engine's getData() returns one of these shapes. Master this and you can debug almost anything.
The page JSON shape
Open demo/themes/default/master/sample/email/Minimal.json (or run builder.getData() in DevTools while on /builder.php). The top-level object has three first-class keys plus the page tree:
theme— string. The theme dir underdemo/themes/. Determines which template family (P.template.html,Heading.template.html, …) renders this tree.name— string. The class name of this node — for the root, always"PageElement". The engine uses this to look up the right Element class when rehydrating.template— string. Which*.template.htmlfile to use. Often matchesnameminus theElementsuffix (PageElement→Page).formats— object. CSS-like properties (text_color,padding_top,border_radius, …). Always lower-snake-case, never camelCase. Different element types declare different format keys; the union is documented indocs/core/ELEMENT_DEFINITION.md.blocks— array (Page only). The page's rows. Each entry is aBlockElement— see next section.
That formats separation is the engine's core abstraction: presentational properties are in formats, structural / behavioural properties are top-level. A heading's text lives at text, but its colour lives at formats.text_color. Every element follows that split.
BlockElement — one row
Inside the page's blocks array, every entry is a row. The shape:
The elements array holds the actual content of that row. Each entry is either a leaf element type (PElement, HeadingElement, ImageElement, …) or a GridElement for N-up layouts.
Leaf elements — by type
The shipped engine surfaces around 30 element types. The four you'll see most often:
PElement (paragraph)
HeadingElement
ImageElement
ButtonElement
GridElement — N-up columns
When a row needs columns (image+text split, 3-up feature grid), wrap the row's content in a GridElement:
The round-trip rule
A page's JSON is its single source of truth. Two engine guarantees fall out of that:
builder.getData()is idempotent. Calling it twice in a row returns identical JSON.load(getData(x))is the identity function. If yougetData()a page, thenload()the result back, the canvas is byte-identical to before.
That round-trip is what lets you debug fearlessly — copy the JSON, mutate it in your editor, paste it back into builder.load() and you'll see the change in the canvas. The examples/api/2-get-data-load/ example walks the round-trip end-to-end with two side-by-side Builder instances copying state between each other.
The pattern that breaks the rule:
Common debug recipes
"My save isn't persisting" — open DevTools, run
builder.getData()in the Console. If it returns something sensible, the engine is fine — your handler is dropping it. If it returnsundefined, the engine isn't loaded yet (script error before boot)."I want to load a saved JSON without my backend" — paste it into
localStorage, then on reload runbuilder.load(JSON.parse(localStorage.getItem('myPage')), THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL)in the Console. TheTHEME_*globals are populated by the page; you only need to substitute the page tree."I edited the JSON manually and now it won't load" — the engine validates
name+template+formatskeys against the active theme. Aconsole.warnin DevTools tells you which node is invalid. Most often: a typo intemplate("Heading "with a trailing space, or"heading"instead of"Heading")."Something I expected isn't in the JSON" — open the canvas, right-click the element, choose "Inspect" in your browser to see the rendered DOM, then
builder.getData()to see the JSON. The two are connected by templates; what's missing in JSON wasn't part of the editable model in the first place.
Did you make it?
You should now be able to:
- ✅ Read a page JSON top to bottom and identify each node's
name+template+formats. - ✅ Distinguish a
BlockElement(row, vertical padding only) from aGridElement(N-up columns) from a leaf element (PElement,HeadingElement, …). - ✅ State the round-trip rule:
load(getData(x)) === x,getData()andgetHtml()are separate methods. - ✅ Open DevTools and use
builder.getData()to debug a save.
If a step was murky:
- 🩹 Real samples look bigger than the snippets here → that's expected.
tree demo/themes/default/master/sample/email/Minimal.jsonis ~250 lines once you include all the rows. Read top to bottom — each chunk fits one of the shapes above. - 🩹 Round-trip not working → confirm you saved the JSON output of
builder.getData(), notJSON.stringify(builder)(which captures internal state, not the page tree). - 🩹 I have a custom element type whose shape isn't here → see Build a custom Element. Custom Elements declare their own JSON shape via
getData()+static parse()overrides.
What's next?
- → Theme overview — what a theme provides, when to author one.
- 🔗
examples/api/2-get-data-load/— round-trip in action with two live Builders. - 🔗
examples/flows/2-import-bundle/— drag-drop a JSON file and watch the canvas swap.
Stuck?
- 🐛 grep
src/includes/<TypeName>Element.js(e.g.PElement.js) — every leaf element class declares itsgetData()shape. The JSON is whatever that method returns. - 🐛 the canonical schema spec is
docs/core/STRUCTURE.md. Heavier than this doc — read it when you need the full surface, not the buyer 80%.