Skip to content
Get started A2 7 min read Overview

Architecture

How BuilderJS thinks about a page — three containers, one hierarchy.

You can use BuilderJS without understanding its architecture — drop the scripts in, point a save URL at your backend, ship. But the moment you want to customise anything (theme, widget, control, save shape), you'll hit one of three concepts you're now ready to internalise. This page walks you through them in the order the engine itself does the work.

The three containers

Every BuilderJS instance is one engine talking to three DOM containers you give it via the constructor. Open /builder.php (you saw it in the Quickstart) and look at the layout — left rail, centre canvas, right rail. That's it.

Construction is one line per container plus the bundle ingredients:
`widgetsContainer` and `settingsContainer` accept `null` (or a missing key) — pass `null` for both and you get a "preview-only" canvas with no editor chrome (the [`cookbook/6-canvas-only/`](/examples/cookbook/6-canvas-only/) example does exactly this). When the buyer picks a non-default chrome shape — split-pane, mobile tabs, floating settings — they pick which containers exist and the engine adapts. The 8 cookbook variants cover every shape we've seen buyers want.

The four globals THEME_JSON · THEME_TEMPLATES · THEME_CONFIG_DATA · MEDIA_URL are the ingredients builder.load() needs to render anything. They come from a theme bundle — the next section explains where they live.

The page hierarchy

Inside the canvas, every page is a tree of typed nodes. The shape is small: four kinds of nodes, three nesting rules.

You'll never edit this tree by hand — drag-drop in the canvas mutates it; selecting an element + tweaking a control mutates it. But understanding the shape pays off the moment you author a sample, write a custom widget, or read a saved JSON file.

Three rules govern the tree:

  1. A BlockElement is a row — it carries vertical rhythm (top + bottom padding) and a single inner column of content. Nothing else owns vertical spacing; never put margin on a Block.
  2. A GridElement plus its CellElement children give you N-up columns — a 3-column hero, a 50/50 image-text split. Each cell is itself a stack of Blocks (so cells can hold rich content, not just one element).
  3. Most leaf nodes are typedPElement (paragraph), HeadingElement, ImageElement, ButtonElement, DividerElement, ListElement, IconElement, plus a dozen more. The full catalogue lives in docs/core/ELEMENT_DEFINITION.md; the canonical "what's the JSON shape?" answer is in JSON structure.

This shape is not the rendered HTML. It's the editable model that the engine serialises, deserialises, and renders into HTML via the active theme's templates.

Themes vs samples

Two related concepts, often confused — get this distinction wrong and the next docs won't land.

A theme is a folder under demo/themes/<name>/. It ships:

  • index.json — manifest (name, displayTitle, configData, formats baseline).
  • *.template.html — one EJS template per element type (P.template.html, Heading.template.html, Image.template.html, …). The engine renders an ImageElement JSON node by piping it through Image.template.html.
  • master/sample/email/<name>.json and master/sample/page/<name>.json — pre-authored sample pages.
  • master/assets/image/ — content images (the AI-generated photo / illustration library).

A sample is one of those *.json files. Each sample is a complete page tree (PageElement + its descendants) ready to paste into a builder.load(themeJson, …) call. The shipped default theme carries 70+ samples — every email + page layout you see on /gallery.php is one.

When demo/builder.php loads, it picks one theme and one sample (?theme=default&sample=master/sample/email/Minimal by default). The PHP-side ThemeRegistry::resolveBundle() reads the four pieces — sample JSON, every template, the theme's index.json, the media URL — and emits them as four named globals (THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL). The JS calls builder.load() with those four arguments in that order. Always four, named, never wrapped (BUILDER.md RULE I).

To author your own theme: copy themes/default/ to themes/<your-brand>/, swap the templates + samples, register the new theme dir in the gallery seed. To author just a sample: drop a JSON file under themes/default/master/sample/email/ and add a thumbnail. Theme overview and Create a sample cover both.

The save / load round-trip

The engine never writes to disk on its own. Persistence is the host's job — the engine just hands you a JSON object on demand.

`builder.getData()` returns a plain JSON-serialisable object (the page tree, plus the `theme` + `formats` baseline). What you do with it is yours: write it to a file (the demo's `save.php`), POST to your API, drop it in a database column, sync over WebSocket. On reload, you re-feed it via `builder.load(themeJson, themeTemplates, themeConfigData, mediaUrl)` — same four globals, same order.

Two methods you'll meet a lot: getData() returns the data object; getHtml() returns the rendered HTML string. They are separate methods. Never write const { html, data } = builder.getData() — that destructure doesn't exist (regression-scar locked in docs/BUILDER.md Lesson 73).

The shipped demo/backend/save.php shows one minimum-viable backend handler: it accepts the POST, validates the JSON shape, writes to demo/uploads/, returns 200. Wire a real backend walks you through replacing it with MySQL / Postgres / S3 / your auth + tenant-scoping pattern.

The events bus

When a user types in the canvas, drags a widget, undoes a change, or clicks Save, the engine emits events on its public bus. You subscribe to react — autosave, dirty-state badge, "last saved 2 minutes ago" pill, multi-window sync, telemetry. The pattern in one line:

`Builder.EVENTS` lists the constants: `DOCUMENT_CHANGED` (any mutation, debounced 120 ms), `MODE_CHANGED` (design ↔ preview flip), `DOCUMENT_SAVED` (after a successful Save call), `ELEMENT_ADDED` / `ELEMENT_REMOVED` (structural mutations). The five Phase-C events examples under [`/examples/events/`](/examples/#events) walk each one with a tiny consumer that's safe to copy verbatim. Most production hosts wire one or two of these — `DOCUMENT_CHANGED` for autosave + `DOCUMENT_SAVED` for the toast.

Where the source lives

These docs are a buyer-tier distillation. When you need to read deeper, head into src/includes/:

  • Builder.js — constructor, load(), save(), getData(), getHtml(), setMode(), selectElement(), the events bus.
  • BaseElement.js — base contract every Element subclasses (Build a custom Element).
  • BaseControl.js — base contract every Control subclasses (Build a custom Control).
  • BaseWidget.js — palette glue (Build a custom Widget).
  • <Type>Element.js — concrete subclasses: PElement, HeadingElement, ImageElement, ButtonElement, … each ~150-300 LOC.

Engine source is not required reading to ship — the docs + examples cover the buyer-tier surface. But it's the canonical truth when you hit a contract that the docs describe in plain English and you want the exact line-by-line behaviour.

Did you make it?

You should now be able to:

  • ✅ Name the three containers and explain what each one shows the buyer.
  • ✅ Walk a page-tree shape from PageElement down to a leaf element + draw the layout that JSON would render.
  • ✅ Distinguish theme from sample and explain what builder.load() consumes.
  • ✅ Describe the save / load round-trip in terms of getData() + load() + your own persistence.

If a step didn't make sense:

  • 🩹 Hierarchy still feels abstract → open the builder, click the canvas, watch the breadcrumb at the top of the engine chrome. Each crumb is one node in the tree. Clicking any crumb selects that ancestor.
  • 🩹 Theme vs sample still blurrytree demo/themes/default/master/sample/email | head -10 lists 30+ samples in one theme. The theme is the dir; each .json is one sample.
  • 🩹 Round-trip doesn't fit your stackWire a real backend covers MySQL, Postgres, S3, multi-tenant via closure capture — same engine, your persistence.

What's next?

Stuck?

  • 🐛 grep src/includes/Builder.js for mainContainer, widgetsContainer, settingsContainer to see the exact constructor option contract.
  • 🐛 inspect demo/builder.php's $builderConfig block — every option a buyer changes is one labelled line.
  • 🐛 file an issue with: page URL, the JSON output of builder.getData(), what you expected vs what you got.