How to generate beautiful-looking PDFs
in Single Page Applications
One of the topics that I keep on running into in the past ten years is generating beautiful-looking, printable documents out of web-applications.
In the times before XMLHttpRequest
life was easy: We generated an HTML page using Symfony or Zend Framework and ran it through wkthmltopdf — a headless browser — to convert it to a PDF.
Now, in 2022, most web applications we build are split between frontend and backend. In our case, the frontend is a Single Page Application built with Vue.js and TypeScript. The backend is built with Django. Communication goes via GraphQL or REST.
We recently had a case for the product realmatch360.com, a PropTech solution using big-data to give insights on a potential home-owner’s willingness to pay. It generates colorful diagrams in the browser using ChartJS.
Now, how can we allow the user to print those reports?
Firefox renders differently than Chrome 🤦♂️
Our initial idea was straightforward: we simply use CSS3 Media Queries (@media print
) to create a print-optimized version of the rendered document and use window.print()
to print the page.
Unfortunately, the output looked different in each browser: Perfect in Chrome, but not-so-nice in Firefox.

Firefox on the right does implement page break CSS rules differently
Moreover, it was not possible to control header and footer of the print version.
How can we get a consistent, good-looking output regardless of the user’s browser?
I remembered the old age: We could use the headless browser approach. This would require that the document is generated on the server. But on the server, we would have to restore the state of the page as the user sees it in the client. In our case, this would mean that we would have to pass various arguments from the client to the server, start a headless browser, e.g. Chromium, to generate the same page as the user is seeing locally.
This feels not elegant, as the page would be generated twice: Once in the user’s browser. And once on the server.
So, we came up with a creative solution: We tried to send the generated content as HTML markup of the already rendered page from the client to the server:
Simplified version of the client-side code
The server receives the HTML and converts it to a PDF using pyppeteer (the Python port of puppeteer). Here’s a simplified version of the code in JavaScript. The full production code can be found at the end of the article 👇
Simplified version of the server-side code
This is how the process looks like:

This worked surprisingly well.
Gotchas ahead! 🚨
The Chart.js graphs were rendered as bitmap in an HTML Canvas element. It was not possible to serialize this bitmap and send it to the server.
The solution was to send the base64 encoded PNG image of the charts to the server instead of Canvas.
Second problem: Resources were not found, the reason was the relative URLs. In the context of the server, they were pointing to nowhere.
The solution was to add a <base href="//url-of-the-app.com">
in the head of the document.
Once these problems were solved, the solution was production-ready. Even things like Fonts or SVG content were rendered correctly in the PDF. 🙌🏻
If you want to try this yourself, check out this proof of concept and look at the code on GitHub.
Thanks to Alexey Matsuk for the review of the article.
Appendix
This is the code we finally ended using in production on the server side. It works reliably for two months now.