DEV Community

David Garay
David Garay

Posted on

I built a Laravel PDF package because I was tired of debugging screenshots from ghosts

Every HTML-to-PDF system eventually hits the same wall.

Chrome captures too early.

Not because Chrome is broken.
Not because the PDF library is bad.

Because the renderer is guessing.

Sometimes the chart is still animating.
Sometimes the font has not swapped in yet.
Sometimes async data arrived 200ms later on a slow queue worker.

And eventually somebody reports:

“The invoice from 3 days ago looked wrong.”

Now you are debugging a render that already shipped.

No logs.
No screenshot.
No DOM state.
No idea what Chrome actually saw.

I have fought this problem for years across different stacks and projects.

The usual fixes always looked like this:

waitUntilNetworkIdle
delay(2000)
// cross fingers 
Enter fullscreen mode Exit fullscreen mode

It works.
Until it does not.

The core problem is simple:

The renderer does not actually know when your application is ready.

Only your application knows that.

So I built Canio.

The readiness contract

Instead of relying on timing heuristics, the page explicitly signals readiness:

<script>
window.__CANIO_READY__ = true;
</script>
Enter fullscreen mode Exit fullscreen mode

That signal can happen after:

  • fonts are loaded
  • charts finish animating
  • async requests complete
  • Vue hydration finishes
  • whatever “done” means for your document

The renderer stops guessing.

Your application decides when capture happens.

That single inversion changes the entire rendering model.

The real feature is not readiness

The part I actually care about is this:

Every render can leave evidence.

Canio can persist:

  • HTML source
  • DOM snapshot
  • screenshot at capture time
  • console logs
  • network logs

So when someone says:

“This PDF looked broken last Tuesday.”

You do not try to reproduce the race condition.

You open the artifact screenshot and see exactly what Chromium saw.

That changes PDF debugging from archaeology into inspection.

Why this matters

Most HTML-to-PDF tooling treats rendering as:

HTML in
PDF out 
Enter fullscreen mode Exit fullscreen mode

But rendering is not instantaneous.

Modern pages are temporal systems.

Fonts load later.
Animations complete later.
Hydration finishes later.
Data arrives later.

The renderer is sampling a moving target.

Canio treats rendering as a synchronization problem instead of a timeout problem.

Under the hood

Canio uses a Go runtime called Stagehand.

Stagehand talks directly to real Chromium over CDP.

Why Go instead of Node?

Mostly deployment ergonomics.

I wanted:

  • a single static binary
  • predictable memory usage
  • no node_modules in production
  • isolated render infrastructure
  • simpler containerization

Canio supports:

  • embedded runtime
  • remote CDP runtime
  • existing local Chrome

It also installs a pinned Chrome for Testing build automatically.

Browsershot is still excellent

This is important to say clearly:

Browsershot is excellent.

I still use it.

Canio is not trying to replace Browsershot for simple rendering.

Canio exists for the cases where:

  • rendering timing matters
  • deterministic capture matters
  • production debugging matters

That is the lane.

Try it

composer require oxhq/canio
php artisan canio:install
Enter fullscreen mode Exit fullscreen mode

Repo:
https://github.com/oxhq/canio

Feedback and criticism are genuinely welcome.
Especially from people who have fought HTML-to-PDF rendering in production.

Top comments (0)