What we're building
Today we're going to be building a PDF certificate generator. This guide will be split over two parts.
- Part 1: Getting started, building the React certificate template and importing it into Make.
- Part 2: Building the certificate generator application and cleaning it up
When you put it altogether this is what we're cooking π¨βπ³. Check it out
Resources
makecm / certificate-app
A simple react application to generate a PDF certificate using Make.cm
makecm / certificate-template
A simple certificate template that can be forked and imported into Make.cm
Why are we building a PDF generator?
There will come a point in time where a PDF generation service is required functionality for your application. It's a fact of life for anything from an invoice or receipt, to a ticket, or even something professionally printable like a business card or event name badge. Building a PDF generation service is not a new workload. On the contrary - it's a well documented procedure at this point, especially since the rise in popularity of Headless Chrome services over the last few years. But with most, the work is still on you as a developer to create a service that scales to meet demand, produces quality output - every time and is flexible enough to meet new capability as it arises. However these services all fall in the trap of: From Andy Fitzsimon's article, Did you ever make, Make Maintaining and managing a PDF service, especially an ageing one, is a large investment. But it doesn't have to be, nor does it have to take away from what you want to build. That's where we come in at Make.cm. Instead of you having to maintain and manage your services and dependencies, let us do it for you. And while we're at it we'll do it in a completely scalable and severless environment so that every PDF will generate as quickly as possible and every PDF will be of the same quality. And by the way we haven't even talked about post processing functionality after the PDF has been generated ... we do that as well.Read more
1. Getting Started
We're going to be creating two react apps with Create React App (CRA). One for our template that we'll import into Make and then the other react app will be our front end application (we'll go through building our app in Part 2).
To get started let's go ahead and create our two react apps.
$ npx create-react-app certificate-template
$ npx create-react-app certificate-app
CRA gives us a lot of lovely functionality out of the box but for these simple apps we just don't need all of that goodness. For sanity's sake we can strip out the following files in both of your newly created apps.
// certificate-app & certificate-template
/node_modules
/public
/src
App.css
App.js
App.test.js π
index.css π
index.js
logo.svg π
reportWebVitals.js π
setupTests.js π
.gitignore
package.json
README.md
yarn.lock
After deleting those files, you'll have to clean up some broken imports in your App.js
and index.js
The last thing I would suggest doing is installing a really simple CSS reset into both of your react apps. For me I really like minireset.css
$ cd certificate-template
$ yarn add minireset.css
$ ..
$ cd certificate-app
$ yarn add minireset.css
Once minireset.css
has been installed in both apps you can import it the App.js
on both applications with the following.
// App.js
import 'minireset.css';
import './App.css';
function App() {
return <div className="App">{/* OUR APP CODE */}</div>;
}
export default App;
2. Creating our template
Let's spin up our server for certificate-template
$ yarn start
Just as a reminder for this template we're building a certificate template that will need to accept the following:
- Recipient name (
name
-string
) - Completed course name (
course
-string
) - Today's date (
date
-string
)
And this is what our lovely certificate will look like.
If you want to cheat you can fork this repo to your Github and skip to Importing the template into Make.cm.
Adding our structure
In our App.js
file let's setup the following structure.
// App.js
import 'minireset.css';
import './App.css';
function App() {
return (
<div className="App">
<Icon />
<p className="byline">Certificate of completion</p>
<div className="content">
<p>Awarded to</p>
<h1>Name Surname</h1>
<p>for completing:</p>
<h2>Creating PDFs with React & Make.cm</h2>
</div>
<p className="date">
Issued on <span className="bold">March 15 2021</span>
</p>
</div>
);
}
const Icon = () => (
<svg
width="99"
height="139"
viewBox="0 0 99 139"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0 0H99V138.406L52.1955 118.324L0 138.406V0Z" fill="white" />
<path
d="M25.4912 83.2515C25.4912 79.4116 27.0222 75.7289 29.7474 73.0137C32.4727 70.2985 36.1689 68.7731 40.0229 68.7731C43.877 68.7731 47.5732 70.2985 50.2984 73.0137C53.0236 75.7289 54.5546 79.4116 54.5546 83.2515M40.0229 59.724C40.0229 55.8841 41.5539 52.2014 44.2791 49.4862C47.0044 46.7709 50.7006 45.2455 54.5546 45.2455C58.4087 45.2455 62.1049 46.7709 64.8301 49.4862C67.5553 52.2014 69.0863 55.8841 69.0863 59.724V83.2515"
stroke="#0379FF"
strokeWidth="10.6193"
/>
</svg>
);
export default App;
Adding our styles
Let's turn the lights on. Copy the following styles and paste them into the App.css
, replacing what was in there.
/* App.css */
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500&family=Poppins:wght@800&display=swap');
:root {
--blue: #0379ff;
--light-blue: #9ac9ff;
--dark-blue: #0261cc;
--white: #fff;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'IBM Plex Sans', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 20px;
}
.App {
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
color: var(--light-blue);
background-color: var(--blue);
background-image: url('data:image/svg+xml;utf8,<svg width="55" height="45" viewBox="0 0 55 45" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M5.49121 44.2515C5.49121 40.4116 7.02223 36.7289 9.74745 34.0137C12.4727 31.2985 16.1689 29.7731 20.0229 29.7731C23.877 29.7731 27.5732 31.2985 30.2984 34.0137C33.0236 36.7289 34.5546 40.4116 34.5546 44.2515M20.0229 20.724C20.0229 16.8841 21.5539 13.2014 24.2791 10.4862C27.0044 7.77095 30.7006 6.24554 34.5546 6.24554C38.4087 6.24554 42.1049 7.77095 44.8301 10.4862C47.5553 13.2014 49.0863 16.8841 49.0863 20.724V44.2515" stroke="%230261CC50" stroke-width="11"/></svg>');
background-size: 160%;
background-position: 90% 150%;
background-repeat: no-repeat;
padding: 2.5rem;
}
svg {
position: absolute;
top: 0;
}
.content {
position: absolute;
top: 12rem;
right: 2.5rem;
width: 65%;
}
.content * {
margin-bottom: 1rem;
}
.content h1 {
font-family: 'Poppins', sans-serif;
color: var(--white);
font-size: 3rem !important;
line-height: 1;
margin-bottom: 2rem;
}
.content h2 {
font-size: 2rem !important;
font-weight: 500;
line-height: 1;
}
.byline {
position: absolute;
right: 2.5rem;
}
.date {
position: absolute;
bottom: 2.5rem;
font-size: 0.75rem;
}
.bold {
font-weight: 500;
}
So with those styles in there your certificate should look something like this.
If you're OCD like me, you can simulate the export size that we'll be passing to Make by cracking open the Dev Tools (I'm on Chrome so this may differ slightly for other browsers) and clicking on the responsive test tool and popping in 595 x 842, which are the pixel dimensions for an A4 page.
π BEAUTIFUL π
Adding our functionality
With the Make API you can send custom data to your template before generation. So let's prep our template to accept the custom data we want to send it from our application.
As a refresher, this is what we want our template to handle:
- Recipient name (
name
-string
) - Completed course name (
course
-string
) - Today's date (
date
-string
)
When sending custom data to a template Make creates a custom window object called templateProps
that your template can then access.
To access this object in our react template we can add the following to our index.js
and spread these window.templateProps
onto our App.
//index.js
ReactDOM.render(
<React.StrictMode>
+ <App {...window.templateProps} />
</React.StrictMode>,
document.getElementById('root')
);
Once we do that it's as simple as de-structuring the props that we expect to receive from Make in our App.js
and voila we can now accept custom data in our template.
// App.js
function App({ name, course, date }) {
return (
<div className="App">
<Icon />
<p className="byline">Certificate of completion</p>
<div className="content">
<p>Awarded to</p>
<h1>{name}</h1>
<p>for completing:</p>
<h2>{course}</h2>
</div>
{date && (
<p className="date">
Issued on <span className="bold">{date}</span>
</p>
)}
</div>
);
}
However on our local environment because we have no concept of the templateProps
object, we've got no data!
We can however use defaultProps
to simulate what Make would send our template.
// App.js
App.defaultProps = {
name: 'James Lee',
course: 'Creating PDFs with React & Make.cm',
date: 'March 15 2021',
};
3. Prepping & pushing to Github
Now that we've got our template we need to import it into Make.
At the moment Make does not have an application build pipeline. To circumvent this you can build your files locally and push its build folder to Github for import into Make.
To do this successfully we need to do 3 things to our react template before pushing to Github:
- Update the build path
- Update the
.gitignore
- Build our template
Updating the build path
By default when you build a react app, CRA assumes that it will be hosted at the the server root. However in our case our template cannot be hosted at the root when imported into Make.
To allow for correct importing you can add the following to your package.json
to serve all assets relative to the root.
//package.json
"homepage": "./",
Updating the .gitignore
By default git will ignore the build folder, but we need to make special allowances so that we can push up the build folder to Github.
To do so, just remove or comment out the /build
line in your .gitignore
.
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
# /build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
Build it!
$ yarn build
Pushing to a Github repo
Once we've done that create a new blank repository on Github and push up your certificate-template
repository.
Remember to NOT initialize anything in the repository as we're about to send up everything from our local environment.
$ git add .
$ git commit -m "initial commit"
$ git branch -M main
$ git remote add origin git@github.com:[your-username]/certificate-template.git
$ git push -u origin main
Your template code (including the build directory) should be in your new Github repo.
4. Importing our template into Make.cm
Now that we've got out template in Github lets finalize our import.
Jump over to app.make.cm and click Import Template
Choose the repository we just created
Don't see your repo?
Provide your template with a name and the root directory to your build path which in our case will be /build
.
Import it! π
Testing it!
Now that we've imported our template, we can test it via the API playground. You can paste this into the playground and hit Send Test Request.
"size": "A4",
"format": "pdf",
"data": {
"name": "[Your Name]",
"course": "Importing templates into Make",
"date": "Right now"
}
Clicking the resultUrl
link will open the generated PDF, which should look something like this!
Nice one! You've just generated your first PDF out of Make. ππ
Concluding Part 1
Give yourself a pat on the back. You've just created your first Make template and generated your very own PDF out the other side.
In Part 2 I'll show you how you can use your new Make template endpoint in your very own certificate generator app. Grab a coffee, snack or whatever you need to do and when you're ready, dive into Part 2.
Top comments (2)
This is helpful! Thank you!
How does Make.cm work with visualization-heavy PDF (SVG mostly, but I meant it as a more general question) in your experience?
Samuel, I'm glad you liked it π
Data vis works great using SVG - and the output in PDF stays vector most of the time which means great file sizes even with lots of data. Most of the d3 based charting tools work a treat by default. Chartjs works fine too but really shines when you use canvas2svg so there's no bitmaps.
If you can generate your complex visualisation in under 30s then it can be handled synchronously (could be up to 5 pages of data-heavy D3 - hard to guess without knowing your workload but 30s is huge).
For even more complex work, say assembling very large and detailed vector tile sets from OpenStreetmaps via Mapbox you'll likely need to use our async functionality. This means you'll have more time to produce exactly what you need.
Give it a try and I'm sure you'll love the output π