DEV Community

Mack
Mack

Posted on

How to Preview and Test HTML Email Templates Programmatically

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Key Tips

  1. Use 600px width — that's the standard email rendering width
  2. Test with and without images — add block_resources: ["image"] to catch broken layouts
  3. Generate dark mode variants — use prefers-color-scheme: dark media query and render both
  4. 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)