The right implementation depends on how your PDF is produced. For HTML rendered in a headless browser, use Puppeteer’s displayHeaderFooter, headerTemplate, and footerTemplate options. For an existing PDF, load it with pdf-lib and draw text or images onto each page. For PDFs assembled directly in Node.js, PDFKit gives you page-level drawing and stream output, but you must implement and verify your own repeating layout.
Choose the workflow before writing code
Headers and footers are not one universal PDF feature. They are either part of browser printing, overlays drawn onto existing pages, or content drawn while your application creates each page.
| Input and goal | Best fit | How repetition works |
|---|---|---|
| HTML, CSS, and web fonts rendered to PDF | Puppeteer | Chromium applies HTML templates during printing and can inject page metadata. |
| An existing PDF that needs branding, labels, or page numbers | pdf-lib | Your code loops over loaded pages and draws at coordinates on each page. |
| A PDF assembled directly from application data | PDFKit | Your code draws on each page and controls pagination; verify the pattern against your installed version. |
These approaches are not interchangeable. Puppeteer templates belong to the browser print pipeline. pdf-lib and PDFKit operate on PDF page coordinates, so they do not automatically reflow document content around an overlay.
Add headers and footers when printing HTML with Puppeteer
Puppeteer’s PDF options define explicit header and footer templates. Set displayHeaderFooter to true; otherwise the templates are not shown. The documented template classes include date, title, url, pageNumber, and totalPages (Puppeteer PDFOptions documentation).
#1 Best Overall
Install and render a complete Node.js example
Install Puppeteer in the project that will run Chromium:
npm install puppeteer
The following script writes an HTML invoice to a PDF, repeats a branded header and footer, and exposes the current page and total page count. The margins are design examples; adjust them for your paper size and template height.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
// In containers you may need the Chromium flags required by your runtime.
headless: true
});
try {
const page = await browser.newPage();
await page.setContent(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; margin: 0; color: #222; }
h1 { color: #174a7e; }
.row { display: flex; justify-content: space-between; border-bottom: 1px solid #ddd; padding: 8px 0; }
</style>
</head>
<body>
<h1>Quarterly report</h1>
${Array.from({ length: 80 }, (_, i) => `<div class="row"><span>Line item ${i + 1}</span><span>$${(i * 17.25).toFixed(2)}</span></div>`).join('')}
</body>
</html>`, { waitUntil: 'networkidle0' });
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
displayHeaderFooter: true,
headerTemplate: `
<div style="width:100%; font-size:9px; margin:0 35px; color:#174a7e;">
Acme Reports · Confidential
</div>`,
footerTemplate: `
<div style="width:100%; font-size:9px; margin:0 35px; color:#666; display:flex; justify-content:space-between;">
<span><span class="title"></span></span>
<span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
</div>`,
margin: {
top: '60px',
bottom: '60px',
left: '40px',
right: '40px'
}
});
} finally {
await browser.close();
}
})();
Reserve space and style the templates
The top and bottom margins reserve room for the templates. If a header is taller than the top margin, it can overlap page content; if the margin is unnecessarily large, usable content area shrinks. Measure the largest header and footer state, including wrapped text, and set margins accordingly.
Template HTML is a print fragment, not your normal page body. Keep it self-contained and use inline styles for predictable rendering. The special classes are replaced by Chromium when the PDF is generated:
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchRank #2
dateinserts the print date.titleinserts the document title.urlinserts the page URL.pageNumberinserts the current page number.totalPagesinserts the document’s total page count.
Use printBackground: true when colored backgrounds or images are part of the printed design. Wait for content and fonts before calling page.pdf(); otherwise a late-loading asset can produce a header, footer, or body that differs from the browser preview.
Add an overlay to an existing PDF with pdf-lib
pdf-lib can load an existing document, expose its pages, draw text or images, and save modified bytes (pdf-lib overview; PDFDocument API). This is a page-level operation: it does not repaginate paragraphs or push existing content down.
Draw a repeated text header and footer
const fs = require('node:fs/promises');
const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');
async function addHeaderFooter(inputPath, outputPath) {
const source = await fs.readFile(inputPath);
const pdfDoc = await PDFDocument.load(source);
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const bold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
const pages = pdfDoc.getPages();
pages.forEach((page, index) => {
const { width, height } = page.getSize();
const left = 36;
const right = width - 36;
const headerY = height - 30;
const footerY = 24;
page.drawText('Acme Reports', {
x: left,
y: headerY,
size: 10,
font: bold,
color: rgb(0.09, 0.29, 0.49)
});
page.drawLine({
start: { x: left, y: height - 42 },
end: { x: right, y: height - 42 },
thickness: 0.6,
color: rgb(0.75, 0.78, 0.82)
});
const label = `Page ${index + 1} of ${pages.length}`;
const labelWidth = font.widthOfTextAtSize(label, 9);
page.drawText('Confidential', {
x: left,
y: footerY,
size: 9,
font,
color: rgb(0.35, 0.35, 0.35)
});
page.drawText(label, {
x: right - labelWidth,
y: footerY,
size: 9,
font,
color: rgb(0.35, 0.35, 0.35)
});
});
await fs.writeFile(outputPath, await pdfDoc.save());
}
addHeaderFooter('input.pdf', 'output.pdf').catch((error) => {
console.error(error);
process.exitCode = 1;
});
Coordinate and collision rules
PDF coordinates normally start at the bottom-left. page.getSize() returns the page’s width and height, so calculate positions from those values rather than assuming A4 or Letter dimensions. Keep the overlay inside a safe inset, and inspect the source PDF before choosing coordinates. A source with content already near the edge can be covered by your new header or footer.
If pages have different sizes or rotations, calculate each page independently, as the example does. For a logo, embed a PNG or JPEG and use page.drawImage(); reserve enough vertical space for its actual dimensions. For long labels, measure text with the embedded font and reduce the font size, wrap deliberately, or truncate according to your document policy.
Recommended Free Tools
Rank #3
Create PDFs directly with PDFKit
PDFKit creates PDF output directly in Node.js and can pipe the document to a writable stream. Its getting-started guide covers importing the library, creating a document, and streaming output (PDFKit getting started).
const PDFDocument = require('pdfkit');
const fs = require('node:fs');
const doc = new PDFDocument({ size: 'A4', margin: 54 });
doc.pipe(fs.createWriteStream('generated.pdf'));
doc.fontSize(10).fillColor('#174a7e').text('Acme Reports', 54, 28);
doc.moveTo(54, 44).lineTo(541, 44).stroke('#c0c6cc');
doc.fontSize(11).fillColor('#222');
for (let i = 1; i <= 80; i += 1) {
if (doc.y > 735) {
doc.addPage();
doc.fontSize(10).fillColor('#174a7e').text('Acme Reports', 54, 28);
doc.moveTo(54, 44).lineTo(541, 44).stroke('#c0c6cc');
doc.fontSize(11).fillColor('#222');
}
doc.text(`Line item ${i}`, 54, doc.y + 8);
}
doc.end();
This demonstrates explicit drawing and a manual page-break decision. The official getting-started material does not establish a dedicated automatic repeating-header API, so verify your exact PDFKit version and pagination pattern. A production implementation should centralize header/footer drawing in a function called after every addPage(), and leave enough bottom space for the footer before writing body content.
Production details that prevent broken output
Fonts, images, and asynchronous content
- For Puppeteer, wait for network resources, images, and fonts before printing. A network-idle condition alone may not match every application’s asset-loading behavior.
- For pdf-lib and PDFKit, embed the fonts and images you actually need. Font metrics affect right-aligned page labels and wrapping.
- Use a consistent color space and test transparent logos on both white and colored backgrounds.
Page numbering and ranges
Browser templates can receive the total page count through totalPages. In pdf-lib, pages.length gives the count after loading; in PDFKit, total pages are not known until pagination is complete, so “Page X of Y” may require a separate strategy or a second pass. Do not print a total that your generation path cannot determine reliably.
Security and resource limits
Treat HTML, URLs, and source PDFs as untrusted input. Restrict network access available to browser rendering, validate uploaded files, set job timeouts, and cap input size. A malformed or unusually large PDF can consume substantial memory while loading and saving.
Rank #4
Troubleshooting common failures
The header or footer is missing in Puppeteer
Confirm displayHeaderFooter: true is present and that the template strings are non-empty. Increase the corresponding top or bottom margin. Also check that you are inspecting the newly generated PDF rather than a cached or earlier file.
Content overlaps the overlay
Increase the reserved margin in Puppeteer. For pdf-lib or PDFKit, move the drawing coordinates farther from the edge and adjust body layout; drawing a header does not push existing PDF content down.
Page numbers are blank
Use the documented class names exactly: pageNumber and totalPages. They are template classes, not JavaScript variables. In pdf-lib, compute the index yourself; in PDFKit, do not assume the final page count is available during the first write.
A pdf-lib output cannot be opened
Await both PDFDocument.load() and pdfDoc.save(), write the returned bytes without converting them to text, and ensure the input is a complete PDF rather than an HTML error response renamed with a .pdf extension.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesPDFKit pages have inconsistent headers
Call the same header routine immediately after every addPage(), and keep body coordinates below the header’s bottom boundary. Confirm the page size and margins used by every page.
Or skip the browser setup
If your goal is a clean PDF or page image of a web URL rather than custom PDF composition, ScreenshotNeo provides a single HTTP request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response reports its page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
For the API parameters and all 63 capture options, see the ScreenshotNeo documentation. A basic Node.js request is:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
The equivalent cURL and Python calls are:
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)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
ScreenshotNeo supports PDF paper size, margins, landscape mode, page ranges, custom CSS and JavaScript, click actions, selector waits, lazy-image loading, custom headers and cookies, geolocation, caching with a chosen TTL, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, and HTML/CSS-to-image. Every feature is on every plan. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Which implementation should you ship?
Use Puppeteer when your source of truth is HTML and you want browser print behavior, including built-in page metadata classes. Use pdf-lib when a PDF already exists and you need a controlled overlay on each page. Use PDFKit when your service owns document layout from the beginning and can maintain its pagination code. In every case, reserve physical space for the header and footer, calculate positions from actual page dimensions, and test multi-page, mixed-size, long-text, and missing-asset cases.
Frequently Asked Questions
Can Puppeteer add a footer to an existing PDF file?
No. Puppeteer’s headerTemplate and footerTemplate are options for Chromium’s HTML-to-PDF print operation. Load an existing PDF with pdf-lib when you need page-level edits.
Can pdf-lib automatically move body content below a new header?
No. pdf-lib draws on existing page coordinates. You must choose safe coordinates or regenerate the document with layout space reserved.
How do I add an image logo to every page?
Embed the image once with pdf-lib or draw it in your PDFKit header routine, then call the drawing operation for each page and size it from the page dimensions.
Free tools Windows power users keep installed
One-click scans. No signup required.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




