How to Make a PDF from an HTML Email Template
Render a complete HTML email template as a PDF with Puppeteer or Playwright. Set print styles, paper size, fonts, and page breaks for dependable output.
To make a PDF from an HTML email template, render the finished HTML in a real browser and use its PDF API. Puppeteer’s Page.pdf() and Playwright’s page.pdf() both create browser-rendered PDFs. Both use print CSS by default, so prepare the template’s print layout, choose paper and margins, wait for required assets, and inspect the resulting file. This workflow renders markup into a document; it does not attach or convert an email message sent through an email service.
The examples below assume you already have the template data and can produce a complete HTML document. They do not establish that the result will match Gmail, Outlook, or another email client: browser PDF output and email-client rendering are separate outcomes.
1. Prepare the email template as a complete HTML document
Resolve your template variables before rendering. The browser should receive the final content, including the subject or heading, recipient-specific values, inline styles or stylesheet links, and any required images. A standalone document is usually easiest to control:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Order confirmation</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; color: #202124; font: 16px/1.5 Arial, sans-serif; }
.email { max-width: 640px; margin: 0 auto; padding: 32px; }
img { max-width: 100%; height: auto; }
@page { size: A4; margin: 16mm; }
@media print {
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.page-break { break-before: page; }
a { color: inherit; }
}
</style>
</head>
<body>
<main class="email">
<h1>Order confirmed</h1>
<p>Thanks for your order. Your receipt is below.</p>
<img src="https://example.com/receipt-banner.png" alt="">
</main>
</body>
</html>
Replace the sample content and asset URL with your own. If rendering a local document or passing HTML directly, use absolute URLs or data URLs for assets as appropriate; relative paths need a base URL and files accessible to the browser process. The browser PDF references describe PDF generation controls, not a universal remote-image loading guarantee, so confirm that your images and styles actually load in your deployment.
2. Generate the PDF with Puppeteer
Install Puppeteer in a Node.js project, then render the page and write the PDF to a file. Puppeteer documents that Page.pdf() uses print CSS and waits for fonts by default. Its guide demonstrates navigation followed by PDF generation and browser cleanup. See the Puppeteer PDF guide and PDFOptions reference.
npm install puppeteer
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('http://localhost:3000/rendered-email/123', {
waitUntil: 'networkidle2',
timeout: 30000,
});
await page.pdf({
path: 'email-template.pdf',
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
margin: { top: '16mm', right: '16mm', bottom: '16mm', left: '16mm' },
waitForFonts: true,
timeout: 30000,
});
} finally {
await browser.close();
}
})();
Change the route to an authenticated or local rendering endpoint that returns the complete document. If you already have an HTML string, set it directly with await page.setContent(html, { waitUntil: 'networkidle2' }); ensure the HTML has a base URL if it uses relative assets. In a production service, handle exceptions around launch, navigation, and PDF creation, and remove or quarantine incomplete output rather than treating a failed render as a valid document.
Use screen styling only when the email design calls for it
PDF generation defaults to print media. If your template intentionally uses its screen styles, select screen media before calling pdf():
await page.emulateMediaType('screen');
await page.pdf({ path: 'email-screen.pdf', format: 'A4', printBackground: true });
For a document intended for printing, leave print media active and define @media print rules instead. Use @page to express page geometry in CSS; set preferCSSPageSize: true when that CSS page size should take precedence over the API’s paper dimensions. Without it, Puppeteer documents that content is scaled to fit the selected paper size.
3. Generate the PDF with Playwright
Playwright is another browser automation option when it already fits your project. Its page.pdf() returns a PDF buffer, which you can write to disk or send through your application’s existing response flow. The API documents print media as the default and offers emulateMedia() for screen rendering.
npm install playwright
const { chromium } = require('playwright');
const { writeFile } = require('node:fs/promises');
(async () => {
const browser = await chromium.launch();
try {
const page = await browser.newPage();
await page.goto('http://localhost:3000/rendered-email/123', {
waitUntil: 'networkidle',
timeout: 30000,
});
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
margin: { top: '16mm', right: '16mm', bottom: '16mm', left: '16mm' },
});
await writeFile('email-template.pdf', pdf);
} finally {
await browser.close();
}
})();
To produce a buffer from HTML rather than a route, call page.setContent(html, { waitUntil: 'networkidle' }) before page.pdf(). The official API notes that screen media requires await page.emulateMedia({ media: 'screen' }) before PDF creation. See the Playwright Page API.
4. Choose the PDF settings deliberately
| Setting | What it changes | Practical choice |
|---|---|---|
| Media type | Which CSS media rules apply | Keep print for document output; use screen only when that is intentional. [Puppeteer, Playwright] |
| Paper size and orientation | Page geometry and line wrapping | Choose the reader’s paper standard, such as A4 or Letter; use landscape for genuinely wide content. |
| Margins | Usable content area and whitespace | Set them in the PDF options or CSS @page; avoid unknowingly applying both sets twice. |
preferCSSPageSize (Puppeteer) |
Whether CSS @page geometry wins |
Set true when the template owns page size. The default is false, which scales content to fit API dimensions. [Puppeteer options] |
printBackground |
Prints background graphics | Enable it for colored panels or background images; Puppeteer defaults it to false. [Puppeteer options] |
| Print colors | Browser print color adjustment | Use -webkit-print-color-adjust: exact when color fidelity matters, then inspect output. Browser printing can modify colors. [Playwright] |
| Fonts | Glyph shape, width, and wrapping | Ensure fonts are available; Puppeteer waits for document.fonts.ready by default, but remote font loads can still time out. [Puppeteer options] |
| Scale | Overall content size | Use sparingly to correct a designed layout. Puppeteer documents a range from 0.1 to 2; redesign excessive overflow instead of shrinking text until unreadable. |
| Page ranges | Which pages are emitted | Useful for extracting selected pages from long output; verify the requested pages exist. |
| Headers and footers | Repeated page metadata | Use the API templates when page numbers or dates are required. Playwright notes these templates have limitations: page styles do not apply inside them and scripts are not evaluated. |
In Puppeteer, format takes precedence over width and height. Its PDF options also include landscape, margin, pageRanges, displayHeaderFooter, header/footer templates, omitBackground, timeout, and waitForFonts. Playwright accepts paper formats including Letter and A4, margins with units, scale, ranges, and header/footer templates; consult its PDF API for the version in your project. Set one authoritative source for page geometry and check that the resulting content is not unexpectedly scaled.
5. Handle page breaks and long messages
Email templates are often designed as narrow, continuous layouts, while a PDF has fixed page boundaries. Add print-specific rules to avoid breaking important blocks and to control deliberate section boundaries:
@media print {
.email-header, .receipt-total, .signature, .keep-together {
break-inside: avoid;
}
.new-page {
break-before: page;
}
.hide-in-pdf {
display: none !important;
}
}
Check the actual output for a heading orphaned at the bottom of a page, a table row split across pages, clipping, or large blank areas caused by forced breaks. Test both a short and a long real template instance; page length affects where otherwise identical markup breaks.
6. Validate the generated document
- Confirm that template variables were resolved and that the rendered page is the intended recipient’s version.
- Check that images, CSS, and fonts load in the rendering environment. A page navigation reaching a network-idle condition does not prove every expected asset succeeded.
- Open the PDF and inspect wrapping, image scale, margins, colors, page breaks, and repeated headers/footers.
- Exercise representative cases: short and long text, missing optional fields, long URLs, non-Latin characters, and messages with and without images.
- Regenerate after browser, template, font, or asset changes, and keep the browser version controlled if reproducibility matters.
PDF generation confirms how that browser rendered the document under those settings. It does not establish identical appearance in email clients. The cited browser API references make no promise of Gmail or Outlook equivalence, so validate that requirement separately if the PDF must mirror a particular client.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Background colors or images are missing | Print backgrounds are disabled by default in Puppeteer, or print color adjustment changed colors. | Set printBackground: true and add -webkit-print-color-adjust: exact to the relevant print styles; inspect the file. |
| PDF looks different from the browser preview | The PDF uses print CSS, while the preview uses screen CSS. | Inspect @media print; explicitly emulate screen media only if that is the target design. |
| Text wraps differently or content is too small | Paper width, margins, CSS page size, or scaling differ from the design. | Choose a paper format, align CSS @page with API options, and use Puppeteer’s preferCSSPageSize when CSS geometry should win. Avoid using scale as the first fix. |
| Images are absent | The URL is unreachable from the browser process, relative paths have no base URL, or the page was printed before required content loaded. | Use reachable absolute URLs or data URLs, configure a base URL for relative paths, and wait for the required image elements or application readiness condition. |
| Font fallback or unexpected line breaks | A font is unavailable, delayed, or still loading. | Make the font accessible to the renderer, wait for document.fonts.ready where needed, and allow enough time; Puppeteer’s waitForFonts defaults to true. |
| Navigation or PDF call times out | A request remains active, the route is slow, or a font/asset never completes. | Check failed and pending requests; use a readiness signal tailored to the template rather than waiting indefinitely for all network activity. Adjust timeouts only when the workload warrants it. |
| Content overlaps or splits awkwardly | Continuous email layout has no print pagination rules. | Add print rules such as break-inside: avoid to small important blocks and deliberate page breaks to sections; inspect longer inputs. |
| Header/footer is blank or missing page styling | Header/footer templates have separate rendering limitations. | Use supported placeholders and self-contained markup/styles; Playwright documents that page styles are not visible within templates and scripts are not evaluated. |
| Browser process fails in deployment | The browser executable or its runtime dependencies are unavailable or mismatched. | Deploy the browser supported by your automation setup, confirm it can launch in the target environment, and record browser/library versions for repeatable output. |
8. Performance, reliability, and cost
Rendering requires launching or reusing a browser process, loading the template and its assets, waiting for readiness, and producing a PDF. For repeated jobs, reuse a browser process where your service architecture permits, but create and close each page and handle browser restarts. Bound concurrency and timeouts so slow or unusually long templates cannot consume all rendering capacity. These are operational choices; the cited API docs do not provide a universal throughput or cost benchmark.
Keep template inputs and remote assets deterministic when output must be reproducible. A remote image or font can change or become unavailable between runs. Record the template version, relevant inputs, browser automation version, and PDF settings alongside the output when you need to explain a later rendering difference. Treat the browser output as a generated artifact and check that the PDF is non-empty and readable before delivering it.
Your direct cost depends on where browser rendering runs and how you operate it; the sources here do not provide a general price per PDF. Budget for compute, browser deployment, storage, and retries based on your own workload rather than assuming a speed or cost advantage for Puppeteer or Playwright.
Or skip the browser setup
If the document you need is a webpage, ScreenshotNeo can return a PDF through one API call. It is a website screenshot API and MCP server by ScreenshotNeo; its API documentation covers the request options. This is useful for capturing a rendered web page. It does not accept an HTML email template as a direct PDF-conversion input in the facts provided here; publish or render the template at a URL first.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://your-site.example/rendered-email/123 -o email-template.pdf
For API clients that inspect the response and save its bytes, use the same endpoint and URL:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://your-site.example/rendered-email/123", "format": "pdf"},
timeout=90,
)
r.raise_for_status()
open("email-template.pdf", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://your-site.example/rendered-email/123',
format: 'pdf',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo request failed: ${res.status}`);
await require('node:fs/promises').writeFile('email-template.pdf', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Plans include the listed features. Create a free ScreenshotNeo account to get started.
FAQ
Does converting the template to PDF send an email?
No. It renders the template markup into a document; email delivery is a separate operation.
Should I choose Puppeteer or Playwright?
Use the library already supported by your project and deployment. Both have browser PDF APIs; the cited documentation does not establish a universal fidelity or speed winner.
Will this PDF look exactly like the email in an inbox?
That is not guaranteed. These APIs render a browser page as a PDF; they do not promise equivalence with any email client.
Can I return the PDF from an HTTP endpoint instead of saving it?
Yes. Playwright returns a buffer, and Puppeteer returns PDF bytes when no output path is supplied. Your application can send those bytes with an appropriate PDF response type.


