ScreenshotNeo

BlogHow-to

How to Load an Image in a Puppeteer PDF Header Template With Node.js

Add an image to a Puppeteer PDF header with Node.js, handle loading limits, layout, troubleshooting, and an API alternative.

By the ScreenshotNeo team30 September 20268 min read

How to Load an Image in a Puppeteer PDF Header Template With Node.js

Short answer: Put your image markup in Puppeteer’s PDFOptions.headerTemplate, set displayHeaderFooter: true, and reserve enough top margin for the header. The template is an HTML fragment rendered by Chromium. Puppeteer’s reference does not promise that a particular image source (remote URL, filesystem path, data URL, or other form) will work in every runtime, nor does it document that page.pdf() waits for header images. Treat the image source as an implementation detail to verify in your version, and check the generated PDF rather than assuming a successful call means the image appeared.

1. The minimal working shape

The official API exposes two controls that matter first: headerTemplate supplies the HTML, and displayHeaderFooter turns the header on. A top margin gives the header physical space; without it, body content can overlap the header or the header can be clipped. Page.pdf() uses print CSS media, so print-specific rules and color behavior apply.

The header template is HTML rendered during PDF print, with the image source resolved by the browser.
The header template is HTML rendered during PDF print, with the image source resolved by the browser.
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setContent('<main><h1>Invoice</h1><p>Body content</p></main>');

  await page.pdf({
    path: 'output.pdf',
    displayHeaderFooter: true,
    headerTemplate: `
      <div style="width:100%; font-size:9px; text-align:center;">
        Document header
      </div>`,
    footerTemplate: `
      <div style="width:100%; font-size:9px; text-align:center;">
        Page <span class="pageNumber"></span> of <span class="totalPages"></span>
      </div>`,
    margin: { top: '60px', bottom: '45px' }
  });
} finally {
  await browser.close();
}

This follows the documented launch, page creation, and PDF flow. The API also supports .date, .title, .url, .pageNumber, and .totalPages classes for values injected at print time. Header and footer HTML should be self-contained: do not assume your page’s normal stylesheet, scripts, or DOM are available inside the template.

2. Adding an image to the header

Add an <img> element to the template and make its source accessible to Chromium in your deployment. The following is an implementation pattern, not a guarantee about any particular URL form. The official PDF options reference defines the template as HTML but does not define image-source support or an image-wait contract.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.goto('https://example.com/report', { waitUntil: 'networkidle0' });

  const headerTemplate = `
    <div style="width:100%; height:32px; display:flex; align-items:center; justify-content:space-between; font:10px Arial, sans-serif;">
      <img src="YOUR_IMAGE_SOURCE" style="height:24px; width:auto;" alt="" />
      <span><span class="title"></span></span>
    </div>`;

  await page.pdf({
    path: 'report.pdf',
    displayHeaderFooter: true,
    headerTemplate,
    margin: { top: '72px', bottom: '48px' },
    printBackground: true,
    preferCSSPageSize: true
  });
} finally {
  await browser.close();
}

Choose and verify the image source

  • Remote URL: Chromium must be able to resolve it, and the host must permit the request. Redirects, authentication, TLS errors, and network policy can leave a broken image.
  • Local or generated bytes: expose them in a way the browser context can read. A path that exists in your Node process is not automatically a browser-loadable URL.
  • Inline data: if you use an inline representation, validate it in the exact Puppeteer and Chromium versions you deploy. The reviewed docs do not declare a universal guarantee for image data forms.

After creating the PDF, open it or extract a page image in your CI check. A successful promise from page.pdf() only says that PDF generation completed; it does not prove the header image was decoded and painted.

3. Make image loading observable

Because header-image waiting is not specified, use a two-part check: confirm the source is reachable in the page context, then inspect the PDF output. You can also preload the same resource before calling pdf(); this reduces surprises but still should be verified.

const imageUrl = 'YOUR_IMAGE_SOURCE';
await page.evaluate(async (src) => {
  const image = new Image();
  image.src = src;
  await new Promise((resolve, reject) => {
    image.onload = resolve;
    image.onerror = () => reject(new Error(`Image failed to load: ${src}`));
  });
}, imageUrl);

await page.pdf({
  path: 'checked.pdf',
  displayHeaderFooter: true,
  headerTemplate: `<div><img src="${imageUrl}" style="height:24px"></div>`,
  margin: { top: '70px' }
});

This validates browser access to the URL from the document page. It does not change the fact that the header is a separate print template, so retain a PDF fixture or visual assertion for the final result.

4. Layout, CSS, and print options

  • Top margin: set it larger than the rendered header height plus padding. Increase it when a logo is clipped or body text starts too high.
  • Paper and orientation: use format such as A4, or explicit width and height. Use landscape: true for wide documents.
  • CSS page size: preferCSSPageSize: true lets an @page rule control paper dimensions. Otherwise the PDF options win.
  • Print colors: PDF output uses print media and colors are modified for printing by default. Add print CSS and printBackground: true when backgrounds are part of the design.
  • Dimensions: use explicit pixel or CSS units for the image. Avoid relying on intrinsic dimensions; a large logo can force wrapping or overflow.
  • Isolation: inline the header’s critical CSS. Page styles are not a dependable way to style the template.

The waitForFonts option waits for document.fonts.ready (its documented default is true). That setting concerns fonts; it is not evidence that an image request is awaited.

5. Dynamic header values

Use the special classes documented by Puppeteer when you need metadata:

const headerTemplate = `
  <div style="font:9px Arial; width:100%;">
    <span class="title"></span>
    <span style="float:right">Page <span class="pageNumber"></span>/<span class="totalPages"></span></span>
  </div>`;

date, title, url, pageNumber, and totalPages are replaced for printing. Keep the markup simple and test long titles, multi-page output, and right-to-left or non-Latin text if those cases matter to your document.

6. Complete reusable Node.js function

import puppeteer from 'puppeteer';

export async function writePdf({ url, imageSource, output = 'output.pdf' }) {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle0' });

    await page.evaluate(async (src) => {
      const img = new Image();
      img.src = src;
      await new Promise((resolve, reject) => {
        img.onload = resolve;
        img.onerror = reject;
      });
    }, imageSource);

    await page.pdf({
      path: output,
      format: 'A4',
      displayHeaderFooter: true,
      headerTemplate: `<div style="width:100%;height:28px;font:10px Arial;display:flex;align-items:center;">
        <img src="${imageSource}" style="height:22px;width:auto" alt="" />
        <span style="margin-left:auto" class="title"></span>
      </div>`,
      footerTemplate: `<div style="width:100%;font:9px Arial;text-align:right;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>`,
      margin: { top: '64px', right: '36px', bottom: '48px', left: '36px' },
      printBackground: true,
      preferCSSPageSize: false,
      waitForFonts: true
    });
  } finally {
    await browser.close();
  }
}

await writePdf({ url: 'https://example.com/report', imageSource: 'YOUR_IMAGE_SOURCE' });

Pin Puppeteer and Chromium versions in production, and keep a representative PDF fixture in your release checks. That makes a change in browser behavior visible before it reaches users.

7. Troubleshooting checklist

Symptom Likely cause Fix
Header is missing displayHeaderFooter is false or omitted. Set it to true and regenerate.
Image icon or blank space Source cannot be fetched, is blocked, or is unsupported in this runtime. Preflight with new Image(), inspect browser/network errors, and verify the exact source form in the resulting PDF.
Header overlaps body Top margin is smaller than header height. Increase margin.top; measure the rendered header including padding.
Logo is clipped Intrinsic size exceeds the template box or page width. Set explicit height/width, preserve aspect ratio, and simplify flex layout.
Styles or fonts are absent Template is isolated from page CSS; font is not ready. Inline critical CSS, use web-safe fallbacks, and retain waitForFonts: true.
Wrong page dimensions format, explicit size, or @page rule wins unexpectedly. Choose one source of truth and set preferCSSPageSize deliberately.
Works locally, fails in CI Different Chromium, network egress, certificates, or filesystem. Pin versions, log the browser error, and test from the CI runtime.

8. Performance, reliability, and cost

Launching a browser for every document is expensive. Reuse one browser process and create or close pages per job, while limiting concurrency so memory does not spike. networkidle0 can wait indefinitely on pages with analytics or long polling; choose a navigation strategy that matches your site and add an application timeout. A preflight image request adds work, but it turns a silent missing logo into an actionable error.

Remote images introduce DNS, TLS, authentication, and availability dependencies. For reliable reports, serve the asset from infrastructure available to the renderer, version the asset, and record the URL and failure reason. Keep headers short: every extra pixel reduces usable body area and can create an unexpected page break. PDF generation itself has no per-call Puppeteer fee; your costs are compute, memory, bandwidth, and any asset hosting or browser service you operate.

9. Or skip the browser setup

If your goal is a clean image or PDF of a URL rather than a custom Chromium pipeline, ScreenshotNeo provides a single API call. It accepts the cookie or consent banner like a visitor, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and lets each cleanup step be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and whether it was billed. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000.

See the ScreenshotNeo API documentation for all options. The same endpoint can return PNG, JPEG, WebP, or PDF and includes controls for full-page and element capture, dark mode, device presets, retina scale, print settings, custom CSS and JavaScript, waits, request blocking, headers, cookies, user agent, timezone, geolocation, caching, signed links, async jobs, bulk capture, and usage reporting.

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

Create a free ScreenshotNeo account to use the 1,000 monthly shots with no card.

10. FAQ

No. It waits for document.fonts.ready; the reviewed documentation does not extend that guarantee to images.

Can I use a filesystem path in src?

The API reference does not promise a filesystem-path form. Make the bytes browser-accessible and verify the generated PDF in your target runtime.

Why is my header not visible even though the PDF opens?

Check displayHeaderFooter: true, a nonzero top margin, and whether the template has valid HTML.

Will page and total-page numbers work in any element?

Use the documented special classes in the header or footer template and test multi-page output.

Should I switch to an API?

Use Puppeteer when you need full browser control. Use ScreenshotNeo when a URL-to-image or URL-to-PDF endpoint, cleanup of consent UI, and usage-based billing are more useful than maintaining browser infrastructure.