Most PDF generation tutorials start with "install this PDF library" and then you spend an hour fighting with fonts, CSS support, and layout quirks. There's a simpler path if your content already lives on a web page.
The idea
Instead of rendering HTML to PDF inside your app, render the page in a real browser and print it to PDF. Chrome's DevTools Protocol has Page.printToPDF — same engine that powers "Save as PDF" in your browser, but automated.
Quick setup with Puppeteer
const puppeteer = require('puppeteer');
async function pageToPdf(url, outputPath) {
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle0' });
await page.pdf({
path: outputPath,
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
printBackground: true
});
await browser.close();
}
pageToPdf('https://example.com/invoice/123', 'invoice.pdf');
That's it. Full CSS support, web fonts, flexbox, grid — everything renders exactly as it does in Chrome.
Things that tripped me up
Print stylesheets matter. If the page has @media print rules, they'll apply. This is actually useful — hide navigation, sidebars, cookie banners. But if you're not expecting it, your PDF might look nothing like the page.
@media print {
nav, footer, .cookie-banner { display: none; }
.content { width: 100%; margin: 0; }
}
waitUntil: 'networkidle0' isn't always enough. Pages with lazy-loaded images or JS-rendered charts might need explicit waits:
await page.waitForSelector('.chart-container canvas', { timeout: 5000 })
.catch(() => console.log('chart not found, proceeding anyway'));
I learned this the hard way — generated 200 invoices with blank chart sections before noticing.
Headers and footers. The headerTemplate and footerTemplate options exist but they're fiddly. They use a tiny isolated context with limited CSS. For anything complex, I just bake the header into the page HTML itself.
When this approach makes sense
- Invoices, reports, receipts — anything you already display on a web page
- Generating PDFs from user-created content (blog posts, documentation)
- Any situation where you want the PDF to match the web version pixel-for-pixel
Where it doesn't: high-volume generation (thousands per minute) where the browser overhead adds up, or cases where you need PDF-specific features like form fields or digital signatures.
Running it without managing Chrome
If you don't want to deal with keeping a Chrome instance running on your server — especially in containerized environments where Chromium dependencies are a pain — screenshot APIs like ScreenshotRun handle the browser part. You send a URL, get back a PDF or PNG. Offloads the rendering infrastructure entirely.
One more thing
The page.pdf() method has a scale option. Default is 1. Setting it to 0.8 or 0.9 can help fit content that's slightly too wide for the page. Beats reworking your CSS for print.
await page.pdf({
path: 'output.pdf',
format: 'A4',
scale: 0.85,
printBackground: true
});
Not something I see mentioned often but it's saved me from the "content gets cut off on the right edge" problem more than once.
Top comments (0)