I want to share an approach more than promote a project, because I'm
curious what this community thinks of the tradeoff.
The problem: every HTML-to-PDF setup I've shipped on headless Chrome
has the same failure mode. The renderer decides when to capture
(network idle + an arbitrary delay), but only the application knows
when the document is genuinely done, webfonts swapped in, Chart.js
finished animating, async data painted. So you end up with
waitUntilNetworkIdle() + delay(2000), it works locally, and then it
flakes on a queue worker in production.
The approach I took in Canio: invert the control. The page declares
readiness explicitly:
window.__CANIO_READY__ = true;
You set it whenever "done" is actually true for that document,
after document.fonts.ready, after the chart's onComplete, after the
fetch resolves, whatever applies. The runtime blocks on that signal
with a timeout fallback. Capture becomes deterministic with respect
to your app instead of the network.
The second piece, which I find more interesting than the readiness
contract itself: every render can persist an artifact bundle, the
exact HTML sent to Chrome, a DOM snapshot at capture time, a
screenshot, the console log, and the network log. When someone
reports "the invoice from three days ago looked wrong," you open
the screenshot artifact and see exactly what Chrome saw, instead of
trying to reproduce a nondeterministic render.
Under the hood it's a Go runtime over CDP (no Node dependency in
the deploy), it ships its own pinned Chrome for Testing, MIT
licensed, Laravel 10–13.
Repo: oxhq/canio (can't post links)
I still use and recommend Browsershot for simple static documents,
this isn't a replacement pitch. Canio is for the cases where
capture timing or production debuggability genuinely bite you.
Genuinely interested in pushback on the readiness-contract model.
Is explicit readiness the right call, or do you prefer keeping the
heuristic and tuning it? What breaks for you with HTML-to-PDF?