ScreenshotNeo

BlogHTML to image & PDF

How to Generate a PDF From a New Puppeteer Page

Create a Puppeteer page, render a URL or HTML, and generate a reliable PDF with the right media, layout, fonts, ranges, and troubleshooting settings.

By the ScreenshotNeo team30 September 20269 min read

How to Generate a PDF From a New Puppeteer Page

To generate a PDF from a new Puppeteer page, launch a browser, create a page with await browser.newPage(), load a URL or set HTML content, and call await page.pdf(). Pass path to write a file, or omit it to receive PDF bytes in memory. Always close the browser in a finally block.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'networkidle2' });
  await page.pdf({ path: 'output.pdf' });
} finally {
  await browser.close();
}

This is the documented Puppeteer workflow: PDF generation guide, Browser.newPage(), and Page.pdf().

1. Set up Puppeteer

Install Puppeteer in a Node.js project. The package downloads a compatible browser during installation unless you use a separately managed browser.

npm install puppeteer

Use an ES module file such as generate-pdf.mjs, or set "type": "module" in package.json. Run it with:

node generate-pdf.mjs

2. Generate a PDF from a URL

browser.newPage() creates a Page in the browser’s default context. Navigate it, wait for the page state you need, and then print it.

A new Puppeteer page moves through navigation and rendering before PDF generation.
A new Puppeteer page moves through navigation and rendering before PDF generation.
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 900, deviceScaleFactor: 1 });
  await page.goto('https://example.com', {
    waitUntil: 'networkidle2',
    timeout: 60_000
  });

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

networkidle2 waits until there are no more than two active network connections. It is useful for ordinary pages, but applications that poll, stream, or keep analytics connections open may never reach the state you expect. In those cases, wait for a specific selector or use a controlled delay instead.

3. Generate a PDF from HTML

When your application creates the document itself, use page.setContent() rather than navigating to a URL. Include a complete HTML document and wait for fonts or other asynchronous assets before printing.

import puppeteer from 'puppeteer';

const html = `<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      @page { size: A4; margin: 18mm; }
      body { font-family: Arial, sans-serif; color: #222; }
      h1 { margin: 0 0 12px; }
      .total { page-break-inside: avoid; }
    </style>
  </head>
  <body>
    <h1>Invoice 1042</h1>
    <p>Prepared for Example Ltd.</p>
    <div class="total">Total: $240.00</div>
  </body>
</html>`;

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setContent(html, { waitUntil: 'domcontentloaded' });
  await page.evaluate(() => document.fonts.ready);
  await page.pdf({ path: 'invoice.pdf', format: 'A4', printBackground: true });
} finally {
  await browser.close();
}

For HTML that references local files, serve those files over a local HTTP server or use carefully controlled file: URLs. Check your deployment’s Chromium sandbox and file access policy before allowing untrusted HTML.

4. Understand the important PDF options

Option What it controls Default or detail
path File destination Omit it to keep the returned Uint8Array in memory.
format Paper preset such as A4 or Letter Letter is the documented default. If set, it takes priority over width and height.
width, height Custom paper dimensions Use CSS units such as mm, in, or px.
preferCSSPageSize Whether CSS @page size wins Defaults to false. Set true when the document defines its own page size.
margin Top, right, bottom, and left margins Accepts CSS lengths or an object with four sides.
landscape Orientation Defaults to false (portrait).
printBackground Background colors and images Defaults to false. Set true for designed reports.
scale Rendered size Adjust carefully; scaling changes line wrapping and pagination.
pageRanges Pages to include Examples include 1-5, 8, 11-13.
displayHeaderFooter Header and footer templates Use with the documented template variables such as page number and total pages.
waitForFonts Font readiness before printing Defaults to true in the current options reference.
tagged, outline Accessibility tagging and document outline Documented as experimental options; verify output for your PDF consumers.

See the complete PDFOptions reference for the exact types and supported values.

5. Control print and screen appearance

Puppeteer generates PDFs using print CSS media by default. If your site has a screen layout that should be printed, switch media before calling pdf():

await page.emulateMediaType('screen');
await page.pdf({ path: 'screen-styled.pdf', printBackground: true });

For ordinary print styles, leave the default in place. Printing also modifies colors for paper by default. Request exact colors in your stylesheet where appropriate:

* {
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

Use printBackground: true for charts, colored headers, and shaded table rows. Background graphics increase output size, so leave it disabled for text-only documents.

6. Wait for the page to be ready

Navigation completion does not guarantee that an application has finished rendering. Pick a readiness signal that matches the page:

  • DOM ready: waitUntil: 'domcontentloaded' for static HTML.
  • Network quiet: waitUntil: 'networkidle2' for pages whose data requests finish.
  • Application marker: await page.waitForSelector('[data-pdf-ready]').
  • Known delay: await new Promise(resolve => setTimeout(resolve, 1000)) for animation or delayed rendering that has no marker.
  • Fonts: await page.evaluate(() => document.fonts.ready); page.pdf() also waits for fonts by default.
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.waitForSelector('#report-loaded', { timeout: 30_000 });
await page.evaluate(() => document.fonts.ready);
await page.pdf({ path: 'report.pdf', format: 'A4' });

Disable transitions and animations when they can capture an intermediate state:

await page.addStyleTag({
  content: `*, *::before, *::after {
    animation: none !important;
    transition: none !important;
  }`
});

7. Handle headers, footers, and page breaks

Headers and footers are HTML templates. They render in a separate context, so include styles directly in the template and reserve enough margin.

await page.pdf({
  path: 'report.pdf',
  format: 'A4',
  displayHeaderFooter: true,
  headerTemplate: '<div style="font-size:8px;width:100%;text-align:center">Quarterly report</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: '24mm', bottom: '20mm', left: '14mm', right: '14mm' }
});

Use CSS to keep related content together:

.keep-together { break-inside: avoid; page-break-inside: avoid; }
.page-break { break-before: page; page-break-before: always; }
thead { display: table-header-group; }

Long unbreakable URLs, wide tables, and oversized images can still force unexpected pages. Give table cells a break strategy, constrain images with max-width: 100%, and test with realistic data.

8. Return bytes, stream output, or save a file

page.pdf() resolves to a Uint8Array. This is useful for an HTTP response or object storage upload:

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

For incremental consumption, Puppeteer provides page.createPDFStream(options), which returns a ReadableStream<Uint8Array>. This can reduce the need to hold a large document in one application buffer. The stream API and its supported options are documented in the Page.createPDFStream reference.

9. cURL, Python, and Node.js alternatives with ScreenshotNeo

If you need a PDF from a URL without managing Chromium, an API can handle navigation and rendering. ScreenshotNeo is a website screenshot API and MCP server. It supports PDF output, paper size, margins, landscape mode, page ranges, custom CSS and JavaScript, waiting rules, headers, cookies, authentication, timezone, geolocation, caching, and bulk capture. The API also accepts parameter names used by other screenshot services, which can simplify migration.

Consent banners and overlays can change what a capture contains.
Consent banners and overlays can change what a capture contains.

Or skip the browser setup

Use the PDF options supported by the API alongside the URL and your access key. The full parameter list is in the ScreenshotNeo documentation.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={
        "access_key": "YOUR_API_KEY",
        "url": "https://stripe.com",
        "format": "pdf",
        "paper_size": "A4"
    },
    timeout=90
)
r.raise_for_status()
open("output.pdf", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com',
  format: 'pdf',
  paper_size: 'A4'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const pdf = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('output.pdf', pdf));

Cookie banners, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the request was billed. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

10. Troubleshooting Puppeteer PDF generation

Symptom Likely cause Fix
PDF is blank Content is rendered after navigation resolves, or the page is blocked. Wait for an application selector, inspect the response, and capture only after content exists.
Missing colors or images Print backgrounds are disabled. Set printBackground: true; ensure assets are reachable from the browser.
Wrong layout Print CSS differs from screen CSS. Use emulateMediaType('screen') or add explicit print styles.
Fonts look wrong Font files failed, CORS blocked them, or capture happened too early. Check browser requests, host fonts correctly, and await document.fonts.ready.
Extra or missing pages Margins, scale, paper size, or unbreakable content changed pagination. Set one paper strategy, adjust margins and scale, and add break rules.
Navigation timeout Long polling, slow resources, or a page that never becomes idle. Use domcontentloaded plus a selector, increase timeout, or block unnecessary resources.
Browser fails in production Missing Chromium dependencies, sandbox restrictions, or an incorrect executable path. Install the required runtime packages, use the deployment’s supported launch flags, and configure executablePath only when managing Chromium yourself.
BiDi option error WebDriver BiDi exposes fewer PDF options. Over BiDi, rely on the documented subset: format, height, landscape, margin, pageRanges, printBackground, scale, and width.

11. Performance, reliability, and cost considerations

  • Reuse a browser: Launching Chromium is expensive. Keep one browser process and create or close pages per job, while isolating jobs that handle sensitive cookies.
  • Bound every wait: Set navigation, selector, and PDF timeouts. A page with an open WebSocket should not hold a worker forever.
  • Control resources: Block trackers, video, and other nonessential requests when they do not affect the document. This reduces work but can change layout if a blocked resource is required.
  • Limit concurrency: Each page consumes memory, especially for full reports and large images. Use a queue and measure memory in your own deployment.
  • Make jobs repeatable: Pin your Puppeteer version, set timezone and locale where relevant, wait for a deterministic marker, and disable animations.
  • Validate output: Check that the result begins with a PDF signature, has nonzero length, and contains expected page count or text before publishing it.
  • Cost: Self-hosting consumes compute and browser operations. ScreenshotNeo charges only for clean shots; bot checks, blank pages, timeouts, failed loads, and cache hits are free, with the verdict and billing status in response headers.

12. A production-ready complete example

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

const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
  await page.waitForNetworkIdle({ idleTime: 500, timeout: 30_000 }).catch(() => {});
  await page.evaluate(() => document.fonts.ready);
  await page.addStyleTag({ content: '* { animation: none !important; transition: none !important; }' });

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

The exact readiness signal should be adapted to the application. A fixed timeout alone is less reliable than a marker emitted when the report has finished rendering.

FAQ

Does page.pdf() create a PDF from the current page?

Yes. Navigate or set content first, then call it. It returns PDF bytes and can write directly to a path.

Why does my screen design change in the PDF?

PDF generation uses print media by default. Call page.emulateMediaType('screen') when screen styles are the intended output.

Should I use format or CSS @page?

Use format for a predictable preset. Set preferCSSPageSize: true when the document’s CSS defines the paper size.

Can I generate only selected pages?

Yes. Set pageRanges, for example 1-3, 7.

Can an AI agent create the PDF?

Yes. ScreenshotNeo’s MCP server exposes a capture_pdf tool for MCP clients such as Claude and Cursor.