Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Short answer: wkhtmltopdf can put a section name in a repeated header or footer with the documented [section] and [subsection] substitutions, and it can show global numbers with [page] and [topage]. It does not document a built-in numeric “page within this section” value. If you need numbering that restarts at every section, divide the document into separately paginated sections or calculate the pagination in your generating application, then verify the resulting PDF with the exact wkhtmltopdf binary you deploy.

The distinction matters because wkhtmltopdf lays out the source as one long WebKit page and cuts that layout into PDF pages afterward. JavaScript can read values supplied to a header or footer, but it cannot reliably discover the final physical page boundaries by scanning the source DOM.

What wkhtmltopdf supports out of the box

The command-line interface supports these header and footer substitutions:

Placeholder Meaning Suitable for
[page] Current printed page Global page numbering
[frompage] First page being printed Ranges and offsets
[topage] Last printed page “Page X of Y”
[section] Current section name Repeated section labels
[subsection] Current subsection name Repeated subsection labels
[title], [doctitle] Page or document title Document identity
[sitepage], [sitepages] Page values for a site or multi-object conversion Multi-object jobs

For ordinary numbering, the simplest command is:

wkhtmltopdf --footer-right "Page [page] of [topage]" input.html output.pdf

This produces a global count. The placeholder list does not include a numeric “current page inside the current section” variable.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Show the current section in an HTML footer

For a section label, use an HTML header or footer document. wkhtmltopdf appends query-string values to that document’s URL. Its documented pattern parses those values and fills elements whose class names match supported keys.

1. Create footer.html

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script>
    function subst() {
      var vars = {};
      var pairs = window.location.search.substring(1).split('&');

      for (var i = 0; i < pairs.length; i++) {
        var pair = pairs[i].split('=', 2);
        vars[pair[0]] = decodeURIComponent(pair[1] || '');
      }

      ['page', 'topage', 'section', 'subsection'].forEach(function (key) {
        var nodes = document.getElementsByClassName(key);
        for (var j = 0; j < nodes.length; j++) {
          nodes[j].textContent = vars[key] || '';
        }
      });
    }
  </script>
</head>
<body onload="subst()" style="font-size:10px; margin:0 12mm;">
  <span class="section"></span>
  <span style="float:right">Page <span class="page"></span> of <span class="topage"></span></span>
</body>
</html>

2. Attach it to the conversion

wkhtmltopdf 
  --footer-html footer.html 
  --margin-bottom 18mm 
  input.html output.pdf

The section span receives the section name that wkhtmltopdf supplies. The page and topage spans receive the global page values. Keep enough bottom margin for the footer; otherwise body content can overlap it or be clipped.

3. Add ordinary header/footer text without JavaScript

If you only need a global number or a section name, text substitutions are shorter:

wkhtmltopdf 
  --header-left "[section]" 
  --footer-right "Page [page] of [topage]" 
  input.html output.pdf

Use the HTML method when you need custom markup, multiple fields, CSS, or conditional display.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Control JavaScript execution and timing

JavaScript is enabled by default in the documented CLI. Relevant controls are:

  • --disable-javascript turns page JavaScript off.
  • --javascript-delay <msec> waits before rendering; the documented default is 200 ms.
  • --run-script <js> executes additional JavaScript after loading and may be repeated.
  • --window-status <value> waits until window.status reaches that value.

A delay is only a timer, not proof that every asynchronous operation has finished. For deterministic content, set a status explicitly:

<script>
  fetch('/data.json')
    .then(function (r) { return r.json(); })
    .then(function (data) {
      document.querySelector('.section').textContent = data.section;
      window.status = 'ready';
    });
</script>
wkhtmltopdf --window-status ready input.html output.pdf

Do not combine --disable-javascript with a footer that depends on JavaScript substitution. Also remember that the footer is a separate HTML document; scripts in the source page do not automatically populate elements in that footer.

Can a JavaScript counter restart at every section?

Not reliably from a single flowing HTML object. A script can count headings, estimate element positions, or maintain a variable while the source DOM is built, but those values are not authoritative PDF page boundaries. wkhtmltopdf’s WebKit pagination lays out the content as one long page and then cuts it into pages. Depending on content and build, lines or images can split across pages, and small changes to fonts, margins, paper size, or loaded assets can move a break.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Therefore, a script such as “increment the counter when a heading appears” counts source elements, not printed pages. It will be wrong whenever a section occupies multiple pages, starts near a break, or is shifted by a layout change.

Choose an implementation based on your document structure

Sections are headings in one HTML object

Use [section] or [subsection] for the label and [page]/[topage] for global numbering. If you need “Section 3, page 2 of 5,” paginate each section before conversion or accept that a custom approximation must be checked against the PDF after every layout change.

Each section is a separate wkhtmltopdf object

Separate objects give your generating application an explicit boundary. Investigate the library’s global pageOffset and object-level pagesCount settings for your workflow. The settings reference describes those controls for offsets and counting, but it does not specify an automatic reset-at-section mechanism. Test the exact command and build rather than assuming that an object boundary resets every header value.

You control document generation

The most dependable approach is application-side pagination:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Render each section to a known HTML fragment.
  2. Determine its page breaks using the same paper size, margins, fonts, and assets used in production.
  3. Pass a section label and local page value to the template for that section.
  4. Convert the sections as separate objects or documents.
  5. Inspect the final PDF, including sections that begin near a page boundary.

This moves the reset logic to a layer that knows the intended boundaries instead of asking page JavaScript to infer them after layout.

Why counters become wrong after PDF pagination

  • Source positions are not PDF pages. A heading’s offset in the DOM does not reveal the page on which WebKit will cut the layout.
  • Fonts change geometry. A missing or substituted font changes line wrapping and can shift every later break.
  • Margins and paper size matter. Changing A4 to Letter, or changing header/footer margins, changes usable height.
  • Images and web fonts may load late. A timer can finish before network resources have settled.
  • Builds differ. Options marked as requiring patched Qt are not necessarily available in every packaged binary.

When a counter is wrong, compare the generated PDF with the same binary, CSS, fonts, viewport, and resource-access settings used by the failing job. Do not validate only with a short sample document.

Diagnostics and fixes

The footer is blank

  • Confirm the command uses --footer-html footer.html, not a source-page URL.
  • Ensure the element class is exactly section, page, or another supported key.
  • Check that JavaScript was not disabled.
  • Open the footer file directly and verify that its onload handler runs.

The section label appears, but the number is stale

The footer receives values through its own query string. Make sure your script parses and decodes that query string on every load, rather than caching a value in the source page.

“Page X of Y” is incorrect

Check whether you are converting multiple objects, using a page range, or applying an offset. Confirm the meaning of [frompage], [topage], [sitepage], and [sitepages] for that job. Compare with a simple single-document conversion before adding custom scripts.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The script runs before data is ready

Use --window-status and set window.status only after the data and layout-affecting resources are ready. If you use --javascript-delay, choose a value based on your slowest expected environment and still inspect the output.

Page breaks changed after an upgrade

Record the installed wkhtmltopdf version and whether it uses patched Qt. Re-run representative PDFs after changing the binary, OS, fonts, CSS, or page settings. Treat a custom section-relative counter as layout-dependent, not as a stable API contract.

Performance, reliability, and maintenance

  • Prefer built-in substitutions over custom scripts for labels and global numbers; they have fewer moving parts.
  • Keep footer JavaScript synchronous and small. Large libraries increase rendering time and introduce compatibility issues in the older WebKit engine used by many wkhtmltopdf builds.
  • Make external assets deterministic. Self-host fonts and critical images where possible, and wait for required data before conversion.
  • Pin the wkhtmltopdf binary in CI and production. A PDF rendering change can invalidate precomputed section page counts.
  • Keep a regression set with short, long, image-heavy, and boundary-heavy sections. Compare both the visible counter and the actual page breaks.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is a clean screenshot or PDF of a web page rather than a wkhtmltopdf section footer, 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; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the result in X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

Use the ScreenshotNeo API documentation for the available options, including full-page capture with lazy-image loading, CSS-selector element capture, dark mode, device presets, retina scale, PDF paper and page-range controls, custom CSS and JavaScript, click and wait actions, request blocking, headers, cookies, user-agent and Authorization settings, timezone and geolocation, transparent backgrounds, resizing, TTL caching, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage data, and the OpenAPI specification.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan. Create a free ScreenshotNeo account.

FAQ

Can [section] show a numeric section page?

No. It supplies the current section name. Use application-side pagination or separate section objects for a resettable number.

Does --javascript-delay guarantee correct pagination?

No. It only waits a specified time. It does not turn DOM measurements into authoritative PDF page boundaries.

Should I use pageOffset to reset each section?

Only after testing your object-based workflow. The settings reference documents offsets, but does not promise automatic reset behavior at arbitrary headings.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why does the same HTML produce different section counts?

Rendering can change when the binary, patched-Qt build, fonts, margins, paper size, resource timing, or content changes. Reproduce with the exact production environment.

Frequently Asked Questions

Can [section] show a numeric section page?

No. It supplies the current section name. Use application-side pagination or separate section objects for a resettable number.

Does –javascript-delay guarantee correct pagination?

No. It only waits a specified time and cannot reveal final PDF page boundaries.

Should I use pageOffset to reset each section?

Only after testing an object-based workflow; the documented setting does not promise automatic reset behavior at arbitrary headings.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.