Page numbers in a PDF from headless Chrome
A footer template with no font size of its own draws at 0.84 points. It is on the page, it is in the text layer, and it is about a twelfth of the height of body text. Most reports of a missing page number are this, which is why adding the template again does not help.
the same footer, measured on an A4 page 842.88 points tall
no styles of its own yMin 827.32 yMax 828.16 0.84 pt tall
font-size: 9pt yMin 817.60 yMax 827.66 10.05 pt tall
font-size: 12px yMin 817.60 yMax 827.66 10.05 pt tallNine points and twelve pixels are the same size written two ways, and they land on the same two coordinates, which is the control rather than a second result. Any size at all is the fix. The default is not small, it is unreadable.
The footer is a document of its own
Chromium renders each header and footer as a separate mini-document dropped into the margin band. It does not inherit your stylesheet, your font, your colour or your base font size, and the size it starts from is the one measured above.
So every style the footer needs has to be inside the template, inline, including the ones you would never think to write down because the page already has them.
<div style="font-size:9pt;font-family:Georgia,serif;width:100%;padding:0 15mm;color:#555">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>The padding matters as much as the size. The band spans the full page width, edge to edge, and text placed in it with no padding sits against the paper edge rather than lining up with your content.
The margin is not what hides it
The advice you will find first is that the bottom margin is too small to leave room. The same template rendered with the bottom margin swept from nothing to twenty millimetres:
bottom margin swept, footer template unchanged
0mm yMin 827.32 present
5mm yMin 827.32 present
10mm yMin 827.32 present
20mm yMin 827.32 presentThe footer does not move. It is placed relative to the page edge rather than inside the space your margin reserves, so the margin never hides it and enlarging the margin never reveals it.
What the margin does control is whether your own content runs into it. With no bottom margin the body is laid out over the band and the footer is drawn on top, which reads as a footer that has gone wrong rather than one that is missing.
When it really is not there
One setting removes it outright. If the renderer is not told to draw headers and footers at all, the template is ignored and nothing is placed in the band, with no error and no warning that a template was supplied and dropped.
In Puppeteer and Playwright that is displayHeaderFooter, which defaults to off. Supplying a footer template does not turn it on. That is the whole of the genuine-absence case: everything else is the size above.
all of the text in the PDF, three ways
no flag, footer supplied Body
flag on, footer supplied 8/12/26, 3:05 AM Body Page 1 My Invoice Title
flag on, both supplied Body Page 1The first row is the setting on its own: a footer template supplied, no flag, and no footer. The second is the trap underneath it. With the flag on and only a footer given, the header band falls back to Chromium’s own, which prints the date and the document title, and a document that never asked for either acquires both.
So supply both templates, and an empty <div></div> for the band you do not want. The third row is that, and it is the only one of the three carrying exactly what was asked for.
Reading it out of your own PDF
The question is whether the text is there and how tall it is, and both are in the word coordinates:
node -e "
const {execSync}=require('child_process');
console.log(execSync('pdftotext -bbox invoice.pdf -').toString()
.split('\n').filter(l => /Page|of/.test(l)).slice(0,4).join('\n'));"Subtract yMin from yMax. Around one point is the unstyled default and the answer is a font size. Nothing at all is the setting above.
Poppler’s pdftotext is the one with -bbox. Xpdf ships a program of the same name without it, and answers a usage message rather than an error that says so.
A right-to-left footer needs telling twice
Because the band is its own document, direction does not reach it either. A document with dir="rtl" on its root element still gets a left-to-right footer, and the page number lands on the wrong edge.
Right-to-left text and layout has that measured, in points, with the attribute on the template and without it.
How we handle this
PaperPony renders PDFs from HTML through an API. Supplying a header or a footer turns the band on for you, so the setting that removes it outright is not one you can leave off by accident. Everything else on this page applies unchanged, because the templates are passed to Chromium as written:
{
"options": {
"margin": { "top": "20mm", "bottom": "20mm" },
"footer_html": "<div style=\"font-size:9pt;width:100%;padding:0 15mm\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>"
}
}The template guide lists the classes those bands understand, and the quickstart is where to start.