Writing a template
Templates are React components rendered the same way for the canvas and for export; this is the contract they keep.
Templates live in templates/ and are plain React components rendered the same way for
the editor preview and for export (renderToStaticMarkup → Playwright). A template module
exports three things:
export const descriptor: TemplateDescriptor; // id, name, fields, targets, override keys
export const overridesSchema: z.ZodTypeAny; // validates screen.overrides (zod, strictObject)
export function render(input: TemplateRenderInput): ReactElement;
export default { descriptor, overridesSchema, render } satisfies TemplateModule;Register it in templates/index.ts. Validation, the editor's override controls and the
render pipeline pick it up from there.
Rules the render pipeline relies on
- Return exactly one root element carrying
data-artwork, sizedtarget.width×target.heightpx. Use<Artwork input={input}>fromtemplates/shared.tsx; it sets the size, background (incl.backgroundImageassets/patterns), text colour, font stack anddir. - Put the capture inside
<DeviceShell>(or any element withdata-device). The overlap check measures text against that element's bounding box. - Put every piece of copy in
<TextBlock>. It emitsdata-check,data-line-height,data-max-lines,data-font-size,data-line-ratio,data-fit-min, which drive:- the in-page fitter (
lib/render/fit.ts): shrinks the font down tofitMinScale × fontSizeuntil the text fitsmaxLines; - the in-page checker (
lib/render/checks.ts): overflow (line based), overlap with the device, missing images, failed font faces.
- the in-page fitter (
- Scale every metric from
target.width(and branch ontarget.familyfor iPad) so one template serves both aspect ratios. Never hard-code pixels. - Use
input.brand.headlineFontStackfor headlines (falls back to the body stack when nobrand.headlineFontis configured). - Load assets only through
input.assetUrl(rel)and the capture throughinput.sourceImageUrl; they becomefile://URLs for export and/api/...URLs in the editor. No remote URLs, ever. - No state, no effects, no animations, no
Date/Math.random. Output must be deterministic for a given input.
Overrides
Extend commonOverridesSchema with .extend({...}) for template-specific keys and list
them in descriptor.overrideKeys. The editors show controls for the keys they know: the Mac
app's Layout tab (mac/StoreShots/Design/InspectorView.swift) and the browser editor
(OVERRIDE_CONTROLS in app/projects/[name]/editor.tsx). Add a control in both for a new key;
until then it can still be set in store/manifest.json.
stackLayout() in shared.tsx computes the text column and device boxes for the
"text + phone" family of templates, honouring all positional overrides (textWidth,
textSide, textOffsetY, screenshotScale, screenshotOffsetX/Y) and RTL mirroring.
hero-top and split-caption both use it with different defaults.
Tests to add
tests/templates.test.ts iterates every registered template: it renders both targets in LTR
and RTL, checks the artwork root size, the presence of data-check="headline" and
data-device, and that overrides apply. Add template-specific assertions next to it.