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.
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.
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.
| Default | What 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 } ignoredFix: 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:
page.pdf() renders with @media print rules, so anything a site hides for print (nav bars, "screen only" blocks, sometimes whole sections) is gone. Call await page.emulateMedia({ media: "screen" }) first to get what you see in the browser.headerTemplate when you only want a footer, otherwise Chrome prints its default date and title line. The templates can't load images or stylesheets: inline everything. More in page numbers, headers and footers.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.
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(...).
PDF generation is only supported for Headless Chromium: page.pdf() is Chromium-only, so it throws on firefox and webkit pages. Launch with chromium.launch(), even if your tests use another browser.libnss3 and friends): run npx playwright install-deps chromium, or start from the mcr.microsoft.com/playwright image. Playwright already launches Chromium with --no-sandbox unless you set chromiumSandbox: true.break-inside: avoid (see page breaks).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.
One PDF = one credit, however many pages it has. €5 buys 2,000 renders, credits never expire and there's no subscription.
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).
All guides: guides · integrations · comparisons