How to Display the Total Page Count in a wkhtmltopdf PDF
Show “Page X of Y” in wkhtmltopdf with footer tokens, custom HTML, correct margins, and fixes for TOC and multi-object totals.

The shortest working command is:
wkhtmltopdf --footer-right "Page [page] of [topage]" input.html output.pdf
[page] is replaced with the current page number, and [topage] is replaced with the final page number for the printed object. The result is a footer such as “Page 1 of 10”. Use a bottom margin so the footer has room to render:
wkhtmltopdf \
--margin-bottom 18mm \
--footer-spacing 4 \
--footer-right "Page [page] of [topage]" \
input.html output.pdf
This guide covers command-line footers, custom HTML footers, numbering offsets, headers, table-of-contents caveats, automation, troubleshooting, and verification.
1. Add a Page X of Y footer from the command line
wkhtmltopdf substitutes special tokens while it converts the document. The most useful tokens for page numbering are:

| Token | Meaning | Typical use |
|---|---|---|
[page] |
Current printed page number | “Page [page]” |
[topage] |
Last page number for the printed object | “of [topage]” |
[sitepage] |
Current page within the current site | Multi-page site jobs |
[sitepages] |
Total pages in the current site | Site-scoped totals |
[frompage] |
First page number in the object | Section labels |
[date], [isodate], [time] |
Conversion date and time | Generated reports |
[title], [doctitle] |
Page or document title | Document identity |
Place the footer on the left, center, or right:
# Right aligned
wkhtmltopdf --footer-right "Page [page] of [topage]" input.html output.pdf
# Center aligned
wkhtmltopdf --footer-center "Page [page] of [topage]" input.html output.pdf
# Left aligned with a line
wkhtmltopdf --footer-left "Page [page] of [topage]" --footer-line input.html output.pdf
Header options use the same pattern: --header-left, --header-center, and --header-right. A header needs top margin and header spacing just as a footer needs bottom margin and footer spacing.
2. Reserve enough space for the footer
A footer can be clipped or overlap document content when the printable margin is too small. Set the bottom margin in millimetres and then tune the gap:

wkhtmltopdf \
--margin-bottom 20mm \
--footer-spacing 5 \
--footer-font-size 9 \
--footer-right "Page [page] of [topage]" \
input.html output.pdf
--footer-spacing is the distance between the document body and footer. Excessive spacing can push the footer outside the page, so increase it gradually. If the footer is missing, inspect the margin first.
3. Use a custom HTML footer
Command-line text is ideal for a fixed label. Use --footer-html when you need CSS, a logo-free layout, conditional content, or several fields on one line. wkhtmltopdf loads the footer file and passes substitution values in its query string.
Create footer.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body { margin: 0; padding: 0; }
body {
font: 9pt Arial, sans-serif;
color: #555;
border: 0;
}
.footer {
display: flex;
justify-content: space-between;
border-top: 1px solid #ccc;
padding-top: 3mm;
}
</style>
<script>
function subst() {
const vars = {};
const query = document.location.search.substring(1).split('&');
for (const item of query) {
const pair = item.split('=', 2);
vars[pair[0]] = decodeURI(pair[1] || '');
}
for (const name of ['page', 'topage', 'frompage', 'sitepage', 'sitepages', 'date', 'isodate', 'time', 'title', 'doctitle']) {
const nodes = document.getElementsByClassName(name);
for (let i = 0; i < nodes.length; i++) {
nodes[i].textContent = vars[name] || '';
}
}
}
</script>
</head>
<body onload="subst()">
<div class="footer">
<span class="doctitle"></span>
<span>Page <span class="page"></span> of <span class="topage"></span></span>
</div>
</body>
</html>
Run it with a margin and spacing:
wkhtmltopdf \
--margin-bottom 18mm \
--footer-spacing 4 \
--footer-html footer.html \
input.html output.pdf
The footer script reads the query string, finds elements by class, and inserts the values. Keep the onload handler; without it, the placeholders may remain empty.
4. Choose between topage and sitepages
For a normal single-input conversion, topage is usually the correct total. It represents the last page for the printed object. sitepages is scoped to the current site and is useful when one conversion contains multiple site pages or objects.
Do not substitute one for the other without checking the desired scope. A multi-object command can contain a cover, table of contents, and one or more page objects. In that case, render a representative PDF and inspect the final number.
5. Start numbering at a different value
If a document is an appendix or follows a cover whose numbering is handled elsewhere, use wkhtmltopdf’s page-offset setting to change displayed numbering:
wkhtmltopdf \
--page-offset 5 \
--footer-right "Page [page] of [topage]" \
input.html output.pdf
The offset changes the displayed numbers; it does not remove physical pages. Confirm how your wkhtmltopdf build applies the offset to the total before publishing, especially when combining objects.
6. Covers, TOCs, and multiple objects
Commands with cover, toc, or several page objects have more complicated numbering. A reported project issue describes [topage] being higher than the actual final page count when a TOC is included. Treat this as a build-specific caveat.
Use this verification checklist for compound jobs:
- Render the complete command, including cover and TOC.
- Open the final PDF and count the physical pages in a viewer or PDF tool.
- Compare the last footer value with that physical count.
- Test a document with short and long sections; page breaks can change totals.
- Keep a representative fixture in CI if the number appears in customer-facing reports.
If the total is wrong only when a TOC is present, test separate conversions or adjust the object structure rather than assuming the token is universally reliable.
7. Automation examples
Shell script
#!/usr/bin/env bash
set -euo pipefail
input="${1:-input.html}"
output="${2:-output.pdf}"
wkhtmltopdf \
--margin-bottom 18mm \
--footer-spacing 4 \
--footer-right 'Page [page] of [topage]' \
"$input" "$output"
Python subprocess
import subprocess
from pathlib import Path
subprocess.run([
"wkhtmltopdf",
"--margin-bottom", "18mm",
"--footer-spacing", "4",
"--footer-right", "Page [page] of [topage]",
"input.html",
"output.pdf",
], check=True)
assert Path("output.pdf").stat().st_size > 0
Node.js
import { spawn } from "node:child_process";
const args = [
"--margin-bottom", "18mm",
"--footer-spacing", "4",
"--footer-right", "Page [page] of [topage]",
"input.html",
"output.pdf"
];
const child = spawn("wkhtmltopdf", args, { stdio: "inherit" });
child.on("close", code => {
if (code !== 0) process.exit(code ?? 1);
});
8. Troubleshooting Page 1 of 10 problems
Footer does not appear
Cause: footer switches were omitted, the bottom margin is too small, or the executable was built without the expected patched-Qt features.
Fix: start with --footer-right, set --margin-bottom 18mm, and run wkhtmltopdf --version to identify the installed build.
The footer is clipped or overlaps content
Cause: insufficient margin or excessive footer spacing.
Fix: increase --margin-bottom slightly, reduce --footer-spacing, and check the result at the target paper size.
It literally prints “[page]”
Cause: the option was passed through a wrapper that escaped the brackets, or the footer HTML did not run its substitution script.
Fix: pass the argument directly to wkhtmltopdf and, for HTML footers, keep onload="subst()" plus the query-string parser.
The total is too high with a TOC
Cause: totals can be inconsistent in compound cover/TOC/multi-object conversions.
Fix: compare against the physical page count, test a single page object, and verify every production template after changing the TOC.
The custom footer is blank
Cause: the footer file cannot be loaded, JavaScript is disabled, or its elements do not use the expected classes.
Fix: use a readable local path, ensure the file contains elements with classes such as page and topage, and confirm the script runs on load.
Numbers change after adding fonts or images
Cause: reflow changes page breaks. A total is calculated after layout, so any content change can alter it.
Fix: wait for all assets, keep print CSS deterministic, and verify the final PDF rather than predicting the count from HTML length.
9. Performance, reliability, and cost considerations
Page-count substitution itself is inexpensive; the expensive work is rendering HTML, fonts, images, JavaScript, and remote resources. For repeatable output, serve assets locally or from stable URLs, specify paper size and margins, and avoid content that changes during conversion.
For batch jobs, reuse a prepared command template and write PDFs to unique output paths. Capture stderr and the exit code. A successful process does not guarantee a correct total in a compound document, so add a PDF inspection step for regulated or customer-visible reports.
wkhtmltopdf is a self-managed executable: budget for installation, patched-Qt compatibility, fonts, sandbox permissions, process timeouts, and storage. If you need screenshots or PDFs without maintaining a browser runtime, an API can move those operational concerns to a service.
10. Or skip the browser setup
ScreenshotNeo provides a website screenshot and PDF API. The same request pattern works from cURL, Python, or Node.js; see the ScreenshotNeo documentation for options.
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}`);
Before capture, cookie banners, newsletter popups, and chat widgets are removed. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. ScreenshotNeo also provides an MCP server so Claude, Cursor, and other MCP clients can take screenshots, inspect pages, and capture PDFs. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
11. FAQ
How do I show Page 1 of 10 in wkhtmltopdf?
Use --footer-right "Page [page] of [topage]" and reserve bottom margin.
Can I put the count in a header?
Yes. Replace the footer option with --header-left, --header-center, or --header-right, then set top margin and header spacing.
What is the difference between topage and sitepages?
topage is the final page value for the printed object. sitepages counts pages in the current site scope.
Can CSS calculate the total page count?
No. The total is known during conversion, so use wkhtmltopdf substitution tokens or a custom HTML footer.
Should I trust topage with a table of contents?
Verify it. Compound objects can produce build-specific totals, including reports of an overly high value with a TOC.


