Looking for a way to create a design-heavy, data-driven, beautifully styled PDF report—server-side with similar tools to what you are already using on the front-end? Stop your Google search. You’ve come to the right place. I was in the same boat as you a few months ago while helping a client with this exact problem. In order to accomplish this feat, I developed a four-step solution using Puppeteer, D3, and handlebars. In this post, I’ll give you step by step instructions on creating server-side pdf reports. Let’s dive in.
An example of a PDF page generated using this method.
In this post, we’ll cover:
- Setting up Puppeteer and Handlebars
- Creating a generator to make our PDF
- Building out a handlebars template
- Adding the finishing touches
The CHallenges of Creating These PDF Reports:
Because we’re using a template framework to access standard web technologies along with Puppeteer to manage the PDF, we’ll need to think about these things during development:
- Pages will manually need to be constrained.
- We won’t have access to CSS media props other than “screen.” (no “page-break-after” or the print media type)
- We won’t be able to use dev tools to debug irregularities once the PDF is compiled and rendered.
- Puppeteer itself adds extra build time and size to your deployments.
- Generating a report can take a while depending on file size.
For this example, let’s assume we already have the base of our project up and running Node/Express, and some type of ORM and DB solutions. We’re all set to feed our sweet, sweet data into a report.
The Tools We Need to Make This Happen
HTML templating framework from the Mustache family. This allows for Partial templating (fancy talk for components) and custom and built-in helper functionality to expand on our logic.
Example using partials and built-in blocks
A node library that will provide us access to a chrome headless instance for generating the PDF based on our compiled Handlebars templates.
A list of use cases:
- Generate screenshots and PDFs of pages.
- Crawl a SPA (Single-Page Application) and generate pre-rendered content (i.e. “SSR” (Server-Side Rendering)).
- Create an up-to-date, automated testing environment.
- Test Chrome Extensions.
D3 (Data-Driven Documents)
Step One: Setting Up Puppeteer & Handlebars
Next, we’ll need to let handlebars compile our template. We will create a compile function which will locate the .hbs file and use the Handlebar’s built-in compile method to do this.
This method allows us to also inject the data that we will be using into our template.
Finally, we’ll want to set up our generatePDF function. Its job will be to open a Puppeteer headless chromium instance to convert our template into PDF format.
We’ve passed some configuration options to our Puppeteer browser that will make it headless and lightweight. We also don’t want multiple browsers to be open at the same time, this can cause performance issues when generating multiple reports.
Next, we’ll be creating a new incognito browser context. We’ll use this instead of the usual context method because it won’t share cookies/cache with other browser contexts. This is helpful for other features of Puppeteer but won’t be needed for this process.
Now we’ll set up our content and tell puppeteer to wait until everything is loaded before rendering the PDF.
* page.goto takes a URL string and config options. We won’t be traveling to a URL, instead we’ll be utilizing our compiled html
* emulateMedia changes the CSS media type used on the page. We’ll want our media type to reflect the CSS used for screens.
We’ll get to set our page format so that Puppeteer knows how to render. Keep in mind that Puppeteer has no concept of where we want to split our actual content (that will be handled later through our templates CSS).
Step Two: set Up Our Handlebars Templates
We’ll start by creating our first handlebars template file for our report. Notice that the syntax looks and acts just like regular HTML.
Let’s have Our Data Brought Into Our Template
We can use some handlebars built-in blocks to help us interact with the data that we injected earlier in our compile function. We can use the “with” block to gain context to the data that we need, then an “each” block to iterate over it.
Now We Can Add Some Process to Generate Our PDF
Now in our Node app we can use our generatePDF function to create our PDF. This would be the time for you to decide what you ultimately want to do with the report. You could store it in your database, serve it to the client-side, or stash it into an S3 bucket. There’s a lot of freedom here depending on what your application’s needs.
If you have different types of reports, we can take this opportunity to toss in a switch statement and some logic to decide which report to generate.
Step Three: Build Out a Handlebars Template
Now we can set up our template styling. We’ll create a file called style.hbs. I like to set up global variables CSS variables to keep me honest with a lot of my styling. pt is a recommended unit for printable documents and I found that px didn’t always work so well for text. I also found that em units translate better for letter spacing than pixels. This made it easier to match the design kerning/letter-spacing when converting the values.
Building Out The Page Constraints
If you recall, we talked about how Puppeteer has no context on when we want to split up our document into pages. It will generate the PDF and break up the pages appropriate regardless of where our content sits. This means that our content will just spill over to the next page automatically when it overflows, and we don’t want that as we would rather be in control. We’ll add some styling to let our HTML body continue forever and a page container which will match the constraints of an A4 formatted page. If you are using a different format, you’ll need to plug the numbers for that in the height and width of the page container.
In the File Where You Setup Puppeteer and Handlebars
Since we created a style.hbs file, we’ll want to register as a partial so that we can just plug it into our template. This way we won’t have to jam all of our styles into our main template file and can reuse the code if we need to.
Now that it has been registered as a handlebars partial, we can simply bring it into our template.
Step Four: Adding in Some D3
We already brought in the D3 CDN link into our template header
Now it’s time to create a partial for our D3 script. We’ll do this using the same method that we used to create the style.hbs partial. Register a d3_script.hbs file as a Handlebars helper.
Then we can drop it into our main template as needed. Also note the canvas anchor div used in the template to give our D3 a foundation to start from.
And there you have it. Let me know your thoughts on this solution to creating server-side pdf reports, and if you run into any issues, feel free to contact me.