{
"name": "page",
"template": "Page",
"formats": { "padding_top": "32", "padding_bottom": "32" },
"page_title": "Hello there",
"blocks": [
{
"name": "block",
"template": "Block",
"formats": {
"padding_top": "24", "padding_bottom": "24",
"padding_left": "16", "padding_right": "16",
"background_color": "#f5f3ff"
},
"elements": [
{
"name": "heading",
"template": "Heading",
"level": "h1",
"formats": { "color": "#5b21b6", "font_size": "32", "font_weight": "700" },
"content": "Hello there 👋"
}
]
}
]
}
Custom Theme
The directory layout BuilderJS reads. Fork themes/default/, change the brand block, edit one or two templates — every page across your product picks it up.
Walkthrough
-
The directory layout
Each theme is a flat folder under
themes/.index.jsonregisters which templates exist; the matching<Name>.template.htmlfiles render them. Sample pages live undermaster/sample/<category>/— the gallery and the in-builder Change Theme modal both read from there.Treethemes/example-mini/ ├── index.json # 1 — config + brand variables ├── Block.template.html # 2 — block frame (the file most buyers edit) ├── Page.template.html # 3 — page wrapper ├── master/ │ └── sample/ │ └── email/ │ └── HelloThere.json # 4 — a minimal sample page └── README.md # not read by the builder; for the buyer # Production themes also ship Grid · Cell · Heading · P · Image · Button # · Alert · Menu · Divider · etc. — copy themes/default/ for the full set. -
Define brand variables in index.json
A
_brandblock is a buyer convention — not part of the BuilderJS schema. Templates read it via the EJS render context (configData._brand) and weave the values into the inline style string. Change one line here, every block on every page reflavors.JSON{ "name": "example-mini", "title": "Example Brand", "_brand": { "primary": "#5b21b6", "primary_fg": "#ffffff", "accent": "#a855f7" }, "pages": ["Page"], "templates": ["Block", "Grid", "Cell", "Heading", "P", "Image", "Button"] } -
Edit Block.template.html — the highest-leverage file
EJS server-side.
formatter.toStyleStringAll()emits the format properties bound by the controls panel; you can append your own CSS to apply brand-wide styling. Below: a 4 px brand-primary stripe on every block — one of the smallest visible diffs against the default theme.EJS<% var brand = (typeof configData !== 'undefined' && configData && configData._brand) || {}; var brandStripe = brand.primary ? ('border-left:4px solid ' + brand.primary + ';') : ''; %> <div style="<%- formatter.toStyleStringAll() %><%- brandStripe %>"> <%- elements %> </div> -
Register the new theme in ThemeRegistry
Open
demo/backend/_lib/ThemeRegistry.phpand add a row to theSEEDconstant under the matching category (standard/extended/page). The gallery + Change Theme modal pick it up on next reload — no DB, no rebuild.PHPprivate const SEED = [ 'standard' => [ // …existing rows… ['dir' => 'example-mini', 'name' => 'Hello There', 'sample' => 'master/sample/email/HelloThere'], ], // 'extended', 'page' — same shape ];
The whole snippet
Click Copy on any file to grab it byte-identical, or Expand to read inline (capped at ~520 px so the page stays scannable). Multi-file snippets surface a side-by-side grid — copy only the files you need.
Notes
Why does the live demo above load themes/default instead of themes/example-mini? Because themes/example-mini/ ships only Page + Block templates + one sample — it's an intentional teaching artefact, not a bootable theme. Production themes need the full template set (Heading, P, Image, Button, Grid, Cell, Menu, Divider, Alert, …). To see your forked theme rendered live, copy themes/default/ wholesale, edit incrementally, and the same mini-builder code in the steps above renders it.
Real-world starting point: copy themes/default/ to themes/your-brand/ wholesale, then customise. The default ships ~35 templates covering every element type the builder supports — far simpler than building from zero.
Where templates run: EJS templates render server-side at canvas paint time and again at HTML export. Both contexts get the same formatter + configData + elements locals — what you see on the canvas is what you get in the exported HTML.
Sample JSON shape: a tree of nested objects, each carrying {name, template, formats, …} plus element-specific fields. See the dev docs for the full schema. The bidirectional contract is documented there too — builder.getData() emits exactly this shape, and builder.load(...) consumes it.
Hardened invariant: don't rename a template that already has consumer pages — buyers loading a saved JSON with the old template name will see a "missing template" error. Add a new template instead, migrate samples gradually, then deprecate. (See docs/BUILDER.md § "Hardened Rules" → "Before adding a new SAMPLE JSON".)