Skip to content
All writing

wkhtmltopdf is dead. I kept using it anyway.

August 9, 2026PDFGoChromiumOpen Source

wkhtmltopdf turns HTML into PDFs. Payslips, invoices, the boring documents a business runs on. It was archived in 2023, and the browser engine inside it is a WebKit build from roughly fourteen years ago.

No CSS custom properties. No working calc(). No Grid. It will never render @layer or a container query, because those need a browser from 2022 or later.

The usual advice is to migrate to headless Chrome, WeasyPrint or Gotenberg. Good advice for a new project. Less good when years of production have wired --header-html, --footer-center "[page] of [topage]", cover and toc into code nobody has opened since 2019.

The tool isn’t the problem. The engine is. So I kept the command line and replaced what was behind it.

bilihtmltopdf

bilihtmltopdf is a single Go binary that answers to wkhtmltopdf. Same flags, same path, same behaviour, rendered by a current Chromium over the DevTools Protocol with pdfcpu assembling the pages.

It keeps headers and footers, cover pages, table of contents, outlines and links. Modern CSS works. Legacy switches with no modern equivalent warn and carry on instead of failing, and --strict makes them fatal if CI should be picky. A headless Chromium ships with it, so there’s no browser to provision and no service to run.

curl -fsSL https://raw.githubusercontent.com/rvanbaalen/bilihtmltopdf/main/setup.sh | bash

The installer backs up whatever binary was there, so undoing it takes five seconds.

The part that fought back

Chromium has a built-in header and footer feature. It runs in a sandbox that can’t load your stylesheets and ignores absolute positioning, so a real footer comes out as naked text in a corner. I shipped that first and it was wrong.

wkhtmltopdf never used templates. It rendered headers and footers as full pages with their real CSS and stamped them onto each sheet. Doing the same fixed the layout, and because the compositing runs over the merged document, page numbers now count continuously across cover, contents and body.

How it was built

Agentic tooling, with Fable 5 doing the work: engine research, the Go, the release pipeline. The constraints came from me, and those decided whether any of it worked.

I fixed the drop-in requirement before any code existed, which ruled out most of the obvious designs straight away. I had the model research QtWebKit and QtWebEngine properly so both could be dropped on evidence rather than after a week of building against each. And when the first footer came back, it looked close enough to ship. Knowing how wkhtmltopdf composited headers internally is the only reason I could tell the approach was wrong rather than the CSS.

The model moves quickly in whatever direction you point it. Choosing that direction, and catching output that is subtly wrong, is still the job.