ScreenshotNeo

BlogHTML to image & PDF

How to Set Different Headers and Footers on PDF Pages with Puppeteer

Use Puppeteer templates for repeated PDF headers, CSS margin boxes for left/right pages, and reliable fallbacks for per-section content.

By the ScreenshotNeo team29 September 20269 min read

How to Set Different Headers and Footers on PDF Pages with Puppeteer

Short answer: Puppeteer has two practical ways to vary PDF headers and footers. For a header or footer repeated on every page with changing page numbers, enable displayHeaderFooter and provide headerTemplate and footerTemplate. For different content on left and right pages, use CSS @page :left and @page :right margin boxes in Chrome 131 or newer. Arbitrary chapter-specific running headers are less reliable in Chromium because the documented string-set/string() implementation has a known bug.

This guide shows both approaches, complete runnable code, page-size and margin settings, debugging steps, and production considerations. The examples use Puppeteer 25.12.0-style APIs; browser support is version-sensitive, so verify the Chromium version bundled with your deployment against the Puppeteer PDFOptions documentation and Chrome’s paged-media guide.

1. Decide what “different” means

Most requests fall into one of three categories:

Requirement Best approach What it supports
Same design on every page, changing page number Puppeteer templates headerTemplate, footerTemplate, date, title, URL, current page and total pages
Different left-page and right-page furniture CSS page margin boxes @page :left, @page :right, top/bottom and side-specific counters
Different arbitrary header for every chapter or page Deliberate pagination or a separate document strategy Requires controlling page boundaries and validating the exact Chrome build

Puppeteer templates are document-wide: one template is applied repeatedly. They can still show changing values through special classes. CSS margin boxes can select page sides, but they do not make arbitrary per-page business logic automatic. Treat “different” as a layout requirement first, then choose the mechanism.

2. Repeated headers and footers with Puppeteer templates

Puppeteer’s displayHeaderFooter option defaults to false. Set it to true and pass HTML strings in headerTemplate and footerTemplate. The documented classes date, title, url, pageNumber, and totalPages are replaced while the PDF is rendered.

Puppeteer renders one document-wide header and footer template across every PDF page.
Puppeteer renders one document-wide header and footer template across every PDF page.

Complete Node.js example

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: true,
  // Set executablePath here when your deployment supplies Chromium itself.
});

try {
  const page = await browser.newPage();
  await page.setContent(`
    <!doctype html>
    <html>
      <head>
        <meta charset="utf-8">
        <style>
          body {
            font-family: Arial, sans-serif;
            font-size: 12pt;
            line-height: 1.5;
          }
          h1 { break-before: page; }
          h1:first-child { break-before: auto; }
        </style>
      </head>
      <body>
        <h1>Quarterly report</h1>
        <p>Generated from HTML with Puppeteer.</p>
        <h1>Details</h1>
        <p>Additional content makes the document span multiple pages.</p>
      </body>
    </html>
  `, { waitUntil: 'networkidle0' });

  await page.pdf({
    path: 'report.pdf',
    format: 'A4',
    printBackground: true,
    displayHeaderFooter: true,
    headerTemplate: `
      
`, footerTemplate: `
Page of
`, margin: { top: '70px', right: '30px', bottom: '70px', left: '30px' } }); } finally { await browser.close(); }

The top and bottom margins reserve printable space for the templates. Without enough room, body content can overlap the header or footer even though the PDF technically renders.

Template rules and limitations

  • Use inline CSS in templates. They are rendered in a small, isolated header/footer context rather than your page’s normal DOM.
  • Keep template markup simple. External stylesheets, scripts, and complex layout dependencies are poor fits for this area.
  • The pageNumber and totalPages spans are the reliable way to add counters.
  • Use title, url, and date when the browser-provided document metadata is sufficient. For custom values, interpolate escaped data into the template before calling page.pdf().
  • Set printBackground: true when colored header bands or backgrounds must be preserved.

3. Different headers and footers on left and right pages

Recent Chrome versions support CSS-generated content in page margin boxes. Chrome’s documentation says this capability is available from Chrome 131. You can define different rules for odd/even physical page sides with @page :left and @page :right.

CSS @page :left and :right margin boxes support page-side-specific furniture in Chrome 131+.
CSS @page :left and :right margin boxes support page-side-specific furniture in Chrome 131+.
<style>
  @page {
    size: A4;
    margin: 25mm 22mm 25mm 22mm;
  }

  @page :right {
    @top-right {
      content: "Technical report";
      font-size: 9pt;
      color: #555;
    }
    @bottom-right {
      content: "Page " counter(page) " of " counter(pages);
      font-size: 9pt;
      color: #555;
    }
  }

  @page :left {
    @top-left {
      content: "Confidential";
      font-size: 9pt;
      color: #555;
    }
    @bottom-left {
      content: "Page " counter(page) " of " counter(pages);
      font-size: 9pt;
      color: #555;
    }
  }

  body {
    font-family: Arial, sans-serif;
    font-size: 11pt;
  }
</style>

Load that stylesheet in the HTML passed to page.setContent(), then generate the PDF with CSS sizing enabled:

await page.pdf({
  path: 'booklet.pdf',
  printBackground: true,
  preferCSSPageSize: true,
  margin: { top: '0', right: '0', bottom: '0', left: '0' }
});

preferCSSPageSize: true gives the CSS @page size priority over Puppeteer’s width, height, or format. If it is false, Puppeteer scales content to fit the selected paper dimensions. Do not mix competing size declarations casually: decide whether CSS or the Puppeteer option owns the page geometry.

CSS margin boxes and Puppeteer templates are separate mechanisms. If you use both, inspect the output carefully for duplicate or competing furniture. For left/right layouts, CSS is usually the clearer owner of the header and footer.

4. What about a different header for every chapter?

A chapter title that changes whenever a new section starts is a running-header problem. CSS features such as string-set and string() appear designed for this, but Chrome’s guide documents a Chromium bug affecting them. Do not promise dynamic chapter labels without checking the exact browser build you deploy.

Safer options are:

  1. Make each chapter a separately rendered PDF and merge the files with a PDF tool that supports page composition.
  2. Insert deliberate page breaks and generate a known header value for each independently rendered segment.
  3. Use a pagination engine whose running-header feature you have validated for your target output.
  4. Keep a document-wide header and put the chapter name in the body or a first-page title block.

These approaches add implementation work, but they make the source of each header explicit. Whichever route you choose, verify first, middle, last, left, and right pages in the exact Puppeteer and Chrome versions used in production.

5. Media type, colors, page size, and margins

Page.pdf() generates using the print media type by default. Print CSS may hide elements, change colors, or select different layout rules. If your screen styles are intentional, call:

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

For exact color output, Chrome’s documentation points to -webkit-print-color-adjust:

<style>
  * { -webkit-print-color-adjust: exact; }
</style>

Use this only when color fidelity matters, because print-oriented rendering can otherwise optimize colors for paper. Puppeteer’s documented default format is Letter, while margin is undefined by default, meaning no margins are set. Explicitly define both when headers or footers matter.

6. Reliable rendering workflow

  1. Wait for content. Use waitUntil: 'networkidle0' where appropriate, and explicitly wait for fonts, images, or application data that load later.
  2. Choose one page-size owner. Use format/width/height, or CSS @page with preferCSSPageSize.
  3. Reserve header/footer space. Set top and bottom margins larger than the template’s content plus padding.
  4. Make print behavior explicit. Decide between print and screen media and set printBackground.
  5. Inspect representative pages. Check the first, a middle page, the last page, and both left/right sides when using page selectors.
  6. Pin compatible versions. Chrome 131+ is required for the documented margin-box feature; a package upgrade can change the bundled browser.

7. Troubleshooting

Symptom Likely cause Fix
No header or footer appears displayHeaderFooter is false or omitted Set it to true and provide a template.
Header overlaps body text Top margin is too small Increase margin.top; do the same for the footer and margin.bottom.
Page number is blank Wrong class name or malformed template Use <span class="pageNumber"></span> and totalPages exactly.
Left/right rules do nothing Chrome is older than 131, or CSS margin boxes are unsupported in that build Check the browser version; fall back to templates or a validated pagination strategy.
Colors or backgrounds changed PDF uses print media and print color adjustment Review print CSS, set printBackground: true, and use emulateMediaType('screen') only when appropriate.
CSS page size is ignored preferCSSPageSize is false or competing dimensions are set Set it to true and remove conflicting format/width/height values.
Chapter titles do not update Chromium’s string-set/string() behavior is affected by a documented bug Use explicit pagination or independently rendered sections.
Header text is clipped Template content is wider than the printable area Reduce padding/font size, adjust left/right margins, and keep template width within the page.

8. Performance, reliability, and cost

PDF generation starts a browser page, loads the document, waits for resources, lays out every page, and writes the file. Reuse a browser process when generating many documents, but create an isolated page per job and close it in a finally block. Set an application timeout around navigation and PDF generation, and record the Puppeteer and Chrome versions with each artifact so rendering changes are explainable.

Large images, web fonts, client-side data fetching, and intentionally long documents increase work. Self-host critical assets or wait for them explicitly. For reliability, render a fixed fixture document in CI and compare key pages after browser upgrades. The research sources do not provide a benchmark, so choose concurrency from your own workload measurements rather than assuming a fixed pages-per-second rate.

9. cURL, Python, and Node.js alternatives

If you need to automate your own Puppeteer service, expose a small endpoint that accepts HTML and options, then call it from any client. A minimal cURL request might look like:

curl -X POST http://localhost:3000/render-pdf \
  -H 'content-type: application/json' \
  --data-binary @request.json \
  -o report.pdf

Python can call the same service with the standard requests package:

import requests

payload = {
    "html": "<h1>Report</h1>",
    "pdf": {"displayHeaderFooter": True}
}
r = requests.post("http://localhost:3000/render-pdf", json=payload, timeout=90)
r.raise_for_status()
with open("report.pdf", "wb") as f:
    f.write(r.content)

For direct browser control, the Node.js example in section 2 is the applicable implementation because Puppeteer is a JavaScript library.

10. Or skip the browser setup

If your goal is a clean screenshot or PDF of a URL rather than maintaining Chromium yourself, ScreenshotNeo provides a one-request website capture API. See the ScreenshotNeo API documentation for the full option set.

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}`);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. ScreenshotNeo also has an MCP server with 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 shots. Create a free ScreenshotNeo account.

11. FAQ

Can Puppeteer assign a unique template to each physical page?

No. headerTemplate and footerTemplate repeat document-wide. Use CSS left/right margin boxes or an explicit pagination design for page-specific differences.

Do CSS margin boxes work in every Chromium release?

The cited Chrome documentation identifies Chrome 131 as the threshold for this feature. Check the actual bundled browser rather than the Puppeteer package version alone.

Why are my screen styles missing from the PDF?

PDF generation uses print media by default. Add print rules or call page.emulateMediaType('screen') before page.pdf() when screen styling is desired.

Should I use Puppeteer templates and CSS margin boxes together?

Usually choose one owner for headers and footers. Combining them can produce duplicate content and makes margin calculations harder to reason about.

How do I get a running chapter title?

Chromium’s documented bug affects string-set and string(). Use deliberate pagination or another validated pagination engine when dynamic running titles are required.