Puppeteer HTML to PDF: page.pdf() guide
Puppeteer HTML to PDF with page.pdf(): options, margins, headers, footers, page breaks, Docker fixes, or skip the browser with an API.
Quick answer. In Puppeteer, load your HTML with page.setContent(html, { waitUntil: "networkidle0" }) and call page.pdf({ format: "A4", printBackground: true }). To get the same Chromium output without installing a browser, send the HTML to an API instead.
curl -X POST https://html-pdf.com/v1/pdf \
-H "Authorization: Bearer $HTML_PDF_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "<h1>Invoice #042</h1>", "format": "A4"}' \
-o invoice.pdf
Puppeteer drives headless Chrome from Node.js, and page.pdf() is Chrome's own "Save as PDF" exposed as an API. That makes it the most common way to turn HTML into a PDF in JavaScript.
This guide shows the full DIY setup, the options that matter and the problems you hit in production. It ends with a way to get the same Chromium output without running a browser yourself.
Install Puppeteer
One npm package gets you started.
npm install puppeteer
The puppeteer package downloads a compatible Chrome build on install. If you already have Chrome on the machine, use puppeteer-core and pass executablePath instead.
Convert an HTML string to PDF
This script turns an HTML string into an A4 PDF file.
// pdf.mjs (run with: node pdf.mjs)
import puppeteer from "puppeteer";
const html = `
<html>
<head><style>
body { font-family: system-ui, sans-serif; margin: 0 }
h1 { color: #1f2937 }
</style></head>
<body><h1>Invoice #042</h1><p>Total: $120.00</p></body>
</html>`;
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle0" });
await page.pdf({
path: "invoice.pdf",
format: "A4",
printBackground: true,
margin: { top: "15mm", right: "15mm", bottom: "15mm", left: "15mm" },
});
} finally {
await browser.close();
}
waitUntil: "networkidle0" waits until images and web fonts referenced by the HTML have loaded. Without it you can get a PDF with missing images or fallback fonts.
Convert a URL to PDF
To print a live page, navigate to it first and then call page.pdf().
await page.goto("https://example.com", { waitUntil: "networkidle0" });
await page.emulateMediaType("screen"); // use screen CSS instead of @media print
const pdf = await page.pdf({ format: "Letter", printBackground: true }); // returns a buffer if no path
Some pages render content with JavaScript after load, such as charts and dashboards. Wait for that content explicitly, for example with await page.waitForFunction("window.chartsReady === true").
The page.pdf() options you will actually use
These are the options that cover almost every real document.
| Option | What it does |
|---|---|
format |
Paper size: A4, Letter, Legal, A3, and others |
width / height |
Custom size, e.g. "210mm" |
landscape |
Rotate the page |
margin |
{ top, right, bottom, left } |
printBackground |
Include background colors and images (off by default) |
preferCSSPageSize |
Let @page { size: ... } in your CSS win |
scale |
Zoom the rendering, from 0.1 to 2 |
pageRanges |
Only output some pages, e.g. "1-3" |
displayHeaderFooter, headerTemplate, footerTemplate |
Repeated header and footer |
Headers, footers and page numbers
This section shows how to repeat a footer with page numbers on every page. Chrome fills special classes inside the templates, namely pageNumber, totalPages, date, title and url.
await page.pdf({
path: "report.pdf",
format: "A4",
displayHeaderFooter: true,
headerTemplate: "<span></span>",
footerTemplate: `
<div style="font-size:9px; width:100%; text-align:center;">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>`,
margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },
});
Watch for two gotchas. The templates don't inherit your page's CSS, so style them inline and set a font size or the text comes out tiny. They also need a top or bottom margin to have room.
More in headers, footers and page numbers.
Page breaks
You control pagination with CSS, not with Puppeteer options.
.page { break-after: page; }
tr, .card { break-inside: avoid; }
thead { display: table-header-group; } /* repeat table headers on each page */
See HTML to PDF page breaks for the full set of rules.
Problems you hit in production
Here is what tends to go wrong once Puppeteer runs on a server.
- Chrome won't start in Docker. Slim images lack the shared libraries Chrome needs, and containers often need
--no-sandboxor a non-root user. You end up maintaining a custom image. - Serverless size limits. Chrome is too big for many function bundles. On AWS Lambda people use
@sparticuz/chromiumwithpuppeteer-core, and still fight cold starts. - Memory and zombie processes. Each browser uses a lot of RAM, and a crash that skips
browser.close()leaves processes behind. At volume you need a pool or a queue. - Fonts. Servers rarely have the fonts your designers used. Install them in the image or load them with
@font-face. - Security. Rendering user-supplied HTML or URLs in a browser on your network can expose internal services unless you block private addresses.
None of this is hard on its own, but it is infrastructure you now own.
Or skip the server: one API call
This section shows the same PDF made with one HTTP request. html-pdf.com runs headless Chromium for you (via Gotenberg), so the output matches what page.pdf() gives you, without installing Chrome. The options map almost one to one.
import { writeFile } from "node:fs/promises";
const res = await fetch("https://html-pdf.com/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HTML_PDF_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
format: "A4",
print_background: true,
margin: "15mm",
footer_html:
'<div style="font-size:9px;width:100%;text-align:center">' +
'Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
wait_for_expression: "document.fonts.status === 'loaded'",
}),
});
if (!res.ok) throw new Error(JSON.stringify(await res.json()));
await writeFile("invoice.pdf", Buffer.from(await res.arrayBuffer()));
| Puppeteer | html-pdf.com |
|---|---|
page.setContent(html) / page.goto(url) |
html / url (public pages) |
printBackground |
print_background (default true) |
preferCSSPageSize, pageRanges, scale |
prefer_css_page_size, page_ranges, scale |
headerTemplate / footerTemplate |
header_html / footer_html |
emulateMediaType("screen") |
media: "screen" |
waitForFunction(...) |
wait_for_expression |
You also get max_pages, which fails a document that runs long without charging you. response: "url" returns a 24-hour download link instead of the file. Private network addresses are blocked, and your HTML isn't stored.
Get a key, then open the link we email you to confirm your address. That adds 100 free credits (1 credit = 1 page).
curl -X POST https://html-pdf.com/v1/signup -H "Content-Type: application/json" -d '{"email": "you@example.com"}'
Keep Puppeteer if you already run browsers at scale, need pages behind a login, or need to click through a page before printing. Use the API when PDF generation is the only reason you run Chrome.
More: HTML to PDF in Node.js · Chrome headless --print-to-pdf · API docs
FAQ
How do I convert HTML to PDF with Puppeteer?
Launch a browser, open a page, load the HTML with page.setContent(html, { waitUntil: "networkidle0" }), then call page.pdf({ path: "out.pdf", format: "A4" }). Close the browser in a finally block so Chrome processes don't pile up.
Why are background colors missing from my Puppeteer PDF?
page.pdf() leaves out backgrounds by default. Pass printBackground: true to keep background colors and images.
How do I add page numbers in Puppeteer?
Set displayHeaderFooter: true and put <span class="pageNumber"></span> and <span class="totalPages"></span> in footerTemplate. Style the template inline and leave a bottom margin so the footer has room.
Can I generate PDFs like Puppeteer without installing Chrome?
Yes. html-pdf.com runs headless Chromium for you, so a POST https://html-pdf.com/v1/pdf request with your HTML returns the same kind of PDF. You get 100 free credits once, and 1 credit is 1 page.
