ScreenshotNeo

BlogHTML to image & PDF

Adding Headers to Each Page of a PDF in Node.js With Puppeteer

Add a repeating header and page numbers to Puppeteer PDFs with headerTemplate, correct margins, runnable Node.js code, and fixes for common print issues.

By the ScreenshotNeo team30 September 20269 min read

Adding Headers to Each Page of a PDF in Node.js With Puppeteer

To add a header to every page of a PDF generated with Puppeteer, pass displayHeaderFooter: true and a valid HTML string as headerTemplate to page.pdf(). Reserve room with margin.top; use footerTemplate and margin.bottom for page numbers or other repeating footer content. The header template belongs to Chromium’s print layout, so it repeats as content flows across pages.

This guide covers a runnable Node.js implementation, page numbering, margins and print options, common failures, and an API alternative when you do not want to manage a browser process. See the Puppeteer PDF API documentation and the ScreenshotNeo API docs alongside the examples.

1. Install Puppeteer and generate a PDF

Install Puppeteer in a Node.js project. The standard Puppeteer package downloads a compatible Chrome for Testing browser during installation. If your deployment provides its own browser, configure the executable path and verify compatibility with your installed Puppeteer version.

npm install puppeteer

Save the following as pdf-with-header.mjs and run it with node pdf-with-header.mjs. It creates a multi-page PDF with a repeated header and page count footer:

import puppeteer from 'puppeteer';

const html = `
  <!doctype html>
  <html>
    <head>
      <meta charset="utf-8">
      <style>
        body { font: 14px Arial, sans-serif; line-height: 1.5; }
        h1 { font-size: 24px; }
        .section { break-inside: avoid; margin: 0 0 18px; }
      </style>
    </head>
    <body>
      <h1>Quarterly Report</h1>
      ${Array.from({ length: 30 }, (_, i) =>
        `<div class="section"><h2>Section ${i + 1}</h2><p>Report content for section ${i + 1}. Add enough content to flow onto multiple printed pages.</p></div>`
      ).join('')}
    </body>
  </html>`;

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.setContent(html, { waitUntil: 'networkidle0' });

  await page.pdf({
    path: 'report.pdf',
    format: 'A4',
    displayHeaderFooter: true,
    headerTemplate: `
      <div style="width:100%; margin:0 30px; font:9px Arial,sans-serif; color:#444; border-bottom:1px solid #bbb; padding-bottom:4px;">
        Quarterly Report
      </div>`,
    footerTemplate: `
      <div style="width:100%; margin:0 30px; text-align:center; font:9px Arial,sans-serif; color:#444;">
        Page <span class="pageNumber"></span> of <span class="totalPages"></span>
      </div>`,
    margin: {
      top: '60px',
      bottom: '45px',
      left: '30px',
      right: '30px'
    },
    printBackground: true
  });
} finally {
  await browser.close();
}

The HTML above is deliberately self-contained. In a real app, replace the sample report with your generated markup. The finally block closes Chrome even if page setup or PDF generation throws, which matters in a server handling repeated jobs.

2. Make the header repeat and leave room for it

Puppeteer’s page.pdf() uses Chromium’s print rendering. A header template is printed in the page’s header area as each PDF page is laid out. It is not a normal element placed once at the top of your document body. That is why a body-level <header> usually appears once, while headerTemplate is designed for repeating page furniture.

Puppeteer’s print header area repeats the template as document content flows across PDF pages.
Puppeteer’s print header area repeats the template as document content flows across PDF pages.

Three settings work together:

  • displayHeaderFooter: true enables the header and footer areas. Its default is false, so omitting it hides both templates.
  • headerTemplate supplies the header HTML. footerTemplate supplies the footer HTML.
  • margin.top and margin.bottom reserve printable space for those areas. Increase them if content crowds, clips, or overlaps the header or footer.

There is no universal correct margin: it depends on template height, font size, and page dimensions. Start with enough room, generate a PDF containing several pages, then adjust while checking the first, middle, and last pages. Margins are page geometry, not padding on the template itself.

Use the built-in page and metadata classes

Puppeteer supports these replacement classes in header and footer templates:

Class Rendered value Example use
date Print date Document generation date
title Page title Report or web page title
url Page URL Source attribution
pageNumber Current page number “Page 2”
totalPages Total page count “of 12”
headerTemplate: `<div style="width:100%;font:9px Arial;text-align:right">
  <span class="title"></span>
</div>`,
footerTemplate: `<div style="width:100%;font:9px Arial;text-align:center">
  <span class="pageNumber"></span> / <span class="totalPages"></span>
</div>`

Use the class names exactly as documented. To include an application-specific label, put literal text in the template or escape and insert a trusted value when constructing the template. Treat user-supplied HTML as untrusted; do not interpolate it into markup without appropriate escaping.

3. Style templates for print output

Keep header and footer markup small, valid, and self-contained. Inline CSS is a predictable choice because the template is separate from the document’s normal body styles. Set explicit font size, alignment, width, and color. Avoid depending on external stylesheets, scripts, or page selectors to style the template.

Top and bottom PDF margins reserve room for the repeating header and footer.
Top and bottom PDF margins reserve room for the repeating header and footer.

Use simple HTML such as div, span, and basic inline styles. A long title can wrap and consume more vertical room than expected; shorten it, reduce its font size, or increase the reserved margin. If the header needs a rule, use a border on its container and check that the line stays inside the printable width.

page.pdf() renders with the print CSS media type. If your layout has screen-only CSS and the PDF should match that presentation, call await page.emulateMediaType('screen') before producing the PDF. Conversely, leave the default print mode in place when print styles and page breaks are intentional.

Chromium may adjust colors for printing. For exact CSS color reproduction, Puppeteer documents using -webkit-print-color-adjust: exact. Put it in the relevant document styles, for example * { -webkit-print-color-adjust: exact; }. This can affect ink-heavy backgrounds, so use it only when the designed colors matter. Set printBackground: true when background graphics should be included; otherwise they may be omitted.

4. Control paper size, scale, and page selection

Header templates are only one part of PDF layout. Choose page geometry and print options deliberately:

Option What it controls When to use it
format Standard paper size, such as A4 or Letter Typical reports and printable documents
width, height Custom page dimensions Labels or a fixed custom sheet
preferCSSPageSize Whether CSS @page dimensions take priority When document CSS owns the paper size
pageRanges Pages included in the output Exporting a subset of a long report
scale Print content scale, from 0.1 to 2 Small adjustments to fit content
printBackground Whether background graphics are printed Colored sections or designed backgrounds

For example, format: 'Letter' selects Letter paper. Custom dimensions can be provided with width and height. If the page’s CSS defines @page { size: ... }, set preferCSSPageSize: true so that CSS takes priority over API dimensions. Avoid changing scale to compensate for a wrong margin; fix page geometry first.

Keep document content from breaking awkwardly

Page headers do not prevent headings, table rows, or cards from splitting badly. Use print CSS such as break-inside: avoid on compact blocks and break-before: page where a deliberate new page is required. Very tall blocks cannot fit intact on one page, so browsers may still fragment them. Test the result at the actual paper size, with the actual fonts and representative long content.

5. Alternative: CSS page margin boxes

Chrome 131 introduced generated content in print margin boxes. CSS can define counters in an @page rule, for example an @bottom-right box using counter(page) for the current page and the pages counter for the total. This lets a stylesheet own page numbering and margin content.

@page {
  @bottom-right {
    content: counter(page) " / " counter(pages);
    font: 9px Arial, sans-serif;
  }
}

This depends on the Chromium version available in the deployed environment. Confirm that version supports the feature and check output in production-like conditions. Puppeteer’s documented headerTemplate and footerTemplate options are the more direct choice when you need the API’s explicit template mechanism.

6. Or skip the browser setup

If you need a PDF from a page and do not want to install, launch, and maintain Puppeteer and Chromium, ScreenshotNeo provides a screenshot and PDF API. It accepts a URL and can return a PDF; see the API documentation for PDF options and other parameters.

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
import requests

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

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 shots a month without a card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Learn more at ScreenshotNeo and the docs. Sign up for 1,000 free screenshots a month, with no card required.

7. Troubleshooting

Symptom Likely cause Fix
No header or footer appears displayHeaderFooter is missing or false Set it to true in the page.pdf() options.
Header appears only on the first page A normal body element is being used as the header Move the repeating markup into headerTemplate.
Text overlaps the header Top margin does not reserve enough space Increase margin.top; similarly increase bottom margin for footer overlap.
Template looks unstyled It relies on document styles or external assets Use simple valid template HTML with inline styles.
Page number is blank Replacement class is misspelled or nested incorrectly Use the documented pageNumber and totalPages class names on spans.
PDF colors differ from browser view Print media and print color adjustment apply Check print CSS, use printBackground for backgrounds, and apply -webkit-print-color-adjust: exact if exact colors are needed.
CSS page size is ignored API format or dimensions take precedence Set preferCSSPageSize: true and confirm the CSS @page rule.
Page breaks changed unexpectedly Content, fonts, scale, or margins changed print layout Inspect a multipage output and tune print-specific break rules and page geometry.

8. Reliability, performance, and cost considerations

PDF creation consumes a browser process and time for page loading, fonts, and layout. Reuse browser processes carefully for batches rather than launching one Chrome per document, and isolate pages or contexts according to your workload. Always close pages and browsers when jobs finish, including error paths. Set an application-level timeout and return a useful failure when navigation or rendering does not complete; a navigation timeout is distinct from a malformed PDF template.

Wait for the condition your page actually needs. networkidle0 is convenient for a self-contained static report, but pages with analytics, long polling, or persistent requests may never become idle. If you control the content, wait for a known selector or explicit readiness signal instead, then produce the PDF. Ensure remote fonts and images have loaded before capture when they affect pagination.

For cost, self-hosting means accounting for compute, memory, browser installation, and maintenance in your own deployment; the exact amount depends on document size and infrastructure. A hosted API trades that setup for per-plan usage. ScreenshotNeo’s published plans are Free 1,000/month, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free. All features are on every plan. Choose based on expected successful captures and required control, and inspect billing headers when using the API.

9. Frequently asked questions

The header template accepts HTML. Any image adds a dependency on its availability and sizing during print rendering, so keep the asset reachable and test its loading and alignment in the generated PDF. The template does not need an image for a text header.

Can I show “Page X of Y”?

Yes. Put <span class="pageNumber"></span> and <span class="totalPages"></span> in the footer template, with displayHeaderFooter: true.

Can I make the header different on the first page?

The documented Puppeteer template is applied as print page furniture. For different first-page design requirements, consider laying out the first page separately or use a browser-supported CSS print technique after verifying the Chromium version and output.

Does setting a header affect page count?

Reserving header and footer space reduces the body area on each sheet and can change where content breaks, which may change the total number of pages. Recheck page ranges and page counts after margin or template changes.