Playwright PDF: page.pdf() and the defaults that break it

Playwright's page.pdf() turns any page into a PDF, but its defaults are not what most people expect. We ran it on Playwright 1.64 and measured four ways the output comes out wrong, then the fix for each. Every snippet below was run as shown.

A working PDF script (Node.js)

Install with npm i playwright and npx playwright install chromium. Save as pdf.mjs:

// pdf.mjs, run with: node pdf.mjs
import { chromium } from "playwright";

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("https://en.wikipedia.org/wiki/PDF", { waitUntil: "networkidle" });
await page.pdf({
  path: "page.pdf",
  format: "A4",               // the default is Letter
  printBackground: true,      // the default drops every background color and image
  margin: { top: "1cm", bottom: "1.5cm", left: "1cm", right: "1cm" },
  displayHeaderFooter: true,
  headerTemplate: "<span></span>", // empty, or Chrome prints the date and title
  footerTemplate: `<div style="font-size:9px;width:100%;text-align:center">
    Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>`,
});
await browser.close();

On the Wikipedia "PDF" article this gives a 24-page A4 file (483 KB) with "Page 2 of 24" in the footer of page 2. Each option in it is there to fix one of the defaults below.

What page.pdf() gets wrong by default

We printed a small invoice page (a dark blue header, a total line and one @media print rule) with each setting and read the PDFs with poppler's pdfinfo, pdftotext -bbox and pdftoppm.

DefaultWhat we measured
Page size is Letter
Fix: format: "A4"
612 × 792 pt (US Letter) when no format is passed, even on a European machine.
Backgrounds dropped
Fix: printBackground: true
The header's pixel was rgb(255, 255, 255) in the default PDF and rgb(30, 58, 138) with the option. White text on a dropped background disappears.
Footer text is ~1 pt tall
Fix: set font-size and width:100% in the template
A footer template with no font size printed glyphs 0.9 pt high at x = 0, stuck in the corner. With font-size:9px;width:100%;text-align:center it was readable and centered.
CSS @page { size } ignored
Fix: preferCSSPageSize: true
A page with @page { size: A5 } came out A4 when format: "A4" was passed and Letter with no format. Only preferCSSPageSize: true gave A5.

Two more things that are easy to miss:

PDF from an HTML string

For invoices and reports you usually have HTML, not a URL. setContent loads it, and emulateMedia makes the PDF match the screen styles:

// html_pdf.mjs: a PDF from an HTML string, styled like the screen
import { chromium } from "playwright";

const html = `<html><body style="font-family:sans-serif">
  <div style="background:#1e3a8a;color:#fff;padding:24px"><h1>Invoice 42</h1></div>
  <p>Total: 120 EUR</p></body></html>`;

const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle" });
await page.emulateMedia({ media: "screen" }); // ignore @media print rules
await page.pdf({ path: "invoice.pdf", format: "A4", printBackground: true });
await browser.close();

Images and fonts in the HTML need absolute URLs (or data URLs), since a setContent page has no base URL. Web fonts that are still loading when page.pdf() runs print in a fallback font: waitUntil: "networkidle" covers most cases, and custom fonts in PDFs has the details.

The same in Python

pip install playwright, then playwright install chromium. The options are the same in snake_case:

# pdf.py, run with: python pdf.py
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://en.wikipedia.org/wiki/PDF", wait_until="networkidle")
    page.pdf(
        path="page.pdf",
        format="A4",
        print_background=True,
        margin={"top": "1cm", "bottom": "1.5cm", "left": "1cm", "right": "1cm"},
        display_header_footer=True,
        header_template="<span></span>",
        footer_template='<div style="font-size:9px;width:100%;text-align:center">'
        'Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
    )
    browser.close()

We ran this unmodified and got the same 24-page A4 file with "Page 2 of 24" on page 2. In asyncio code, use playwright.async_api and await page.pdf(...).

Errors you'll hit

Skip the browser: one HTTP call

If PDFs are a side feature, running Chromium means fonts, system libraries, memory limits and a browser per request in your deployment. Shotmint runs the same Chromium print engine and sets the sensible defaults for you: A4, backgrounds on, 10 mm margins and lazy images scrolled into view before printing.

// api_pdf.mjs, run with: SHOTMINT_KEY=... node api_pdf.mjs
import { writeFileSync } from "node:fs";

const res = await fetch("https://shotmint.creasework.com/v1/pdf", {
  method: "POST",
  headers: { "X-API-Key": process.env.SHOTMINT_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    url: "https://en.wikipedia.org/wiki/PDF", // or html: "<h1>Invoice 42</h1>"
    format: "A4",
    margin: "10mm",
    page_numbers: true,    // centered "1 / 24" footer
  }),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
writeFileSync("page.pdf", Buffer.from(await res.arrayBuffer()));
console.log("credits left:", res.headers.get("x-credits-remaining"));

On the same Wikipedia page this returned the same 24-page A4 PDF in 2.7 s, with "2 / 24" on page 2. The options map to Playwright's: format, landscape, margin (with a unit, e.g. "0mm"), scale, print_background, screen_media (the emulateMedia fix), header_template and footer_template (same placeholders as Playwright), and wait_until. Python, with the standard library only:

# api_pdf.py, run with: SHOTMINT_KEY=... python api_pdf.py
import json, os, urllib.request

req = urllib.request.Request(
    "https://shotmint.creasework.com/v1/pdf",
    data=json.dumps({
        "url": "https://en.wikipedia.org/wiki/PDF",
        "format": "A4",
        "page_numbers": True,
    }).encode(),
    headers={"X-API-Key": os.environ["SHOTMINT_KEY"], "Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=90) as res, open("page.pdf", "wb") as f:
    f.write(res.read())

Related: Playwright screenshots, HTML to PDF in Node.js, HTML to PDF in Python, free HTML to PDF converter, API reference.

What it costs

One PDF = one credit, however many pages it has. €5 buys 2,000 renders, credits never expire and there's no subscription.

Try it with 25 free renders a month

No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire). From a script, an AI agent, Make or Zapier, POST /v1/keys/free (no body) returns a key as JSON (openapi.json, no-code recipes).

Get a free API key Full API reference

More guides: Headless browser how-tos

All guides: guides · integrations · comparisons