Approaches at a Glance
| Approach | CSS support | JS execution | Binary needed | Best for |
| Playwright | Full | Yes | Chromium (~150 MB) | Pixel-perfect, SPAs |
| Puppeteer | Full | Yes | Chromium (~150 MB) | Same as Playwright |
| PDFKit | None | No | None | Programmatic, dynamic |
| jsPDF | Partial (html2canvas) | Client-side only | None | Browser-side export |
| SnapAPI | Full | Yes | None | Serverless, production API |
Playwright PDF Generation
Playwright's page.pdf() produces PDF from the rendered Chromium output — full CSS, custom fonts, WebGL, everything. This is the gold standard for HTML→PDF fidelity.
npm install playwright
npx playwright install chromium
Basic PDF from URL
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com/invoice', { waitUntil: 'networkidle' });
await page.pdf({
path: 'invoice.pdf',
format: 'A4',
printBackground: true, // includes background colors and images
margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
});
await browser.close();
PDF from HTML string
const html = `
Invoice #1042
Client: Acme Corp
Date: April 4, 2026
Total: $1,200.00
`;
await page.setContent(html, { waitUntil: 'networkidle' });
const pdfBuffer = await page.pdf({ format: 'A4', printBackground: true });
// pdfBuffer is a Buffer — write to file or return from HTTP endpoint
Headers and footers
await page.pdf({
format: 'A4',
printBackground: true,
displayHeaderFooter: true,
headerTemplate: `
ACME CORP — CONFIDENTIAL
`,
footerTemplate: `
Generated
Page of
`,
margin: { top: '40px', bottom: '40px' },
});
Header/footer CSS: The header and footer templates run in an isolated scope — external stylesheets don't apply. Inline all your styles. Use .date, .pageNumber, .totalPages, .title class names which Playwright populates automatically.
Puppeteer PDF Generation
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.goto('https://example.com/report', { waitUntil: 'networkidle2' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
displayHeaderFooter: true,
headerTemplate: '
',
footerTemplate: 'Page /
',
margin: { top: '50px', bottom: '50px', left: '20mm', right: '20mm' },
});
await browser.close();
// pdf is a Buffer
Express PDF download endpoint with browser reuse
import express from 'express';
import puppeteer from 'puppeteer';
const app = express();
let browser;
async function getBrowser() {
if (!browser || !browser.connected) {
browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
}
return browser;
}
app.get('/export/pdf', async (req, res) => {
const { url, filename = 'export.pdf' } = req.query;
if (!url) return res.status(400).json({ error: 'url required' });
const b = await getBrowser();
const page = await b.newPage();
try {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
const pdf = await page.pdf({ format: 'A4', printBackground: true });
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${filename}"`,
'Content-Length': pdf.length,
});
res.send(pdf);
} finally {
await page.close();
}
});
app.listen(3000);
CSS Print Styles for Better PDFs
The most important step for good PDF output is controlling page breaks. Chromium respects standard CSS print media queries:
@media print {
/* Force page break before major sections */
.section { page-break-before: always; }
/* Prevent breaking inside elements */
.invoice-item, .chart, blockquote {
page-break-inside: avoid;
}
/* Avoid orphaned headings */
h1, h2, h3 {
page-break-after: avoid;
}
/* Hide interactive elements */
nav, .sidebar, .chat-widget, button, .no-print {
display: none !important;
}
/* Expand full width (no sidebar) */
.content { width: 100% !important; max-width: none !important; }
/* Ensure backgrounds print */
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
}
Inject print CSS before generating PDF
await page.goto(url, { waitUntil: 'networkidle' });
// Add print overrides without modifying the source HTML
await page.addStyleTag({
content: `
@media print {
nav, .sidebar, .cookie-banner { display: none !important; }
.content { width: 100% !important; }
* { -webkit-print-color-adjust: exact !important; }
}
`
});
await page.pdf({ format: 'A4', printBackground: true });
SnapAPI: PDF Without Chromium Infrastructure
Self-hosting Chromium for PDF generation means managing memory leaks, zombie processes, and binary compatibility. SnapAPI handles this as a service:
// Generate PDF from URL — 3 lines
const res = await fetch('https://api.snapapi.pics/v1/screenshot', {
method: 'POST',
headers: { 'X-Api-Key': process.env.SNAPAPI_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
url: 'https://example.com/invoice',
format: 'pdf',
pdfOptions: {
format: 'A4',
printBackground: true,
displayHeaderFooter: false,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
},
}),
});
const pdfBuffer = Buffer.from(await res.arrayBuffer());
// Or via the SDK
import { SnapAPI } from 'snapapi-js';
const snap = new SnapAPI({ apiKey: process.env.SNAPAPI_KEY });
const { data } = await snap.screenshot({
url: 'https://example.com/report',
format: 'pdf',
waitUntil: 'networkidle',
blockAds: true,
blockCookieBanners: true,
});
// data is a PDF Buffer
Express PDF endpoint using SnapAPI (no Chromium binary)
import express from 'express';
const app = express();
app.get('/pdf', async (req, res) => {
const { url } = req.query;
const snap = await fetch('https://api.snapapi.pics/v1/screenshot', {
method: 'POST',
headers: { 'X-Api-Key': process.env.SNAPAPI_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ url, format: 'pdf' }),
});
const pdf = Buffer.from(await snap.arrayBuffer());
res.set({ 'Content-Type': 'application/pdf', 'Content-Disposition': 'attachment; filename="export.pdf"' });
res.send(pdf);
});
app.listen(3000);
SnapAPI free tier: 200 captures/month, PDF included. No Chromium to install or manage.
Get your API key →