Puppeteer HTML to PDF: page.pdf() guide

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.

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.

This page as markdown · text