diff --git a/docs/template-pack.md b/docs/template-pack.md index ec5507f..1871980 100644 --- a/docs/template-pack.md +++ b/docs/template-pack.md @@ -217,3 +217,57 @@ Reusable themes may also define these constrained tokens: - Palette: `backgroundColor`, `surfaceColor`, `textColor`, `mutedColor`, `accentColor`, `buttonTextColor`, `borderColor` These tokens are editable from Preview and remain part of the draft/published theme. + + +# Production email output + +Email documents may define: + +```json +{ + "email": { + "subject": "Welcome {{customer.name}}", + "preheader": "Your account is ready" + }, + "blocks": [] +} +``` + +The email render API returns a provider-neutral payload: + +```json +{ + "subject": "Welcome Kauri", + "preheader": "Your account is ready", + "html": "...", + "text": "Welcome Kauri...", + "warnings": [], + "metadata": { + "key": "customer-welcome", + "version": 3, + "html_bytes": 18420 + } +} +``` + +This payload is suitable for transport providers such as SendGrid, Amazon SES, Postmark and Mailgun. Template Studio remains the canonical template renderer; providers only deliver the rendered subject/HTML/plain-text parts. + +## Email-safe blocks + +The email renderer supports heading, text, KPI, button, image, divider, spacer and table blocks. Tables are emitted through MJML's table component rather than browser CSS grid/flex layouts. + +Plain text is generated automatically from the same blocks, including table rows and button URLs. + +## Compatibility checks + +Each email render includes warnings for common risks including: + +- missing subject or preheader; +- missing image alt text; +- insecure HTTP image URLs; +- image-only email content; +- tables wider than six columns; +- decorative background image dependence; +- compiled HTML approaching common clipping limits. + +The editor exposes Mobile, Desktop, Plain text and Checks preview tabs. These checks are advisory and complement, rather than replace, real-client screenshot testing. diff --git a/server/index.js b/server/index.js index 456a527..ce5e2a3 100644 --- a/server/index.js +++ b/server/index.js @@ -3,7 +3,7 @@ import express from 'express' import { chromium } from 'playwright-core' import { initDb, getPool } from './db.js' import { readSession, registerAuthRoutes, requireAuth, verifyMachineToken } from './auth.js' -import { renderEmailHtml, renderReportHtml } from './templateEngine.js' +import { renderEmailMessage, renderReportHtml } from './templateEngine.js' import { getTemplateAsset, importTemplatePack, templatePackSpec } from './templatePack.js' import { getThemeAsset, importThemeJson, importThemePack, themePackSpec } from './themePack.js' @@ -188,10 +188,10 @@ async function publishedTemplateByKey(key) { return rows[0] || null } -async function sendRendered(res, type, document, data, format) { +async function sendRendered(res, type, document, data, format, metadata = {}) { if (format === 'email') { if (type !== 'email') return res.status(400).json({ error: 'format_not_valid_for_template' }) - return res.json({ html: renderEmailHtml(document, data) }) + return res.json(renderEmailMessage(document, data, metadata)) } const html = renderReportHtml(document, data) @@ -256,7 +256,12 @@ app.post('/api/v1/templates/:key/render', async (req, res) => { const data = req.body?.data ?? {} const format = req.body?.format || (template.type === 'email' ? 'email' : 'html') res.setHeader('Cache-Control', 'no-store') - await sendRendered(res, template.type, template.document, data, format) + await sendRendered(res, template.type, template.document, data, format, { + key: template.template_key, + name: template.name, + version: template.published_version, + published_at: template.published_at + }) }) /** @@ -468,7 +473,13 @@ app.post('/api/templates/:id/render', async (req, res) => { const data = req.body?.data ?? template.sample_data ?? {} const format = req.body?.format || (template.type === 'email' ? 'email' : 'html') res.setHeader('Cache-Control', 'no-store') - await sendRendered(res, template.type, template.draft_document, data, format) + await sendRendered(res, template.type, template.draft_document, data, format, { + id: template.id, + key: template.template_key, + name: template.name, + version: null, + draft: true + }) }) app.get('/api/api-keys', async (_req, res) => { diff --git a/server/templateEngine.js b/server/templateEngine.js index 2f51680..5fdab3b 100644 --- a/server/templateEngine.js +++ b/server/templateEngine.js @@ -202,6 +202,9 @@ function blockHtml(block, data, theme) { case 'divider': html = '