How I automated CV generation on my blog
I’m in the moment in my life where I’m trying to automate stuff that I consider boring. Recently I just automated the creation of my resumé on my website.
The Problem
For some time I would just download my LinkedIn profile as .pdf
and apply. But I had some issues that really bothered me, like the skills displays messing up everytime I changed something on my profile and that feeling that recruiters might think I’m being too careless to use a generic tool to generate my CV.
Then I tried improving things: I started using a template on Figma.
I must confess I was really satisfied with the design, but with the work I had when I had to change the information was not worth it. Besides, the PDF exported by Figma was so big I always had to compress it before using it.
So I decided I would build my own, but without having to worry about clicking layers or external tools.
The Solution
How to generate the PDF
I did some search and found out a good tool to generate a PDF server side: puppeteer. And I chose handlebars to generate the HTML since I already knew how to use it before (as I write this, I’m looking for other alternatives, but haven’t implemented yet, ping me if you would like to help)
Astro
Ok, but how to do it using astro (the tool I use for my blog)?
Well, for that, I static file endpoint that would be responsible for fetching/generating the PDF at build time (so I don’t have to manually build it).
In cv.pdf.ts
:
import { readFileSync } from 'node:fs';
import handlebars from 'handlebars';
import { DateTime } from 'luxon';
import { personalData } from '../data/personal';
import type { APIRoute } from 'astro';
export const GET: APIRoute = async () => {
const htmlTemplate = readFileSync('src/hbs/cv-template.hbs', 'utf8');
// Generating the HTML
const template = handlebars.compile(htmlTemplate);
const html = template(personalData);
// Connecting to puppeteer in order to create the PDF
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
await page.setContent(html);
const pdf = await page.pdf({ format: 'a4' });
await browser.close();
return new Response(pdf, { headers: { 'Content-Type': 'application/pdf' } });
};
The code above works wonders on development!
Time to ship - another problem
As soon as pushed my changes that triggered vercel build, I realized that the build failed since chromium could not be installed (puppeteer runs a version of it).
Turns out vercel had limitations with the free plan I’m using, so I couldn’t use puppeteer.
Another Solution
In order to fix that, I decided to create my own API that renders the PDF for me, it’s a small project I created just for it, it’s open sourced.
Then, going back to my blog code, I created a function to fetch the PDF from the generated HTML:
type PDFResponse = {
pdf: string;
};
async function fetchPdf(html: string): Promise<Buffer> {
const response = await fetch(import.meta.env.PDF_ENDPOINT, {
headers: { 'Content-Type': 'application/json', authorization: import.meta.env.PDF_GEN_API_KEY },
method: 'POST',
body: JSON.stringify({ html })
});
if (!response.ok) {
throw new Error(`PDF Api not working: ${(await response.json()).error}`);
}
const data: PDFResponse = await response.json();
return Buffer.from(data.pdf, 'utf-8');
}
Then I fixed the GET
route:
export const GET: APIRoute = async () => {
const templateHtml = readFileSync('src/hbs/cv-template.hbs', 'utf8');
const template = handlebars.compile(templateHtml);
const html = template(personalData);
const pdf = await fetchPdf(html);
return new Response(pdf, { headers: { 'Content-Type': 'application/pdf' } });
};
And that worked.
I changed. Again
I was not satisfied with using handlebars and I found out ReactPDF.
I could make it work by using renderToFile
on cv.pdf.ts
. I got stuck in a problem with fonts where the font would not change from the default one, no matter what I did, so I left it.
Yes, I abandoned all of this and I am currently just using a nice overleaf template.
Conclusion
Do you have any recommendations? Make sure you contact me.