Puppeteer PDF Page Numbers with Header and Footer Templates
HTML-to-PDF rendering fails once multi-page layouts, dynamic content, and page numbers are involved. Puppeteer can generate PDFs out of HTML-templates with stable page numbers, but only when its header and footer templates are used correctly.
Following you will find a working approach for dynamic multi-page PDFs with clean page numbers using NodeJS Chromium Puppeteer's header and footer templates.
Common Pitfalls
Typical issues appear when the HTML grows beyond one page. CSS counters are unreliable, JavaScript inside HTML cannot read the total page count, and most templating engines cannot calculate pagination. Puppeteer does support page numbers, but only through its internal template engine, not through the HTML body.
Extracting Templates from the HTML
A predictable approach is to place three elements inside the final HTML:
<template id="pdf-header-template"> ... </template>
<template id="pdf-footer-template"> ... </template>
<script id="pdf-options" type="application/json">{ ... }</script>
The Node.js layer reads their content and forwards them to page.pdf(). This keeps all layout logic in the HTML, independent of the templating engine that generated it.
Minimal HTML Example
<html>
<head>
<script id="pdf-options" type="application/json">
{
"format": "A4",
"preferCSSPageSize": false,
"printBackground": true,
"useHeaderFooter": true,
"margin": { "top": "15mm", "bottom": "15mm" }
}
</script>
</head>
<body>
<p>... here your html-template ... </p>
<template id="pdf-header-template">
<div style="width:100%; font-size:8pt; padding:4mm 10mm; display:flex; justify-content:space-between; color:#666;">
<span>Pantarey GmbH</span>
</div>
</template>
<template id="pdf-footer-template">
<div style="width:100%; font-size:8pt; padding:4mm 10mm; display:flex; justify-content:space-between; color:#666;">
<span>Pantarey GmbH</span>
<span><span class="pageNumber"></span>/<span class="totalPages"></span></span>
</div>
</template>
</body>
</html>
Margins are required. Puppeteer does not allocate space for headers or footers automatically.
Puppeteer exposes two placeholders inside header and footer templates: pageNumber and totalPages. They must appear exactly as <span class="pageNumber"></span> and <span class="totalPages"></span> to be replaced during PDF generation. These values are injected by Chromium during the final rendering pass, which makes them reliable even when the document length is fully dynamic.
User-Defined Templates
The HTML for the PDF is often generated by the end user or by a dynamic template system. Because the layout is not known at development time, the header, footer, and PDF options must be defined inside the HTML itself rather than in the Node.js code. Extracting these elements at runtime ensures that users can control page numbers, margins, and layout without changing the backend. This pattern scales well when multiple tenants or applications render their own documents, and it is also used internally at Pantarey.io to keep HTML-driven PDF generation flexible and isolated from execution logic.
Template Extraction in Puppeteer
const { headerHtml, footerHtml, htmlOptions } = await page.evaluate(() => {
const headerEl = document.getElementById('pdf-header-template');
const footerEl = document.getElementById('pdf-footer-template');
const optionsEl = document.getElementById('pdf-options');
let parsedOptions = {};
if (optionsEl?.textContent) {
try { parsedOptions = JSON.parse(optionsEl.textContent); }
catch { parsedOptions = {}; }
}
return {
headerHtml: headerEl?.innerHTML || "",
footerHtml: footerEl?.innerHTML || "",
htmlOptions: parsedOptions
};
});
Rendering the PDF
const defaults = {
format: "A4",
landscape: false,
preferCSSPageSize: false,
printBackground: false,
useHeaderFooter: false,
margin: { top: "0mm", right: "0mm", bottom: "0mm", left: "0mm" }
};
const options = {
...defaults,
...htmlOptions,
margin: { ...defaults.margin, ...(htmlOptions.margin || {}) }
};
const pdf = await page.pdf({
format: options.preferCSSPageSize ? undefined : options.format,
landscape: options.landscape,
displayHeaderFooter: options.useHeaderFooter,
printBackground: options.printBackground,
headerTemplate: headerHtml || "<div></div>",
footerTemplate: footerHtml || "<div></div>",
margin: options.margin,
preferCSSPageSize: options.preferCSSPageSize
});
This configuration produces consistent multi-page PDFs with working page numbers.
AWS Lambda Notes
Puppeteer requires a Lambda-compatible Chromium build. Use @sparticuz/chromium(Link NPM) with puppeteer-core. Lambda functions should run with at least 1024 MB memory, and reusing the browser instance reduces cold-start overhead.
Conclusion
Embedding header, footer, and PDF options inside the HTML provides a stable foundation for multi-page PDFs with correct page numbering. The Node.js code extracts the templates and passes them directly to Puppeteer, independent of how the HTML was generated. This pattern is also used internally at Pantarey to ensure consistent serverless PDF generation.
Details for end users on how to use this are available in the Pantarey documentation: Pantarey Documentation - PDF generation
FAQ
Does Puppeteer allow dynamic total page numbers?
Yes. Puppeteer injects page numbers only inside headerTemplate and footerTemplate
using <span class="pageNumber"></span> and <span class="totalPages"></span>.
Can I use CSS counters for page numbers?
No. CSS counters do not work inside Chromium’s print layout engine.
Does this approach work in AWS Lambda?
Yes. Use @sparticuz/chromium together with puppeteer-core, allocate enough memory, and avoid external resources in templates.
Can users define their own header and footer HTML?
Yes. The header, footer, and PDF options are extracted directly from the HTML, which allows user-controlled layouts.
Do headers and footers repeat automatically across pages?
Yes. Puppeteer repeats them on every printed page.
Troubleshooting
Footer is cut off
Margins must include enough space. Puppeteer does not allocate layout space automatically. Assign them via pdf-options section dynamically.
PDF renders without styles
Use inline or embedded CSS only. Puppeteer cannot load external stylesheets in Lambda unless explicitly allowed.
PDF renders without styles
Use inline or embedded CSS only. Puppeteer cannot load external stylesheets in Lambda unless explicitly allowed.
Script inside HTML not executed
Puppeteer's print layout ignores client-side JavaScript. All dynamic content must be rendered before calling page.pdf().