A programmatically generated Resume

In nextjs, react and tutorialMarch 14, 20224 min read

I like to update my cv now and then, not because I’m looking for something new just for the simple fact of avoiding a lot of work the next time I do.

I don’t enjoy the task when working with the available tooling. I’ve used services like Novoresume because of the simplicity; I could create a CV in no more than an hour, but they all look the same. I’ve also tried making my template in Word. Per the recommendation of this blog, we should always use columns.

But it didn’t work for me. I like to code, and I can get better results this way.

The plan was straightforward:

  • Write a page with my CV
  • Style it as I like
  • Generate a PDF from HTML.

And the stack I’m using for my site is:

The CV

The first step was to code the page. Once you figure out the content, the rest should go quite fast. A bit of research on layouts, and voila, the page is implemented.

At this point we have a React component for the CV that is being used by a Next.js page, something like this

const Resume: FC = () => (
  <div className="mt-8 md:mt-20">
    <CV />
  </div>
);

CV is a React component and even though it looks like HTML we all know that it is not, it is JSX. So we need to render it as HTML. ReactDom exposes a method renderToStaticMarkup that can be use especifically for this situation.

import { renderToStaticMarkup } from 'react-dom/server';
 
const jsx = (
  <div className="greeting">
    <h1>Hello World</h1>
  </div>
);
const html = renderToStaticMarkup(jsx);
console.log(jsx); // { type: "div", key: null, ref: null, props: Object, _owner: FiberNode… }
console.log(html); // <div class="greeting"><h1>Hello World</h1></div>

With this, we can now build the html page that will be used to generate the PDF.

const readyHtml = `
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Yusef Habib - CV</title>
    <link rel="stylesheet" href="http://localhost:3000/build.css" />
  </head>
  <body style="padding: 40px 60px;">
    ${ReactDOMServer.renderToStaticMarkup(CV())}
  </body>
</html>`;

We can do this with npx tailwindcss -o public/build.css --minify. We export it to public so Next.js serves it as a static files. Now we can access it in localhost:3000/build.css.

Great, we are almos done. Last step is to generate the PDF with puppeteer and expose it through our endpoint.

export default async (_: NextApiRequest, res: NextApiResponse) => {
  const args = ['--no-sandbox', '--disable-setuid-sandbox'];
  const html = readyHtml;
  let browser;
  try {
    browser = await puppeteer.launch({ args, pipe: true });
    const page = await browser.newPage();
    await page.setContent(html);
    const pdf = await page.pdf({
      scale: 0.85,
      pageRanges: '1',
    });
 
    res.setHeader('Content-Type', 'application/pdf');
    res.send(pdf);
  } catch (e) {
    console.log(`Error: ${e.message}`);
    res.statusCode = 500;
    return res.json({ error: e.message });
  } finally {
    await browser.close();
    console.log('closed');
  }
};

Lets go through this snippet step by step

  • browser = await puppeteer.launch({ args, pipe: true }); we launch a new browser session
  • page = await browser.newPage(); now we create a new "tab" in the session we just created
  • await page.setContent(html); we set the content of the tab to the html with styles that we previously created
  • const pdf = await page.pdf(); we generate the PDF with some options to fit the content into one page and render this one only

Last but not least we send it back to the client so we can inspect the results in "development mode"

res.setHeader('Content-Type', 'application/pdf');
res.send(pdf);

Finally, we add a couple of npm scripts for convenience. One for testing purposes so we can open the pdf in the browser and the second one to generate the final file that will be pushed into the public folder, ready to be committed.

"build:styles": "npx tailwindcss -o public/build.css --minify",
"pdf:open": "npm run build:styles && open http://localhost:3000/api/pdf",
"pdf:download": "npm run build:styles && curl -o public/cv.pdf http://localhost:3000/api/pdf"

Conclusion

Well, I’m pretty happy with the result. The rendered page looks good, and the generated CV looks as good as the previous one. There might be better ways of generating the PDF. I’m not so happy with the exposed route, but it works, so why should I touch it!

I've created a repo for you to quickly bootstrap this project

What do you think?