ScreenshotNeo

BlogHTML to image & PDF

Tips for Generating PDFs with Puppeteer

Generate reliable PDFs with Puppeteer by choosing the right media type, page size, print CSS, and readiness checks. Includes runnable code and fixes for common layout problems.

By the ScreenshotNeo team4 October 20269 min read

page.pdf() is Puppeteer’s documented method for printing a page to PDF. A dependable result depends on more than calling it: wait for the page’s actual content, decide whether the PDF should use print or screen styles, and configure paper size, margins, and background graphics deliberately. [Puppeteer’s PDF guide](https://pptr.dev/guides/pdf-generation) covers the basic workflow.

1. Install Puppeteer and generate a basic PDF

In a new Node.js project, install Puppeteer. The puppeteer package downloads a compatible browser as part of its installation workflow; follow the [official installation guide](https://pptr.dev/guides/installation) for your environment.

npm init -y
npm install puppeteer

Save this as make-pdf.mjs and run it with node make-pdf.mjs. It navigates to a page, waits for network activity to settle, writes a PDF, and closes the browser even if the operation fails.

import puppeteer from 'puppeteer';

const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch();

try {
  const page = await browser.newPage();
  await page.goto(url, {
    waitUntil: 'networkidle2',
    timeout: 45_000,
  });

  await page.pdf({
    path: 'page.pdf',
    format: 'A4',
    printBackground: true,
    preferCSSPageSize: true,
    margin: { top: '16mm', right: '14mm', bottom: '16mm', left: '14mm' },
  });
} finally {
  await browser.close();
}

Run it against another page with node make-pdf.mjs https://example.com/docs. The networkidle2 wait is a useful starting point, not proof that a single-page application has finished loading its data. Add an application-specific wait when necessary.

2. Wait for the content you intend to print

Navigation completion and application readiness are different conditions. A page can finish loading while API data, client-rendered components, charts, or images are still pending. Puppeteer’s PDF options wait for fonts by default, but that does not wait for your app’s data to appear. [The PDF API reference](https://pptr.dev/api/puppeteer.pdfoptions) documents waitForFonts; the [PDF guide](https://pptr.dev/guides/pdf-generation) uses networkidle2 in its example.

Prefer a selector or explicit application signal that means the printable content is ready:

await page.goto('https://example.com/report', {
  waitUntil: 'domcontentloaded',
  timeout: 45_000,
});

// Replace this selector with an element that appears after report data renders.
await page.waitForSelector('[data-report-ready="true"]', {
  timeout: 20_000,
});

// If the page uses web fonts, this is also available explicitly.
await page.evaluate(() => document.fonts.ready);

await page.pdf({ path: 'report.pdf', format: 'A4', printBackground: true });

A fixed delay can cover a known short animation or transition, but it is less reliable than waiting for an observable condition. If there is no suitable selector, expose a stable readiness marker in the page or check the specific data state your application controls.

3. Choose print or screen styling

page.pdf() uses the print CSS media type. That means @media print rules apply, and responsive or print-specific layout can differ from the browser window. To render with screen styles, call page.emulateMediaType('screen') before generating the PDF. Puppeteer notes that print output may modify colors; CSS -webkit-print-color-adjust can request exact colors. See the [Page API](https://pptr.dev/api/puppeteer.page).

// Use print styles (the default).
const printPdf = await page.pdf({ format: 'A4' });

// Or use screen styles for a browser-like rendering.
await page.emulateMediaType('screen');
const screenPdf = await page.pdf({ format: 'A4' });

Usually, print media is the better document output: it lets the site hide navigation and controls, adjust line lengths, and avoid splitting key components. Use screen media when matching the on-screen composition is more important than print-specific layout.

4. Set paper size, margins, and page breaks with CSS

You can set the paper size through Puppeteer’s format, width, and height options, or through CSS @page. If both are present, preferCSSPageSize: true gives the CSS page size priority. Otherwise, Puppeteer scales content to fit the API-selected paper dimensions. format takes priority over width and height; the default format is Letter. [The PDFOptions reference](https://pptr.dev/api/puppeteer.pdfoptions) lists the controls and defaults.

await page.addStyleTag({ content: `
  @page {
    size: A4 portrait;
    margin: 16mm 14mm;
  }

  @media print {
    .site-nav, .cookie-banner, .print-hidden { display: none !important; }
    h1, h2, h3 { break-after: avoid; }
    figure, table, .keep-together { break-inside: avoid; }
    a { color: inherit; }
  }
` });

await page.pdf({
  path: 'report.pdf',
  preferCSSPageSize: true,
  printBackground: true,
});

Use either CSS margins or the API’s margin option as the source of truth for the printable layout. If you set both, review the output to ensure their interaction matches your intended page geometry. The API accepts margin values with units such as mm, cm, in, or px.

5. Configure the PDF options that affect the output

Option What it controls Practical note
format Standard paper format; default is Letter. Overrides width and height when set.
width, height Custom paper dimensions. Use a number or a string with units.
landscape Landscape orientation; default is false. Useful for wide tables and dashboards.
margin Top, right, bottom, and left page margins. Defaults to no margins in the API.
preferCSSPageSize Whether CSS @page sizing takes priority. Defaults to false; content is otherwise scaled to the chosen paper size.
printBackground Whether background graphics print. Defaults to false.
scale Rendering scale. Range is 0.1–2; default is 1. Reduce only if content must fit and CSS cannot solve the layout.
pageRanges Pages to include, such as 1-3, 7. An empty string means all pages.
displayHeaderFooter Whether to show print headers and footers. Defaults to false; templates support injected date, title, URL, page number, and total pages.
headerTemplate, footerTemplate HTML for the header and footer. Use the documented classes such as pageNumber and totalPages for injected values.
omitBackground Hides the default white page background. Can allow transparent PDF output.
waitForFonts Waits for document.fonts.ready. Defaults to true. A background page may need to be brought to the front.
timeout PDF generation timeout in milliseconds. Defaults to 30,000; 0 disables it.
tagged, outline Experimental tagged PDF or document outline generation. Confirm support and behavior for your installed Puppeteer version before relying on them.
path File destination. Without it, page.pdf() returns PDF bytes instead of writing to disk.

Header and footer templates have special constraints. Keep their markup self-contained, and use the supported placeholder classes instead of assuming arbitrary page CSS will style them.

await page.pdf({
  path: 'invoice.pdf',
  format: 'A4',
  displayHeaderFooter: true,
  headerTemplate: '<div style="font-size:8px;width:100%;text-align:center"><span class="title"></span></div>',
  footerTemplate: '<div style="font-size:8px;width:100%;text-align:center">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
  margin: { top: '18mm', bottom: '18mm', left: '12mm', right: '12mm' },
});

6. Return bytes, stream a PDF, or serve it over HTTP

page.pdf() returns a Uint8Array. You can save it yourself, send it as a response, or use page.createPDFStream() when a readable stream suits the surrounding code. The [Page API](https://pptr.dev/api/puppeteer.page) documents both methods.

import { writeFile } from 'node:fs/promises';

const bytes = await page.pdf({ format: 'A4', printBackground: true });
await writeFile('output.pdf', bytes);

const stream = await page.createPDFStream({ format: 'A4' });
for await (const chunk of stream) {
  // Pipe chunks to a writable destination in a server or storage workflow.
}

In an HTTP handler, set the response content type to application/pdf and either send the returned bytes or pipe a stream to the response. Make sure the browser is closed and the response lifecycle is handled if PDF generation throws.

7. Troubleshoot common PDF problems

Symptom Likely cause Fix
Content is missing or only partly rendered. Navigation finished before app data or client rendering completed. Wait for an app-specific selector or readiness signal. Do not rely on a generic network-idle condition for every app.
Fonts look wrong or fall back. Font files have not loaded, are blocked, or document.fonts.ready has not resolved. Keep waitForFonts: true (the default), check font requests, and wait for the app’s fonts before printing. Bring a background page to the front if needed.
Background colors or images are absent. printBackground defaults to false. Set printBackground: true. For color fidelity, apply -webkit-print-color-adjust: exact to the relevant print styles.
PDF layout differs from the browser. PDF generation uses print media by default and print CSS may alter layout. Inspect @media print. Call page.emulateMediaType('screen') first only when screen styling is desired.
Pages are unexpectedly scaled or clipped. CSS @page and API paper dimensions conflict, or content exceeds the printable area. Choose one page-size source, set preferCSSPageSize intentionally, and adjust margins or layout.
Rows, headings, or cards split awkwardly. Print layout does not specify break behavior. Use print CSS such as break-inside: avoid for appropriate blocks and break-after: avoid for headings. Test long content because a block taller than one page must still break.
PDF generation times out. The page or PDF operation exceeds its timeout, perhaps due to slow resources or oversized content. Wait on a meaningful readiness condition, investigate slow dependencies, and adjust navigation or PDF timeouts for the expected workload. Setting timeout: 0 disables the PDF timeout and can leave work hanging indefinitely.
Browser launch fails on a server. Browser dependencies, executable path, permissions, or deployment setup differ from development. Use Puppeteer’s bundled browser for the supported pairing and follow its installation guidance for the runtime. A custom executable path is your responsibility.
Repeated jobs exhaust resources. Browser instances or pages are not being closed, or concurrent rendering exceeds available memory. Close pages and browsers in finally blocks, limit concurrency, and monitor memory and job duration.

8. Reliability, performance, and cost considerations

  • Use a consistent browser pairing. Puppeteer guarantees compatibility with its bundled browser. The [launch options reference](https://pptr.dev/api/puppeteer.launchoptions) warns that a custom executable path is used at your own risk. Pin your Puppeteer dependency and document browser/runtime versions for repeatable output.
  • Bound each stage. Set navigation and PDF timeouts to match the job, and use an application readiness check. A timeout of zero disables the PDF timeout; it is not a general reliability improvement.
  • Control concurrency. Browser rendering consumes resources. Limit simultaneous jobs based on your deployment capacity, reuse infrastructure thoughtfully, and close resources after failures as well as successes.
  • Keep PDFs appropriately sized. Avoid loading assets the document does not need, remove unnecessary print-only content, and use print CSS to control layout. Large pages and high-resolution assets can increase generation time and output size.
  • Account for variable page content. A remote site may change, fail to load, or serve content conditionally. If PDFs are business records, capture the source and generation metadata you need for auditing, and handle retries deliberately.
  • Budget infrastructure, not just code. Self-hosting means managing the browser runtime and the compute, memory, storage, and operational work required for your PDF volume. Puppeteer’s documentation does not publish a universal per-PDF cost or performance benchmark; measure your own pages and deployment.

9. Or skip the browser setup

If you need a PDF from a URL without operating Puppeteer, [ScreenshotNeo](https://screenshotneo.com) provides a screenshot API and MCP server. The API supports PDF output; see the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/) for request options.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -d format=pdf \
  -o page.pdf

Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. [Create a free ScreenshotNeo account](https://screenshotneo.com/account/sign-up/).

10. FAQ

Can Puppeteer create a PDF from HTML that is not hosted at a URL?

Yes. Set page content with page.setContent(html), wait for any required resources or application logic, then call page.pdf().

Does Puppeteer’s PDF method return a file path?

No. With path, Puppeteer writes the file there. Without it, the method returns PDF bytes as a Uint8Array.

Can I create just selected pages?

Yes. Set pageRanges, for example '1-3, 7'. Leave it empty to print all pages.

Should I use puppeteer or puppeteer-core?

Use the [official installation guide](https://pptr.dev/guides/installation) to choose the package for your browser setup. The key compatibility point is to use a browser version supported by your Puppeteer configuration.