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.
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.
Three rules govern the tree:
- A
BlockElementis 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. - A
GridElementplus itsCellElementchildren 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). - Most leaf nodes are typed —
PElement(paragraph),HeadingElement,ImageElement,ButtonElement,DividerElement,ListElement,IconElement, plus a dozen more. The full catalogue lives indocs/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 anImageElementJSON node by piping it throughImage.template.html.master/sample/email/<name>.jsonandmaster/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.
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:
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
PageElementdown 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 blurry →
tree demo/themes/default/master/sample/email | head -10lists 30+ samples in one theme. The theme is the dir; each.jsonis one sample. - 🩹 Round-trip doesn't fit your stack → Wire a real backend covers MySQL, Postgres, S3, multi-tenant via closure capture — same engine, your persistence.
What's next?
- → JSON structure — concrete: read your first saved page and recognise every node.
- 🔗
examples/basic/1-quickstart/— the smallest runnable host for everything on this page. - 🔗
examples/cookbook/3-compact-3col/— a 200 / canvas / 240 px chrome shape that uses all three containers.
Stuck?
- 🐛 grep
src/includes/Builder.jsformainContainer,widgetsContainer,settingsContainerto see the exact constructor option contract. - 🐛 inspect
demo/builder.php's$builderConfigblock — 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.