@cartella/renderer-email
v0.3.0
Published
Canvas JSON → cross-client-safe HTML email. Outlook-VML / Apple Mail / Gmail / iOS / Android.
Readme
@cartella/renderer-email
Canvas JSON → cross-client-safe HTML email.
Compiles an email-doctype Cartella canvas into a standalone HTML email
document. Nested <table> layouts, inline styles, MSO conditional
comments for Outlook fallbacks, bulletproof VML buttons, and a single
@media (max-width: 480px) block for mobile stacking. Zero deps beyond
@cartella/core. Isomorphic — runs in Node and the browser.
Status: phase 2b — library-only. The Studio editor (
@cartella/studio) for designing email canvases lands in phase 2d. For now, hand-author the JSON or use the reducer.
Install
pnpm add @cartella/renderer-email @cartella/coreUse
import { renderEmail } from '@cartella/renderer-email';
const { html, attachments } = renderEmail(canvas, {
subject: 'Your certificate is ready',
preheader: 'Click below to download — no expiry.',
data: {
studentName: 'Amara',
certificate: { url: 'https://...', imageUrl: 'https://...' },
},
});
// `html` is a self-contained email document. Hand it to your mailer.
// `attachments` lists any `email-document-ref` blocks with
// deliveryMode='attachment'; your mailer fetches the PDF and attaches it.What the renderer guarantees
- XHTML 1.0 Transitional doctype — Outlook desktop's Word renderer requires it.
- All styling inline. Gmail strips
<style>blocks under "Auto-show inline images"; we don't trust the document stylesheet for content. - Bulletproof buttons. Outlook gets a
<v:roundrect>VML branch inside<!--[if mso]>; everyone else gets a styled<a>. Same visual button across the matrix. - Mobile stacking via
cartella-column.mobile-stackand an@media (max-width: 480px)block. Opt-out per column withmobileStack: false. - Dark-mode opt-in.
<meta name="color-scheme" content="light dark">plus aprefers-color-scheme: darkblock and Apple Mail / Outlook web vendor selectors. Authors don't design for dark explicitly — clients auto-invert. - Safe URLs.
javascript:/vbscript:/ non-imagedata:schemes rewrite to#. Defence-in-depth: substitution runs before sanitisation, so a merge value that resolves to a dangerous URL is still caught.
Client coverage targets
Apple Mail (macOS + iOS), Gmail (web / iOS / Android), Outlook 365 web, Outlook desktop, Yahoo, ProtonMail. Pre-2018 clients (Outlook 2007–2013, Lotus Notes, AOL desktop) are explicitly out of scope.
Cross-client visual snapshot testing (Litmus / Email on Acid) is
phase 2c. The 2b snapshots in tests/snapshots/ are HTML-string diffs
only — they catch markup regressions, not rendering differences between
clients.
Variable substitution
{{var}} and {{var||fallback}} syntax — a tiny subset of Mustache,
matching the engine Academia already uses on their email-template side:
| Template | data | Output |
| ----------------------- | --------------------------- | ---------------- |
| Hi {{name}} | { name: 'Ada' } | Hi Ada |
| Hi {{name}} | {} | Hi |
| Hi {{name\|\|Friend}} | {} | Hi Friend |
| {{a.b.c}} | { a: { b: { c: 'deep' }}} | deep |
| {{n}} | { n: 0 } | 0 |
| {{user}} | { user: { name: 'Ada' } } | (empty — object) |
| {{x}} | { x: '<script>' } | <script> |
Substitution applies to text content, image src / alt / href,
button label / href, and the implicit {{<refField>.url}} /
{{<refField>.imageUrl}} constructed by email-document-ref. Output
is HTML-escaped per context after substitution — merge values flow
through the same defence as static authoring.
email-document-ref delivery modes
Reference a Cartella-designed document (certificate, ID card, receipt) from the email body. Each mode compiles to a different markup shape:
| deliveryMode | Compiles to |
| -------------- | ----------------------------------------------------------------------------------------------------- |
| link | Bulletproof button. Href = {{<refField>.url}}. |
| image | <img> preview. Src = {{<refField>.imageUrl}}. Alt falls back to templateName. |
| both | Image preview followed by the bulletproof button. |
| attachment | No body markup. Pushes {templateId, refField} onto the returned attachments array. Host attaches. |
refField defaults to 'document'. The host wires per-recipient data
through it: Academia's certificate flow uses refField: 'certificate'
and passes { certificate: { url, imageUrl, alt } }.
Examples
Hand-authored email canvases live in tests/fixtures/:
welcome.json— single section, text + button, basic merges.receipt.json— divider stack, multi-section, line items.newsletter.json— hero image, 2-col feature row.appeal-decision.json— Academia letterhead with Georgia serif.certificate-delivery.json—email-document-refboth.payment-receipt.json—email-document-refattachment.transactional-acceptance.json— dark hero + light body.mobile-stacking.json— 4×25% columns to exercise mobile stack.
Their pinned outputs in tests/snapshots/ are
the easiest way to see what real renderer output looks like.
See also
@cartella/core— the Canvas schema this renderer consumes.docs/email-mode-plan.md— the multi-phase work plan this package implements.
