Templates
A template bundles a subject, optional preheader, and a block-composed body per locale.
import { defineTemplate, compileTemplate, renderEmail } from '@grundtone/email';
const welcome = defineTemplate({
key: 'welcome',
variables: ['name', 'url'],
locales: {
en: {
subject: 'Welcome {{name}}',
preheader: 'Verify your email to get started.',
build: (b) => ({
content:
b.heading({ text: 'Welcome {{name}}' }) +
b.button({ label: 'Verify', href: '{{url}}' }),
}),
},
},
});Compile once to the publishable artifact (placeholders intact), or compile and fill data in one step:
const artifact = compileTemplate(welcome, 'en'); // { subject, html, text, variables, errors }
const { subject, html, text } = renderEmail(welcome, 'en', { name: 'Allan', url: 'https://…' });The built-in templates — magic-link, verify-email, org-invite, invoice — are exported from @grundtone/email/templates.
The send-time variable contract
The compiled artifact carries Handlebars syntax that survives MJML compilation untouched:
{{var}}— a value. HTML-escaped in the HTML body, raw in subject/text.{{#each items}} … {{/each}}— loops (e.g. invoice line items, where each item exposes{{this.description}},{{this.quantity}},{{this.amount}}).{{#if name}} … {{/if}}— conditionals.
renderTemplate(artifact, data) performs the substitution in JS — for previews, tests, and TypeScript consumers. The Go notifications service performs the equivalent (Handlebars-compatible) substitution on the published artifact before sending via Resend.
URL placeholders
HTML escaping makes recipient data safe in HTML text and quoted-attribute contexts, but it does not validate URL schemes — a value in href="{{url}}" is emitted verbatim, so javascript:/data: URLs survive. Pass trusted/validated URLs for link placeholders.
Plain text
Every artifact ships a plain-text fallback. It is auto-derived from the compiled HTML (links become text [href], the hidden preheader is skipped, and placeholders are preserved). Templates whose body doesn't auto-derive into readable text — such as the invoice table — provide an authored text body instead.
Publishing
pnpm compile:templates compiles every built-in template × locale into a versioned, CDN-shaped artifact tree under published/ (sibling to dist, so the npm tarball stays clean):
v{version}/{key}/{locale}.json the publishable artifact (placeholders intact)
v{version}/manifest.json index of templates, locales and variables
manifest.json "current" pointer to the latest manifestThis mirrors the design-token publish pipeline: change a token → re-compile → re-publish → the email design updates. Everything is compiled and validated in memory first; the script refuses to publish (and touches nothing on disk) if any template fails to compile. It does not upload — it produces the artifact a deploy step or the studio publish pipeline consumes.
