DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Reading an HTML File, Parsing It and Converting It to a PDF File With the Pdfbox Library
  • How to Get Plain Text From Common Documents in Java
  • Next-Gen Lie Detector: Stack Selection
  • Introduction To Template-Based Email Design With Spring Boot

Trending

  • Docker Base Images Demystified: A Practical Guide
  • The Evolution of Scalable and Resilient Container Infrastructure
  • Start Coding With Google Cloud Workstations
  • Designing for Sustainability: The Rise of Green Software
  1. DZone
  2. Coding
  3. JavaScript
  4. How to Generate Server-Side PDF Reports With Puppeteer, D3, and Handlebars

How to Generate Server-Side PDF Reports With Puppeteer, D3, and Handlebars

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 search. You’ve come to the right place.

By 
Carlos Lantigua user avatar
Carlos Lantigua
·
Aug. 10, 20 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
11.5K Views

Join the DZone community and get the full member experience.

Join For Free

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.

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

Handlebars 

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. 

Shell
 




x


 
1
npm install handlebars


Example using partials and built-in blocks 

HTML
 




xxxxxxxxxx
1
11


 
1
{{#each poleComparison as |page|}}
2
<div class="page">
3
  {{#each page.pairs as |polePair|}}
4
    {{> comparison-header polePair=polePair }}
5
        <div class="comparison-tables">
6
            {{> comparison-body polePair=polePair }}
7
        </div>
8
  {{/each}}
9
  {{> footer @root }}
10
</div>
11
{{/each}}


Puppeteer

A node library that will provide us access to a chrome headless instance for generating the PDF based on our compiled Handlebars templates. 

Shell
 




xxxxxxxxxx
1


 
1
npm install puppeteer


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) 

D3.js is a JavaScript library for manipulating documents based on data. D3 helps you bring data to life using HTML, SVG, and CSS. D3’s emphasis on web standards gives you the full capabilities of modern browsers without tying yourself to a proprietary framework, combining powerful visualization components and a data-driven approach to DOM manipulation. 

HTML
 




xxxxxxxxxx
1


 
1
<script src="https://d3js.org/d3.v5.min.js"></script> 


Step One: Setting Up Puppeteer & Handlebars 

First, we’ll create a directory for our PDF then import the required modules. This will be a JavaScript file that we’ll place within the server-side structure of our application. We can call this generatePDF.js for convenience.  

JavaScript
 




xxxxxxxxxx
1


 
1
const puppeteer = require("puppeteer"); 
2
const hbs = require("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. 

JavaScript
 




xxxxxxxxxx
1
11


 
1
const puppeteer = require("puppeteer");
2
const hbs = require("handlebars");
3
 
4
const compile = async (templateName, data) => {
5
    const filePath = path.join(__dirname, "templates", `${templateName}.hbs`);
6
    if (!filePath) {
7
        throw new Error(`Could not find ${templateName}.hbs in generatePDF`);
8
    }
9
    const html = await fs.readFile(filePath, "utf-8");
10
    return hbs.compile(html)(data);
11
};


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.    

JavaScript
 




xxxxxxxxxx
1
15


 
1
let browser; 
2
const generatePDF = async (fileName, data) => {
3
    try {
4
        if (!browser) {
5
            browser = await puppeteer.launch({
6
                args: [
7
                "--no-sandbox",
8
                "--disable-setuid-sandbox",
9
                "--disable-dev-shm-usage"
10
                ],
11
                headless: true,
12
            })
13
        }
14
    } catch (err) {
15
        ...


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. 

JavaScript
 




xxxxxxxxxx
1
12


 
1
                ],
2
                headless: true,
3
            })
4
        }
5
        const context = await browser.createIncognitoBrowserContext();
6
        const page = await context.newPage();
7
        const content = await compile(fileName, data);
8
 
9
    } catch (err) {
10
        ...
11
    }
12
}


Now we’ll set up our content and tell puppeteer to wait until everything is loaded before rendering the PDF. 

JavaScript
 




xxxxxxxxxx
1
12


 
1
        const content = await compile(fileName, data);
2
 
3
        await page.goto(`data: text/html, ${content}`, { 
4
            waitUntil: "networkidle0" 
5
        });
6
        await page.setContent(content);
7
        await page.emulateMedia("screen");
8
 
9
    } catch (err) {
10
        ...
11
    }
12
}


* 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). 

JavaScript
 




xxxxxxxxxx
1
14


 
1
        await page.emulateMedia("screen");
2
 
3
        const pdf = await page.pdf({
4
            format: "A4",
5
            printBackground: true,
6
        });
7
 
8
        await context.close();
9
        return pdf;
10
 
11
    } catch (err) {
12
        ...
13
    }
14
}


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. 

Plain Text
 




xxxxxxxxxx
1


 
1
our_report.hbs


HTML
 




xxxxxxxxxx
1
13


 
1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
  <meta charset="UTF-8">
5
  <title>Our Cool PDF Report</title>
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
7
  <script src="https://d3js.org/d3.v5.min.js"></script>
8
</head>
9
  <body>
10
      <div>
11
        <p>Hello World<p>
12
      </div>
13
  </body>


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. 

HTML
 




xxxxxxxxxx
1
17


 
1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
  <meta charset="UTF-8">
5
  <title>Our Cool PDF Report</title>
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
7
  <script src="https://d3js.org/d3.v5.min.js"></script>
8
</head>
9
  <body>
10
      <div>
11
        {{#with Data as |myData| }}
12
         {{#each myData.text as |text| }}
13
         <p>{{text}}<p>
14
         {{/each}}
15
        {{/with }}
16
      </div>
17
  </body>


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.  

JavaScript
 




xxxxxxxxxx
1


 
1
const generatePDF = require('./generatePDF.js');
2
 
3
const generateReportWithData = reportData => {
4
 return generatePDF("our_report", reportData);
5
  }


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 

Plain Text
 




xxxxxxxxxx
1


 
1
style.hbs 


JavaScript
 




xxxxxxxxxx
1
11


 
1
:root {
2
    --font-s-small: 8pt;
3
    --font-s-normal: 10pt;
4
    --font-s-mid: 12pt;
5
    --font-s-large: 14pt;
6
    /* Kerning */
7
    --ltr-spc-200: 0.2em;
8
    --ltr-spc-100: 0.1em;
9
    --ltr-spc-020: 0.02em;
10
    --ltr-spc-025: 0.025em;
11
}


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. 

HTML
 




xxxxxxxxxx
1
16


 
1
<style>
2
html, body {
3
    height: 100%;
4
    margin: 0;
5
    padding: 0;
6
    }
7
.page {
8
    background: white;
9
    display: block;
10
    margin: 0 auto;
11
    margin-bottom: 8.5em;
12
    /* Size = A4 */
13
    width: 21cm;
14
    height: 29.7cm;
15
    padding: 5em 30px 0 30px;
16
    position: relative;


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. 

JavaScript
 




xxxxxxxxxx
1


 
1
hbs.registerPartial("style", fs.readFileSync(
2
    path.join(__dirname, "/path/to/style.hbs"), "utf-8"),
3
);


Now that it has been registered as a handlebars partial, we can simply bring it into our template. 

HTML
 




xxxxxxxxxx
1
11


 
1
{{> styles }}
2
</head>
3
  <body>
4
      <div class="page">
5
        {{#with Data as |myData| }}
6
         {{#each myData.text as |text| }}
7
         <p>{{text}}<p>
8
         {{/each}}
9
        {{/with }}
10
      </div>
11
  </body>


Step Four: Adding in Some D3 

We already brought in the D3 CDN link into our template header 

HTML
 




xxxxxxxxxx
1


 
1
<script src="https://d3js.org/d3.v5.min.js"></script>


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. 

JavaScript
 




xxxxxxxxxx
1


 
1
hbs.registerPartial("d3_script", fs.readFileSync(
2
    path.join(__dirname, "/path/to/d3_script.hbs"), "utf-8"),
3
);


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. 

Plain Text
 




xxxxxxxxxx
1


 
1
my_template.hbs


HTML
 




xxxxxxxxxx
1
13


 
1
     {{/each}}
2
    {{/with }}
3
  </div>
4
  <div class="canvas"></div>
5
</body>
6
 
7
<script type="text/javascript">
8
const svg = d3
9
    .select('#canvas')
10
    .append('svg')
11
    .attr('viewBox', [-width / 2, -height / 2, width, height]);
12
</script>



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.  

PDF Template JavaScript library HTML Plain text

Published at DZone with permission of Carlos Lantigua. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Reading an HTML File, Parsing It and Converting It to a PDF File With the Pdfbox Library
  • How to Get Plain Text From Common Documents in Java
  • Next-Gen Lie Detector: Stack Selection
  • Introduction To Template-Based Email Design With Spring Boot

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!