@unitpost/email
v0.1.5
Published
JSON document model and cross-client HTML renderer for email. Constrained TSX, a component catalog, and layouts that render the same in Outlook, Gmail, and Apple Mail.
Maintainers
Readme
@unitpost/email
Author email as JSON or a tiny TSX dialect. Render table-based HTML that actually works in Outlook, Gmail, and Apple Mail.
npm install @unitpost/email zodPlayground · Template gallery · Components · npm
No account. No API key. zod is a peer (^4). Node 18+.
Quick start
import { parseTsx, renderToHtml, resolveVariables } from "@unitpost/email";
const doc = parseTsx(`
<Section padding-x={24} padding-y={32}>
<Heading level={1}>Hi {{first_name}} 👋</Heading>
<Text>Welcome to {{product_name}}.</Text>
<Button href="{{cta_url}}">Get started</Button>
</Section>
`);
const { values, missing } = resolveVariables(doc, {
first_name: "Ada",
product_name: "Northwind",
cta_url: "https://example.com",
});
if (missing.length > 0) throw new Error(`Unresolved: ${missing.join(", ")}`);
const html = renderToHtml(doc, values);Paste that markup into the playground to see the HTML before you install.
Unresolved {{tokens}} render literally. Values are HTML-escaped; javascript: URLs are dropped; a value containing {{other}} is never re-interpolated.
Examples
Same renderer as the samples above — copy one from the gallery or getSampleTemplate.
import { getSampleTemplate, renderToHtml, resolveVariables } from "@unitpost/email";
const welcome = getSampleTemplate("welcome")!;
const { values } = resolveVariables(welcome.design, { first_name: "Ada", product_name: "Northwind" });
const html = renderToHtml(welcome.design, values);| Transactional | Marketing | | --- | --- | | welcome · email-verification · magic-link · password-reset · receipt · invoice · team-invite · shipping-confirmation · subscription-renewal · trial-ending · security-alert · comment-mention | newsletter · july-newsletter · product-announcement · event-invite · discount-offer · re-engagement · feedback-survey · waitlist-invite · abandoned-cart · case-study · webinar-recap · referral |
Layouts
Pre-built bands you drop in, then edit. Live preview + TSX for each one: all layouts.
import { getSectionLayout, printFragmentTsx } from "@unitpost/email";
const hero = getSectionLayout("hero-simple")!;
const tsx = printFragmentTsx(hero.build());| Group | Layouts | | --- | --- | | Header | Logo header · Logo + links | | Hero | Simple hero · Image hero · Status hero | | Content | Card · Detail rows · Verification code · Quote / callout · Action feature · Announcement banner · Article feature | | Columns | Split 50/50 · Two cards · Three features · Numbered steps | | Call to action | CTA band · Centered CTA | | Footer | Simple footer · Rich footer · Footer + apps |
Footer bands are chrome only (logo, nav, a reply line). If you send marketing mail, add your own unsubscribe copy and postal address.
Components
Full prop tables and live previews.
| Group | Components |
| --- | --- |
| Layout | Section · Row · Column |
| Content | Heading · Text · Divider · Spacer · Markdown · Code |
| Media | Image |
| Interactive | Button · Link |
| Advanced | Html (sanitized) |
Every block accepts common props (spacing, alignment, Tailwind-style className compiled to inline CSS). Document chrome (<html>, <head>, preheader, the centered paper) is the renderer’s job — not missing components.
Or skip TSX and build the JSON:
import { emptyDocument, createBlock, renderToHtml } from "@unitpost/email";
const doc = emptyDocument();
doc.blocks.push(
createBlock("heading", { text: "Hello", level: 1 }),
createBlock("text", { text: "Built as data, rendered as email." }),
);
const html = renderToHtml(doc, {});API
| Area | Exports |
| --- | --- |
| Document | EmailDocument, EmailDocumentSchema, parseDocument, migrateDocument, COMPONENT_DEFAULTS, STYLE_TOKENS, TEMPLATE_CATEGORIES |
| Rendering | renderToHtml, resolveVariables, resolveVariablesWithContact, collectVariables, documentHasPerRecipientVariables |
| Codec | parseTsx, printTsx, printFragmentTsx, TsxParseError |
| Catalog | COMPONENT_CATALOG, COMPONENT_GROUPS, COMMON_PROPS, getComponentDoc, resolvePropDefault |
| Layouts | SECTION_LAYOUTS, LAYOUT_GROUPS, getSectionLayout |
| Samples | SAMPLE_TEMPLATES, getSampleTemplate |
| Sanitizer | sanitizeEmailHtml, safeUrl, safeImageUrl, hasForbiddenHtml |
| Helpers | createBlock, createRow, regenerateBlockIds, emptyDocument, safeParseDocument, BLOCK_LABELS, FONT_STACKS |
| Styling | compileClasses, cssToUtilities |
Development
npm install
npm run typecheck
npm test
npm run preview # HTML of every sample, open locallyA change to the HTML a published document renders to is a breaking change. See CHANGELOG.md.
License
MIT © Unitpost
