How to generate a PDF from a webpage with Node

The standard method of showing job history and experience is, of course, the Curriculum Vitae, so naturally I've included mine of this site. I've had my CV written in HTML and CSS for several years now. Originally it was an example of my skills when I had little else as far as a portfolio went, but I've retained it because a responsive CV works much better for displaying on a website.

However, typically any recruiter or prospective employer wants your CV in a standard, printable format - PDF. You don't want to have to manage two separate versions of your CV every time you make a change so how do you create a PDF version of your web CV?

First of all, you need a printable version of your CV independent of any other layout, such as the header of the site. In my case, I have the CV as its own React component, which means I only need to edit it in one place, and the cv page on the site consumes this component.

The web CV as displayed on the site
The web CV on it's own

Once you can view the CV all on its own, you need to make sure it will actually look good when you print it. That's where the CSS @media print media query comes in. If you're not familiar, it is just like the @media screen media query you use for responsive design, but this is specifically for print. Here you can tweak your design until you're happy.

Protip: Use the break-after property to create natural page breaks

Don't get too attached as you'll likely need to return to this later once you see how the PDF is generated.


So how do we convert this into a PDF? That's where Puppeteer comes in. This API allows us to control a headless Chrome instance using Node.

First of all, let's install it.

npm i puppeteer --save-dev

We then need to create our service which will generate the PDF. I placed mine at utils/generate-pdf.js but you may have somewhere else for your scripts.

const puppeteer = require(`puppeteer`);

(async () => {
    const browser = await puppeteer.launch();
    let page = await browser.newPage();
    await page.goto(`localhost:9000/mycv`); // URL of the document
    await page.pdf({
        path: `./my-cv.pdf`, // path to save the PDF to.
        format: `A4`, // Page format
    });
    await browser.close();
    console.log(`CV pdf generated.`);
})();

Modify the URL to point to your own document, and change the path and file name to suit you.

This script opens Chrome browser in the background, goes to your document, prints a PDF copy, and closes the browser. As these are asynchronous actions, we have an await before them. Simple enough.

Now all we need to do is add this under scripts in our package.json file.

"generate-cv": "node generate-pdf.js",

Once you've changed the path to where you've saved your own script, you can run it via the terminal with npm run generate-cv. Ensure your web document is up and running!

That's it! Run the script and check the generated file to make sure all looks as expected, tweaking your styles as needed. You can add the terminal command to your build process so it will generate a new PDF with each build, so you know it'll always be up-to-date with your current web document. Now it's just a matter of adding a link to that file on your site.

Edit in 2023:

Although it's not too difficult to just run this script manually when you need to, wouldn't it be nice to automate it?

You could run the script as part of the CI pipeline, however that means spinning up a headless Chromium on your CI machine and on something like Netlify that's not as easy as it sounds.

Another option is to use a tool like Husky. This is often used for running things like linters when you commit, but can also be used to run our PDF script.

It's nice and easy to install.

npx husky-init && npm install

It will:

  1. Add prepare script to package.json
  2. Create a sample pre-commit hook that you can edit (by default, npm test will run when you commit)
  3. Configure Git hooks path

We could just get Husky to run our script on every commit, but that would makes things very slow. We only need it to run if we actually change the CV. We can utilise git diff in a conditional for this.

npx husky add .husky/pre-commit "if ! git diff --staged --quiet -- 'src/components/common/cvRaw.jsx'; then npm run generate-pdf; fi"