How to Preview and Test HTML Email Templates Programmatically
Email templates are notoriously hard to test. They render differently across Gmail, Outlook, Apple Mail, and dozens of other clients. But before you worry about cross-client compatibility, you need a reliable way to see what your template looks like — especially when you're generating them dynamically.
In this guide, I'll show you how to programmatically render HTML email templates as images, which is useful for:
- Visual regression testing — catch layout breaks before sending
- Thumbnail previews in your email builder UI
- Approval workflows — let non-technical stakeholders review without sending test emails
- Documentation — screenshot every template variant for your design system
The Problem with Email Preview
Most developers test emails by... sending them to themselves. This works for one template. It doesn't work when you have 50 templates with dynamic content, dark mode variants, and localized versions.
What you really want is a programmatic pipeline:
HTML template + data → rendered image → automated comparison
Option 1: Playwright (Self-Hosted)
If you're already running Playwright for testing, you can use it to render email HTML:
const { chromium } = require('playwright');
async function renderEmailTemplate(html) {
const browser = await chromium.launch();
const page = await browser.newPage();
// Set viewport to common email width
await page.setViewportSize({ width: 600, height: 800 });
// Load the HTML directly
await page.setContent(html, { waitUntil: 'networkidle' });
// Auto-resize to full content height
const height = await page.evaluate(() => document.body.scrollHeight);
await page.setViewportSize({ width: 600, height });
const screenshot = await page.screenshot({ fullPage: true });
await browser.close();
return screenshot;
}
Pros: Free, full control
Cons: You manage the browser binary, memory, and concurrency. Gets messy in CI/CD.
Option 2: Use a Screenshot API
If you don't want to manage browser infrastructure, a screenshot API handles rendering for you:
curl "https://api.rendly.dev/v1/screenshot?url=https://your-app.com/emails/welcome&width=600&full_page=true" \
-H "Authorization: Bearer YOUR_API_KEY" \
-o welcome-email.png
Or render raw HTML directly:
curl -X POST "https://api.rendly.dev/v1/screenshot" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<html><body style=\"font-family: Arial;\"><h1>Welcome!</h1><p>Your order #1234 has shipped.</p></body></html>",
"width": 600,
"full_page": true
}' \
-o email-preview.png
Pros: No infrastructure to manage, scales automatically, works in CI
Cons: API costs (though most have generous free tiers)
Building an Email Preview Pipeline
Here's a practical setup for a Node.js project with multiple email templates:
const fs = require('fs');
const path = require('path');
// Your template renderer (Handlebars, EJS, MJML, etc.)
const Handlebars = require('handlebars');
const RENDLY_API_KEY = process.env.RENDLY_API_KEY;
const TEMPLATE_DIR = './email-templates';
const OUTPUT_DIR = './email-previews';
// Sample data for each template
const templateData = {
'welcome': { name: 'Jane', company: 'Acme Inc' },
'invoice': { amount: '$49.00', date: 'Feb 15, 2026', items: ['Pro Plan - Monthly'] },
'password-reset': { resetUrl: 'https://example.com/reset/abc123' },
};
async function renderAllTemplates() {
const templates = fs.readdirSync(TEMPLATE_DIR)
.filter(f => f.endsWith('.hbs'));
for (const file of templates) {
const name = path.basename(file, '.hbs');
const source = fs.readFileSync(path.join(TEMPLATE_DIR, file), 'utf8');
const template = Handlebars.compile(source);
const html = template(templateData[name] || {});
const response = await fetch('https://api.rendly.dev/v1/screenshot', {
method: 'POST',
headers: {
'Authorization': `Bearer ${RENDLY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, width: 600, full_page: true }),
});
const buffer = Buffer.from(await response.arrayBuffer());
fs.writeFileSync(path.join(OUTPUT_DIR, `${name}.png`), buffer);
console.log(`✓ ${name}.png`);
}
}
renderAllTemplates();
Adding Visual Regression Testing
Once you have screenshots, you can compare them against baselines:
const { PNG } = require('pngjs');
const pixelmatch = require('pixelmatch');
function compareImages(baselinePath, currentPath) {
const baseline = PNG.sync.read(fs.readFileSync(baselinePath));
const current = PNG.sync.read(fs.readFileSync(currentPath));
const { width, height } = baseline;
const diff = new PNG({ width, height });
const mismatchedPixels = pixelmatch(
baseline.data, current.data, diff.data, width, height,
{ threshold: 0.1 }
);
const mismatchPercent = (mismatchedPixels / (width * height)) * 100;
if (mismatchPercent > 1) {
console.warn(`⚠️ ${mismatchPercent.toFixed(2)}% pixel difference detected`);
fs.writeFileSync('diff.png', PNG.sync.write(diff));
}
return mismatchPercent;
}
CI/CD Integration
Add this to your GitHub Actions workflow:
name: Email Template Preview
on:
pull_request:
paths:
- 'email-templates/**'
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Render email templates
run: node scripts/render-email-previews.js
env:
RENDLY_API_KEY: ${{ secrets.RENDLY_API_KEY }}
- name: Upload previews
uses: actions/upload-artifact@v4
with:
name: email-previews
path: email-previews/
- name: Comment PR with previews
uses: marocchino/sticky-pull-request-comment@v2
with:
message: |
## 📧 Email Template Previews
Updated templates have been rendered. Check the artifacts for screenshots.
Key Tips
- Use 600px width — that's the standard email rendering width
-
Test with and without images — add
block_resources: ["image"]to catch broken layouts -
Generate dark mode variants — use
prefers-color-scheme: darkmedia query and render both - Version your baselines — commit baseline screenshots to git for easy diffing in PRs
Wrapping Up
Email template testing doesn't have to be manual. With a screenshot API and a simple script, you can render every template variant on every PR and catch visual bugs before they reach your users' inboxes.
The full pipeline: template engine → HTML → screenshot API → image comparison → CI report. No browser to manage, no infrastructure to maintain.
Rendly is a screenshot and image generation API built for developers. Free tier available — no credit card required.
Top comments (0)