How to Access Page Number and Total Pages in Puppeteer PDFs
Add “Page X of Y” to Puppeteer PDFs with built-in placeholders, correct margins, runnable code, troubleshooting, and production tips.
Use Puppeteer’s built-in pageNumber and totalPages classes in a PDF header or footer template. Enable displayHeaderFooter, add enough print margin for the template, and Puppeteer will substitute the current page and document total while printing.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.5; }
.page { break-after: page; min-height: 900px; }
</style>
</head>
<body>
<section class="page"><h1>Report</h1><p>First page content.</p></section>
<section class="page"><h2>Details</h2><p>Second page content.</p></section>
</body>
</html>
`, { waitUntil: 'load' });
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
displayHeaderFooter: true,
footerTemplate: `
<div style="width:100%; text-align:right; font-size:9px; padding:0 12mm; color:#444;">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>`,
margin: {
top: '15mm',
right: '15mm',
bottom: '20mm',
left: '15mm'
}
});
await browser.close();
The placeholders are inserted by Chromium during PDF printing. They are not JavaScript variables returned by page.pdf(). The PDF call returns the generated byte data (a Uint8Array in current Puppeteer versions) when you omit path.
1. How the placeholders work
Puppeteer’s PDFOptions supports HTML templates for headers and footers. Inside those templates, Chromium recognizes these classes:
| Class | Value inserted during printing |
|---|---|
pageNumber |
Current printed page number |
totalPages |
Total number of pages in the generated document |
date |
Print date |
title |
Document title |
url |
Page URL |
Put the classes in either headerTemplate or footerTemplate. A footer is usually the clearest location for “Page X of Y.” See the Puppeteer PDF options reference for the version installed in your project.
2. Minimal “Page X of Y” example
const pdf = await page.pdf({
format: 'A4',
displayHeaderFooter: true,
footerTemplate: '<div style="width:100%; text-align:center; font-size:10px;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
margin: { bottom: '18mm' }
});
displayHeaderFooter defaults to false, so the template is ignored unless you turn it on.
3. Complete Node.js script
import puppeteer from 'puppeteer';
import { writeFile } from 'node:fs/promises';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setTitle('Invoice report');
await page.setContent(`
<style>
@page { size: A4; }
body { font-family: system-ui, sans-serif; margin: 0; }
h1 { color: #16324f; }
.section { break-after: page; padding: 8mm; }
.section:last-child { break-after: auto; }
</style>
<div class="section"><h1>Invoice report</h1><p>Summary and totals.</p></div>
<div class="section"><h2>Line items</h2><p>Detailed charges.</p></div>
<div class="section"><h2>Notes</h2><p>Payment terms and support details.</p></div>
`, { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
displayHeaderFooter: true,
headerTemplate: '',
footerTemplate: `
<div style="font-family:Arial,sans-serif; width:100%; font-size:9px; color:#555; padding:0 12mm; display:flex; justify-content:space-between;">
<span>Invoice report</span>
<span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
</div>`,
margin: { top: '15mm', right: '12mm', bottom: '22mm', left: '12mm' }
});
await writeFile('invoice-report.pdf', pdf);
} finally {
await browser.close();
}
4. Header versus footer
Both templates support the same placeholders. Choose based on document layout:
- Footer: best for page counters, document names and confidentiality labels.
- Header: useful when the bottom margin is reserved for signatures or totals.
- Both: provide a template for each; keep unused templates visually empty if you need consistent spacing.
5. Options that affect pagination
| Option | Why it matters |
|---|---|
displayHeaderFooter |
Must be true for templates to render. |
headerTemplate, footerTemplate |
HTML containing the placeholders and inline styles. |
margin |
Creates room so headers and footers are not clipped by page content. |
format, width, height |
Change the physical page size and therefore page breaks. |
preferCSSPageSize |
Lets CSS @page size take precedence when enabled. |
pageRanges |
Prints selected pages, such as 1-3 or 2,4; inspect the output if your workflow depends on numbering after selection. |
printBackground |
Includes background colors and images, which can change visual pagination. |
scale |
Changes layout density and may alter where content breaks. |
6. Styling rules for templates
- Use inline CSS. Page templates are isolated from the document’s stylesheet.
- Set an explicit, readable font size; extremely small substituted text can be difficult to see.
- Use a fixed width such as
width:100%and explicit alignment. - Keep template markup simple. Complex layout or external assets can render inconsistently.
- Reserve space with the corresponding top or bottom margin.
7. Common errors and fixes
| Symptom | Cause | Fix |
|---|---|---|
| No footer appears | displayHeaderFooter is false or missing. |
Set displayHeaderFooter: true. |
| Literal class names appear | The class is outside a header/footer template or is misspelled. | Use exactly class="pageNumber" and class="totalPages" inside a template. |
| Footer overlaps content | Bottom margin is too small. | Increase margin.bottom, then inspect the rendered PDF. |
| Numbers are present but invisible | Font size or color is too faint; template width may collapse. | Use a legible size, strong contrast and width:100%. |
| Unexpected page count | Fonts, images, CSS size, scale or late-loading content changed pagination. | Wait for required resources, then generate the PDF; avoid relying on guessed page counts. |
| Header/footer asset is missing | External resources are unavailable to the isolated template. | Prefer inline CSS and data URLs, or remove nonessential assets. |
| Selected pages have surprising labels | pageRanges behavior may not match your desired renumbering. |
Render a sample and verify whether labels should reflect source pages or the selected subset. |
8. Reliable PDF generation checklist
- Navigate or set content with an appropriate wait condition.
- Wait for fonts with
document.fonts.readywhen typography affects layout. - Wait for important images and application data before calling
page.pdf(). - Enable
displayHeaderFooter. - Put both placeholders in a template.
- Allocate header or footer margin.
- Render representative long and short documents in CI or staging.
- Open the resulting PDF and verify clipping, contrast, page breaks and numbering.
9. Performance, reliability and cost considerations
Page numbering is computed during printing, so there is no second pass in your application to count pages. The expensive work is browser startup, page rendering, fonts, images and JavaScript on the source page. Reuse a browser process for batches, create isolated pages per job, and close pages after completion. Set an application timeout around navigation and PDF generation, and retry transient browser failures with a bounded policy.
Page count can change when content, viewport, CSS, fonts or assets change. Treat the generated PDF as the source of truth. If you need deterministic output, pin browser and Puppeteer versions, control page dimensions, wait for all required resources and avoid time-dependent content.
10. Or skip the browser setup
ScreenshotNeo provides a website capture API that can return PNG, JPEG, WebP or PDF from one GET request. It can handle PDF paper size, margins, landscape mode and page ranges while you avoid maintaining a Puppeteer browser.
Cookie banners, newsletter popups and chat widgets are removed before the shot. Bot checks, blank pages and failed loads are never billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server so Claude, Cursor and other MCP clients can call take_screenshot, get_page_info and capture_pdf.
See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The Free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
11. FAQ
Can I read the total page count in JavaScript before the PDF is saved?
No. totalPages is a print-template placeholder. If your application needs a number, inspect the generated PDF with a PDF parser after creation.
Do I need both a header and a footer?
No. Use either template. For “Page X of Y,” a footer alone is sufficient.
Why does the page count differ between machines?
Different Chromium versions, fonts, available resources, CSS and content timing can change line wrapping and page breaks. Pin the environment and wait for layout-critical resources.
Can I add text around the numbers?
Yes. Surround the spans with ordinary template text, such as Page <span class="pageNumber"></span> of <span class="totalPages"></span>.
Does page.pdf() return the page number?
No. It returns PDF bytes when no output path is supplied. The page-number values are rendered inside the PDF template.


