Create a sample
Add a NEW email sample to the default theme — copy along, ship in 30 minutes.
This is the headline doc. By the end of it you'll have a brand-new email sample shipped inside the default theme — appearing in /gallery.php, opening in /builder.php, surviving a save round-trip, passing the smoke E2E. Lock-step copy-along: read top to bottom, paste each block where the doc says, run the verify step. Time budget: 30 minutes.
The fixture you'll build: a "Welcome to Brand" onboarding email — a single-column layout with a hero image, a heading, two body paragraphs, a CTA button, and a footer. Same shape any SaaS sends a new signup.
Heads up — sandbox safety. All file paths in this doc point inside
themes/default/. If you'd rather not pollute the shipped theme during practice, copy the dir first:cp -r demo/themes/default demo/themes/sandboxand substitutesandboxfordefaultin every command. The exercises work identically.
Step 1 — Pre-flight
Your dev server must be running. From the repo root:
- Open http://localhost:8000/gallery.php. You should see the theme browser with 43 cards across "Standard email" / "Rich content" / "Page templates".
- Open http://localhost:8000/builder.php. The Minimal sample should render in the canvas.
If either fails, return to Quickstart — those are the boot-time blockers.
Step 2 — Pick a target
Decide three things before you write a line of JSON:
- Internal slug — the filename. Use
PascalCasematching the rest of the shipped samples:WelcomeToBrand(notwelcome-to-brandorwelcome_to_brand). - Display name — what buyers see in the gallery:
"Welcome to Brand". Title-cased English; no markdown, no emoji. - Layout shape — what stack of rows the email needs:
- Hero block (logo + h1 headline + sub-paragraph)
- Body block (two paragraphs introducing the product)
- CTA block (one button, centered)
- Footer block (single paragraph + unsubscribe link)
For this walkthrough, every choice is fixed: slug WelcomeToBrand, name Welcome to Brand, the four-row layout above.
Step 3 — Author the JSON
Below is the page tree for the new sample. Every node uses the shapes from JSON structure — verify each as you read.
- Every node has
name,template,formats(RULE-H6 — round-trip needs them). - Every block carries only
padding_top+padding_bottom. No margin, no horizontal padding (RULE-H11). - Every element has explicit pixel widths / sizes. No
"width": "100%"or"font_size": "1em"(RULE-H12). - Three colours (
#F4F4F4,#000000,#FFFFFF) plus#111111/#212121/#444444/#888888are all greyscale variants — counts as one chromatic family. Within the 4-colour cap (RULE-H8).
Step 4 — Put the file in the right place
Save the JSON above as:
Step 5 — Render the HTML preview
Open the new sample in the live builder by passing it as a query param:
- The canvas should render the four blocks: hero → body → CTA → footer.
- Click the heading → the right-rail settings panel shows H1 controls.
- Click the button → the panel shows button-shape + colour controls.
- The four blocks should be vertically stacked with the padding values from the JSON; no horizontal scroll.
If the canvas is blank, open DevTools → Console. The most common cause is a JSON typo: a stray comma, a quoted boolean ("true" instead of true), a missing template. The console message names the offending node.
Step 6 — Add the thumbnail
The gallery shows a card per sample — the card needs a thumbnail. Two paths:
Auto-generated (recommended): the shipped helper takes a screenshot at 496×558px:
Hand-crafted: drop a WelcomeToBrand.png file at the same path yourself. Same dimensions (496×558); PNG only.
PNG vs SVG. Extended templates use
.png(full-bleed photo render); base templates use auto-detected.svg. Match the existing convention by shipping.pngfor any new sample registered under the'extended'SEED bucket.
Step 7 — Register the sample in the seed
Open demo/backend/_lib/ThemeRegistry.php. Find private const SEED. Append a row to the 'extended' array:
dir— the theme folder name.default(or your fork name).name— the gallery display label. Title-case English, ≤ 30 chars.sample— the path underthemes/<dir>/, no leading slash, no.jsonsuffix.
That's the whole registration.
Step 8 — Verify it shipped
Reload the gallery:
- Open http://localhost:8000/gallery.php.
- Find your new card under "Rich content".
- Click it. The builder should open with your new sample.
- The sample should be card #44 (43 shipped + your new one).
Step 9 — Hard-rule audit
Walk the 8-rule checklist from Theme anatomy:
| Rule | Did you pass? | What to check |
|---|---|---|
| H1 — No copyrighted images | ✅ | You used master/assets/image/email/logo.png, the shipped AI-generated logo. |
| H2 — Hierarchy | ✅ | Page → Block → Element. No skipped layers. |
| H3 — No hardcoded image sizes (except logos) | ✅ | The logo's "width": 130 is fine — logos are exempt. |
| H4 — Aspect-lock | ✅ | The image element doesn't override aspect ratio. |
| H5 — Crop is page-only | ✅ | You didn't add a crop format key. |
| H8 — 4-colour palette cap | ✅ | Black (#000), white (#FFF), accent (light grey #F4F4F4), greys for body text — ≤ 4 chromatic families. |
| H10 — 5-typography-tier cap | ✅ | One H1 + paragraphs at 14 + 16 + 12 (body / lede / footer). 4 tiers used; 5 max allowed. |
| H11 — No margin on Block | ✅ | Every block uses only padding_top / padding_bottom. |
| H12 — Explicit units | ✅ | Every dimension is an integer (pixels). |
| H13 — Dead keys forbidden | ✅ | Every key in your JSON is a valid format / attribute. |
Step 10 — Round-trip gate
This proves your sample survives a save:
- Open http://localhost:8000/builder.php?theme=default&sample=master/sample/email/WelcomeToBrand.
- Click the H1 → change the text to
"Welcome aboard, friend". - Click Save in the top-right.
- Reload the page.
- The H1 still reads "Welcome aboard, friend".
- The save indicator showed "Saved ✓" before reload.
The save writes to demo/uploads/; the next load reads back from there. (When you ship a real backend, that storage layer changes — see Wire a real backend — but the round-trip rule stays the same.)
Step 11 — Smoke E2E
The shipped Playwright test discovers every sample in themes/default/master/sample/ automatically and renders each one to confirm it boots. Run it:
npx playwright test e2e/master-samples-smoke.spec.js -g WelcomeToBrand- One test executes (the one that targets your new sample).
- Output:
1 passed. - If it fails, the error names what's wrong (a missing thumbnail, a JSON parse error, a template that errored at render).
If you didn't pass -g WelcomeToBrand you'd run the full suite (~30 seconds) — useful to confirm you didn't accidentally break existing samples.
Did you make it?
You should now have:
- ✅ A
WelcomeToBrand.jsonshipped underthemes/default/master/sample/email/. - ✅ A matching
WelcomeToBrand.pngthumbnail. - ✅ A new row in
ThemeRegistry::SEED['extended']. - ✅ A 44th card in
/gallery.phpthat opens the sample in the builder. - ✅ A passing
master-samples-smokeE2E test.
If a step didn't work:
- 🩹 Canvas blank on the new sample URL → open DevTools, look for a JSON parse error. Most often a stray trailing comma or a typo'd
templatevalue. - 🩹 Thumbnail script fails →
node scripts/core/gen-thumb.cjs <slug>requires Playwright installed (npm install). Run the install once at the repo root. - 🩹 Card not appearing in gallery → confirm the
'extended'SEED array got your row. Watch out for missing comma at the end of the previous row. - 🩹 Round-trip drops your changes → check
demo/uploads/is writable (chmod 755 demo/uploads). Save's POST to/backend/save.phpwould 500 otherwise. - 🩹 Smoke test fails with "thumbnail file does not exist" →
gen-thumb.cjsneeds a sample that renders cleanly first. Reorder: render in builder OK → run gen-thumb → re-run E2E.
What's next?
- → Create a theme — graduate from one new sample to one new theme dir (logo, colours, custom Page template).
- 🔗
examples/basic/3-custom-theme/— runnable code that loads a custom theme via?theme=<dir>URL params.
Stuck?
- 🐛 grep
themes/default/master/sample/email/Minimal.jsonfor the smallest-possible reference shape. - 🐛 the shipped 50+ samples are your library — if you want a feature you don't see in this doc (background image, full-width hero, animated GIF), open a similar sample's JSON and copy the relevant block.
- 🐛 file an issue with: your
WelcomeToBrand.json, the gallery URL, exact error from console / E2E output.