Debugging templates
A template renders without error, but the PDF looks wrong: a line of text runs off the page, a table splits awkwardly across a break, an asset never showed up, a font fell back to something else. This page is about finding out why.
It assumes the setup from Authoring workflow — your templates on your machine, bind-mounted into the PDF Server image. Every command below is run that way.
Turn the debug surface on
The /_debug routes exist only when the server is started with
--debug-enabled:
docker run --rm -p 9999:9999 \
-v "$PWD/templates:/src/templates" \
registry.gitlab.com/c0va23/pdf-server:latest-chromium \
server --debug-enabled
--debug-enabled renders arbitrary caller-supplied data against any loaded
template and lets anyone who can reach the port download the rendered HTML, PDF
and page images, with no authentication of its own. Use it on your development
machine, not on a shared or production host. See
Security model.
Ask for a bundle
One request renders the template and returns everything about that render as a tar:
curl -X POST -H 'Content-Type: application/json' \
-d '{"number": "INV-2026-0152"}' \
'http://localhost:9999/_debug/templates/invoice/render?artifacts=pdf,html,snapshot,png' \
-o bundle.tar
tar -xf bundle.tar
For a dataset you already committed under examples/, a GET is enough:
/_debug/templates/invoice/examples/sample.
Keep that server running while you work. It holds a warm browser pool, so the second render and every one after it costs a fraction of the first — which matters more than anything else on this page once you are iterating.
One-shot, for CI
debug render does the same without a server, and writes the bundle to disk
instead of returning it:
mkdir -p debug-out
docker run --rm --user "$(id -u):$(id -g)" \
-v "$PWD/templates:/src/templates" \
-v "$PWD/debug-out:/out" \
registry.gitlab.com/c0va23/pdf-server:latest-chromium \
debug render --template invoice --example sample \
--artifacts pdf,html,snapshot,png --out-dir /out
Three things about running it this way. The bundle directory has to be mounted
and --out-dir pointed at it: the image's working directory is not writable,
so the default .pdf-debug/ fails with mkdir .pdf-debug: permission denied.
It has to exist before the run — docker creates a missing mount point owned by
root, which is the one directory --user cannot write. And --user is what
makes the bundle yours: the image runs as its own user otherwise, and the files
come back needing a chown before you can read them.
--fail-on error|warn|never decides the exit code, which is the point of
running it in CI at all.
Every example in one call
Swap --example sample for --all-examples and the command renders every
data set the template ships, one bundle each, one line per example:
invoice/sample ok pages=1
invoice/overdue 1 error pages=2 /out/invoice/overdue/problems.txt
invoice/credit failed (data) pages=-
The count is of the worst severity present, and the path is printed only when there is something to read there.
This is the cheapest way to find out where you stand — one browser start-up for the whole set instead of one per render, and one answer to read instead of several. Start here and open a single bundle only for the line that named a problem.
What a bundle contains
problems.txt one line per defect — read this first
problems.jsonl the same findings, one JSON object per line
body.html the markup, before the browser paginated it
snapshot.meta.json the page geometry the browser settled on
snapshot.jsonl one measured box per element, after pagination
document.pdf the render itself
page-001.png one raster per page
manifest.json every artifact, including any that could not be produced
Read them in roughly that order. Each is more expensive to produce and to read than the last, so working top-down finds most problems before you pay for the bottom of the list.
Start with the report
problems.txt is the first thing to read, always:
pages=1 paper=576x720pt content=504x648pt scale=1.0
! overflow-x p1 +40px div.wide "Wider than the content box"
! http-error 404 http://…/assets/logo.png
~ font-substituted Helvetica Neue -> Open Sans (3 nodes)
! is an error, ~ a warning, . informational — read errors first. The
header line is the geometry every distance below was measured against. A clean
render prints ok pages=N no problems and nothing else. Every kind the
report can carry, and what its detail columns mean, is listed under
Reading a render's problems.
A line about an element ends with a short quoted snippet of the actual offending text. Grep that string in your template source — it is usually enough to jump straight to the offending markup without opening anything else.
ok means both halves: a document came out, and nothing was found in it. A
render that reached no verdict at all says so instead, naming the class of
failure and carrying a render-failed record:
failed (template) pages=0
! render-failed template not found: no-such-template
The classes are usage, startup, template, data, render and output,
in the order a render meets them. manifest.json is still where to learn
which artifacts are missing and why; the summary line is enough to know
whether to look.
Pick the right instrument for the layer
Not every defect is the same kind of bug, and the two kinds need different tools:
- If your data produced the wrong markup — a helper returned the wrong
string, a partial didn't render, a value came out HTML-escaped when it
shouldn't have — look at
body.html. The browser hasn't paginated yet at that point, so page breaks and overflow don't show up there; you're checking that the template did the right thing with your data. - If the markup is right but the page layout is wrong — content
overflowing the page box, an awkward break inside a table row, an asset that
404'd, a font that silently substituted —
body.htmlwon't show it either, because none of that is decided until the browser prints. Stay with the report and the layout snapshot.
Asking "which of these two is it" before opening anything is usually faster than opening both and comparing.
Open a page image last
A page image answers a different question than the report does — "does this look right," rather than "what's structurally broken" — and it's the most expensive artifact to look at. Reach for it when the report is clean but the output still looks wrong, or when the question is genuinely about visual judgment: spacing, alignment, balance — the things no automated rule can catch.
What the snapshot does not see
The layout snapshot is measured from the main document only. Elements inside a
shadow root, inside an iframe, or positioned fixed — which repeats on every
printed page but is assigned to one page in the snapshot — are not represented.
If your template uses any of them and the report looks clean, open the page
image.
It is also honest about being incomplete in two ways worth knowing before you
trust a clean report: elements_truncated says the element cap dropped boxes,
and pages_approximate says the printed page count and the measured geometry
disagreed. The page-level rules stand down in both cases rather than guess, and
a cut table also prints a diagnostics-capped line of its own — a report that
judged only part of your document never says no problems. See
Layout snapshot.
Related
- Reading a render's problems — what every reported kind means.
- Layout snapshot — every field the measurement publishes.
- CLI & configuration — the
debug rendercommand and every flag. - OpenAPI / Swagger — the
/_debugHTTP routes. - Tester UI — the same artifacts in a browser, without curl.
- Debugging with a coding agent — this loop, handed to an agent as a skill.