Skip to content
Build a theme B3 8 min read Customisation Copy-along · 30 min

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/sandbox and substitute sandbox for default in every command. The exercises work identically.

Step 1 — Pre-flight

Your dev server must be running. From the repo root:

Then in your browser:

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:

  1. Internal slug — the filename. Use PascalCase matching the rest of the shipped samples: WelcomeToBrand (not welcome-to-brand or welcome_to_brand).
  2. Display name — what buyers see in the gallery: "Welcome to Brand". Title-cased English; no markdown, no emoji.
  3. 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.

Notice four invariants — verify each:
  • 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 / #888888 are 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:

Filename `WelcomeToBrand` (no `.html`, no `_`, no spaces). The engine looks for samples in this exact folder; mis-spelling silently means "this sample doesn't exist" (gallery card 404 on click).

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:

That writes `demo/themes/default/master/sample/email/WelcomeToBrand.png` next to your JSON.

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 .png for 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:

Three keys per row:
  • dir — the theme folder name. default (or your fork name).
  • name — the gallery display label. Title-case English, ≤ 30 chars.
  • sample — the path under themes/<dir>/, no leading slash, no .json suffix.

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:

  1. Open http://localhost:8000/builder.php?theme=default&sample=master/sample/email/WelcomeToBrand.
  2. Click the H1 → change the text to "Welcome aboard, friend".
  3. Click Save in the top-right.
  4. 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:

Bash
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.json shipped under themes/default/master/sample/email/.
  • ✅ A matching WelcomeToBrand.png thumbnail.
  • ✅ A new row in ThemeRegistry::SEED['extended'].
  • ✅ A 44th card in /gallery.php that opens the sample in the builder.
  • ✅ A passing master-samples-smoke E2E 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 template value.
  • 🩹 Thumbnail script failsnode 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.php would 500 otherwise.
  • 🩹 Smoke test fails with "thumbnail file does not exist"gen-thumb.cjs needs 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.json for 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.