Basic Page-to-PDF
The simplest Playwright PDF generation takes a URL and outputs a file. The page.pdf() method only works with Chromium in headless mode — Firefox and WebKit don't support it.
const { chromium } = require('playwright');
async function generatePDF(url, outputPath) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle' });
await page.pdf({
path: outputPath,
format: 'A4',
printBackground: true,
margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' }
});
await browser.close();
console.log(`PDF saved to ${outputPath}`);
}
generatePDF('https://example.com', 'output.pdf');
Key options: format accepts standard sizes (A4, Letter, Legal, Tabloid), printBackground must be true to capture CSS backgrounds, and margin takes CSS units. You can also use width/height instead of format for custom dimensions.
Custom Headers and Footers
Playwright supports HTML templates for PDF headers and footers. The templates run in an isolated context with special CSS classes that auto-populate page data.
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
displayHeaderFooter: true,
headerTemplate: `
<div style="font-size:10px; width:100%; padding:5px 20px;
display:flex; justify-content:space-between;">
<span>Acme Corp — Confidential</span>
<span class="date"></span>
</div>`,
footerTemplate: `
<div style="font-size:9px; width:100%; text-align:center;
padding:5px 0;">
Page <span class="pageNumber"></span>
of <span class="totalPages"></span>
</div>`,
margin: { top: '2cm', bottom: '2cm', left: '1cm', right: '1cm' }
});
Available template classes: .date (formatted print date), .title (document title), .url (page URL), .pageNumber, and .totalPages. Important: header/footer templates have their own isolated CSS scope — your page styles don't apply. You must inline all styles directly. Increase top/bottom margins to make room for the header and footer content.
Print-Optimized CSS
Control exactly what appears in the PDF using @media print rules. This is how you hide navigation, sidebars, and ads while controlling page breaks for clean output.
@media print {
/* Hide non-content elements */
nav, footer, .sidebar, .ads, .cookie-banner {
display: none !important;
}
/* Prevent awkward page breaks */
h1, h2, h3 {
page-break-after: avoid;
}
table, figure, pre {
page-break-inside: avoid;
}
/* Force page breaks */
.chapter {
page-break-before: always;
}
/* Typography for print */
body {
font-size: 12pt;
line-height: 1.5;
color: #000;
background: #fff;
}
}
Inject print CSS into any page before generating the PDF:
await page.addStyleTag({
content: `
@media print {
nav, .cookie-popup, .chat-widget { display: none !important; }
.content { max-width: 100%; margin: 0; }
}
`
});
await page.pdf({ path: 'clean.pdf', format: 'A4', printBackground: true });
HTML Template to PDF
Generate PDFs from raw HTML strings instead of URLs. This is perfect for invoices, reports, and certificates where you control the full markup.
async function invoiceToPDF(invoiceData) {
const browser = await chromium.launch();
const page = await browser.newPage();
const html = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: 'Helvetica', sans-serif; padding: 40px; }
.header { display: flex; justify-content: space-between; margin-bottom: 40px; }
.logo { font-size: 24px; font-weight: bold; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
.total { font-size: 20px; text-align: right; margin-top: 20px; }
</style>
</head>
<body>
<div class="header">
<div class="logo">${invoiceData.company}</div>
<div>Invoice #${invoiceData.number}<br/>${invoiceData.date}</div>
</div>
<table>
<tr><th>Item</th><th>Qty</th><th>Price</th></tr>
${invoiceData.items.map(i =>
`<tr><td>${i.name}</td><td>${i.qty}</td><td>$${i.price}</td></tr>`
).join('')}
</table>
<div class="total">Total: $${invoiceData.total}</div>
</body>
</html>`;
await page.setContent(html, { waitUntil: 'networkidle' });
const pdfBuffer = await page.pdf({ format: 'A4', printBackground: true });
await browser.close();
return pdfBuffer;
}
Express PDF Endpoint
Build a production PDF generation API with browser reuse. Launching a browser per request is slow (~500ms) — keep a persistent instance and create pages on demand.
const express = require('express');
const { chromium } = require('playwright');
const app = express();
app.use(express.json());
let browser;
async function getBrowser() {
if (!browser || !browser.isConnected()) {
browser = await chromium.launch({
args: ['--disable-dev-shm-usage', '--no-sandbox']
});
}
return browser;
}
app.post('/api/pdf', async (req, res) => {
const { url, html, format = 'A4', landscape = false } = req.body;
if (!url && !html) return res.status(400).json({ error: 'url or html required' });
const b = await getBrowser();
const page = await b.newPage();
try {
if (html) {
await page.setContent(html, { waitUntil: 'networkidle' });
} else {
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
}
const pdfBuffer = await page.pdf({
format,
landscape,
printBackground: true,
margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' }
});
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="generated.pdf"'
});
res.send(pdfBuffer);
} catch (err) {
res.status(500).json({ error: err.message });
} finally {
await page.close();
}
});
app.listen(3000, () => console.log('PDF API on :3000'));
For production, add request queuing (BullMQ), timeouts, memory limits, and health checks. Browser instances leak memory over time — restart them every ~100 pages or implement a pool with generic-pool.
Easier: SnapAPI PDF Endpoint
Skip the infrastructure entirely. SnapAPI handles browser management, queuing, and scaling — you just send a request and get a PDF back.
// Node.js — one API call, no browser needed
const response = await fetch('https://api.snapapi.pics/v1/pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': 'sk_live_your_key'
},
body: JSON.stringify({
url: 'https://example.com/report',
format: 'A4',
printBackground: true,
margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' }
})
});
const pdf = await response.arrayBuffer();
fs.writeFileSync('report.pdf', Buffer.from(pdf));
# Python — same simplicity
import httpx
resp = httpx.post(
'https://api.snapapi.pics/v1/pdf',
headers={'X-Api-Key': 'sk_live_your_key'},
json={
'url': 'https://example.com/report',
'format': 'A4',
'printBackground': True,
'displayHeaderFooter': True,
'headerTemplate': '<div style="font-size:10px;width:100%;text-align:center;">Report</div>',
'footerTemplate': '<div style="font-size:9px;width:100%;text-align:center;">Page <span class="pageNumber"></span></div>'
},
timeout=30
)
with open('report.pdf', 'wb') as f:
f.write(resp.content)
SnapAPI supports all Playwright PDF options — format, margins, headers/footers, landscape, page ranges, scale — plus extras like ad blocking, cookie banners removal, and custom CSS/JS injection before PDF generation. The full API docs cover every parameter.
Advanced Tips
- Wait for fonts: Use
await page.evaluateHandle('document.fonts.ready')before generating — web fonts load async and missing fonts break layouts. - Emulate screen media: Call
await page.emulateMedia({ media: 'screen' })to capture screen styles instead of print styles — useful when the page looks better in screen mode. - Page ranges: Use
pageRanges: '1-3,5'to output specific pages only — great for extracting sections from long documents. - Scale: Set
scale: 0.8to fit more content per page, orscale: 1.2to zoom in. Range is 0.1 to 2.0. - Prefer tagged PDF: Set
tagged: truefor accessibility — adds document structure tags for screen readers. - Buffer vs file: Omit the
pathoption to get a Buffer returned instead of writing to disk — better for API responses and streams.