Jose Santos
Aug 14 · 6 min read

Introduction

At GumGum Sports we strive to give the most value to our customers every day. Understanding our clients is fundamental, so we anonymously collect several metrics to give us a picture of how our clients interact with our products.

Based on our metrics, we found that one of the most used pages in our applications is what we call “Insights”. This page allows our clients to generate custom tables and graphs based on all the data that we aggregate for them.

Working with our clients, we understood that they were accessing this data to make customized reports to be shared with their partners. Knowing this we sought ways for our clients to easily generate all the reports they needed.

That’s how we came up with the idea of generating custom reports that our customers could quickly download, print and share with others without having to manually aggregate data and create reports on their own. One of the most standard formats for sharing reports are PDFs, and that’s how our journey begins.

In this article, I’ll guide you into generating cool-looking reports by simply calling a function in the cloud (serverless):

Pre-requisites

  • Node 10.15.x or older
  • Npm 6.9.x or older

Installation

First, we need to install serverless globally:

npm install --global serverless

Then we can clone the following boilerplate code:

git clone git@github.com:JMSantos94/pug-a-tron.git

This includes the setup for serverless, webpack, docker, and other goodies that will save you a ton of time.

What’s Included

First, let’s talk about the serverless.yml file, which includes the configuration for our lambda. While ours works around AWS’s services, Serverless is platform-agnostic, and the changes should be minimal if you prefer other options like Google Cloud Functions or Azure’s Serverless.

service: demo-pdf-generationprovider:
name: aws
runtime: nodejs10.x
stage: ${opt:stage, 'dev'}
functions:
pug-a-tron:
role: customRole
handler: src/index.handler
events:
- http:
path: pdf-generator
method: get
cors: true
plugins:
- serverless-offline
- serverless-webpack
package:
individually: true
custom:
webpack:
webpackConfig: './webpack.config.js'
includeModules: true,
packager: 'yarn'

Our configuration is pretty standard, it is highly recommended that you use serverless-webpack since we will be using React for our PDF’s templates. Another highly recommended plugin is serverless-offline for a great local development experience.

The boilerplate also includes an example for a custom role that will allow you to upload your PDFs to AWS’s S3.

There is not much to talk about regarding webpack’s configuration. Feel free to modify the boilerplate’s configuration as you see fit.

const path = require('path');
const slsw = require('serverless-webpack');
const nodeExternals = require('webpack-node-externals');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
stats: 'minimal',
entry: slsw.lib.entries,
mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
resolve: {
extensions: ['.js', '.jsx', '.json'],
},
target: 'node',
optimization: { /* ... */ },
performance: { /* ... */ },
devtool: 'nosources-source-map',
externals: [nodeExternals()],
plugins: [ /* ... */ ],
module: {
rules: [
{
test: /\\.jsx?$/,
exclude: /node_modules/,
use: 'babel-loader',
},
/* ... */
],
},
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, '.webpack'),
filename: '[name].js',
sourceMapFilename: '[file].map',
},
};

Just make sure to keep our babel-loader plugin which will allow us to transpile React’s JSX files. This is our .babelrc file to do so:

{
"comments": false,
"presets": [
[
"@babel/env",
{
"targets": {
"node": "10.15"
}
}
],
"@babel/preset-react"
],
"plugins": ["source-map-support"]
}

Now to the more interesting bits. To generate PDFs we use react-pdf, which uses React for its layout/templates. Let's look at an example to generate a basic PDF:

import React from 'react';
import ReactPDF, { Text, Font, StyleSheet, Document, Page } from '@react-pdf/renderer';
const handler = async (event, ctx) => {
const pdfStream= await ReactPDF.renderToStream(
<Document>
<Page style={styles.body}>
<Text style={styles.title}>
Hello World
</Text>
</Page>
</Document>,
);
return { /* ... */ };
};
Font.register({
family: 'Oswald',
/* ... */
});
// PDF Styles
const styles = StyleSheet.create({ /* ... */ });
export { handler };

Note how the markup seems pretty familiar if you have worked with React before. It is important that your PDF is wrapped with a <Document> component. The <Page> component is used to break your layout between multiple pages, and <Text> component is necessary to render any kind of text.

For more information, it is highly recommended that you visit the docs: https://react-pdf.org/components

It is also worth mentioning that not all CSS properties work on react-pdf and some will behave quite differently from what they do on the web, so be warned! Here is the list of supported properties: https://react-pdf.org/styling#valid-css-properties

Now a report wouldn’t be complete without graphs. For those we use chart.js. It uses the canvas API on the browser to generate graphs. However, this ended up being more challenging than we expected. All canvases in react-pdf were rendered blank, so after tinkering for a while, we decided to transform the charts into images then include them on the PDF.

To do so we used the package chartjs-node-canvas which in turn uses node-canvas that down the road uses cairo for 2D graphics, and pango for text rendering. Understanding all this will pose crucial later on. You see, when you run your code in serverless you don’t know much about the machine that you are running on unless you do some digging.

What we found is that amazonlinux and amazonlinux2, the images that Amazon runs your functions on, are the bare minimum to run your code. With things like cairo or pango that require lots of utilities to be included on the operating system, this was a big issue.

FROM amazonlinux2:latestRUN curl --silent --location <https://rpm.nodesource.com/setup_10.x> | bash -
RUN yum install -y nodejs zip awscli
RUN npm install -g yarn serverless
RUN mkdir /app
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn --pure-lockfile
COPY . ./CMD sls deploy --stage prod

Conveniently amazon provides their Linux images on Dockerhub, so we can emulate locally what our functions will encounter once we upload them to AWS Lambda.

One final thing that we must do in order for pango to have fonts to work with (remember, your code runs in a very limited machine) is to include our fonts.

First, we must create a font.conf file that looks like this:

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<dir>./fonts</dir>
<cachedir>/tmp/fonts-cache/</cachedir>
<config></config>
</fontconfig>

We recommend that you place it in the root of your functions. Then, in your function’s main file, preferably close to the top of your file. We should set an environment variable pointing to our file.

process.env.FONTCONFIG_PATH = __dirname;

That’s all the configuration that we have to make. Now you can upload your fonts to the /fonts folder. Do not forget to register them on node-canvas like this:

const { registerFont } = require('canvas');registerFont(
'./fonts/your-custom-font.tff',
{ family: 'customfont' }
)

Note: it seems that only .tff fonts are supported by react-pdf.

Deploying

If you cloned the boilerplate, it should be as simple as running the two following commands:

yarn run docker:build
yarn run docker:run

These two commands should build our docker image and run it. Do not forget to set up your AWS credentials in your machine. Usually located in the ~/.aws folder. Serverless will automatically pick them up.

Deploying to lambda requires lots of permissions, so make sure that your account is privileged enough.

If everything went well, you should have a function that uploads your PDFs to S3 and returns you a link. Like this:

With some extra work, you can achieve really impressive reports. Here is a sample of one of the reports we generate (note: some data points have been changed or removed for demonstration purposes):

Conclusion

While at first, all this configuration may be a bit too much, it is only necessary once. The boilerplate should have it all set for you. We found this alternative to be the best for our goals since other PDF generators simply generate images by rendering your app’s HTML.

After we implemented this feature, our clients generated more than 80 reports in less than a week. Many were surprised that we could generate PDFs on demand. We hope that this guide helped you as well to make your app that much more special.


We’re always looking for new talent! View jobs.

Follow us: Facebook | Twitter | | Linkedin | Instagram

gumgum-tech

Thoughts from the GumGum tech team

Jose Santos

Written by

Senior Frontend Developer @ GumGum, Inc.

gumgum-tech

Thoughts from the GumGum tech team

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade