Skip to content

Auto-generating your resume for fun and profit

Posted on:March 17, 2023

There are few constants in life—death, taxes, and I will automate anything I can possible get my hands on. As I’ve been putting some work into building a portfolio site recently, I kept thinking what a bummer it would be to pour all this effort into a sweet skills page, just for the PDF version to be out of date.

So after a bit of trial-and-error, I’ve devised a process to ensure my PDF resume always reflects the resume page here on my website. Let’s dive in!

Wanna jump straight to the code? Check out the gist here.

Context

I use Astro for my website, and I’ve really been loving the developer experience. It’s got just enough bells and whistles to let you do what you need to do, but generally gets out of your way. One of the key tenants of Astro is that it’s basically a static-site generator, but lets you utilize islands of reactivity when you need it.

Since the build process is baked into Astro, and this isn’t a highly-dynamic single-page application, it shouldn’t be too difficult to generate a PDF of a given HTML page.. right?

How it went

I started off googling around for “HTML to pdf” solutions, and first stumbled upon html-pdf-node. This package seemed to be exactly what I needed, and I was able to get a PDF generated in a matter of minutes. However, the styles were just a bit… off. I wasn’t really sure why, so I tried to load the HTML from an HTML file:

const fs = require("fs");
const cheerio = require("cheerio");
const pdf = require("html-pdf-node");

// HTML content with some elements having class 'remove-me'
const htmlContent = fs.readFileSync("test.html", "utf8");

// Load the HTML content into Cheerio
const $ = cheerio.load(htmlContent);

// Remove elements with class 'remove-me'
$(".remove-me").remove();

// Generate PDF from updated HTML content
const options = { format: "A4" };
const file = { content: $.html() };
pdf.generatePdf(file, options).then((pdfBuffer) => {
    // Write PDF buffer to file
    fs.writeFileSync("test.pdf", pdfBuffer);
});

Since the whole point of this project was decent styling, and I didn’t want to write a ton of custom styles just for the PDF version of the page, I spent a few minutes trying to hack around this. (To no avail)

I tried things like selectively modifying the href attributes to point to the proper localhost location so I could maybe get the CSS that way. However, that didn’t work, because the astro styles are actually a vite module while running the local dev server, which means when I’m trying to change those href properties, I’m still not actually downloading real CSS files.

Hmm… So what to do?

Puppeteer to the rescue

I found this idea at Bannerbear. But, it did take a bit of tinkering to get working juuuuust right.

Puppeteer is an unbelievably flexible tool. Not only can you use it to automate your personal finance categorization but you can also use it to generate PDFs from HTML pages! As a bonus, you can also modify the DOM before generating the PDF. This allowed me to configure the theme and create some handy pdf-render classes.

Within the build script:

await page.evaluate(() => {
    const htmlElement = document.querySelector("html");
    // This class prevents us from rendering elements with ".no-pdf-render"
    // and consequently WILL render elements with ".render-pdf"
    htmlElement.classList.add("pdf-render");
    htmlElement.setAttribute("data-theme", "dark");
});

Base CSS styles:

html.pdf-render .no-pdf-render {
    display: none;
}

html .pdf-render {
    display: none;
}
html.pdf-render .pdf-render {
    display: block;
}

(As I’m writing this article, I realize that the duplication of the .pdf-render class is confusing. I already stepped on my own footgun here, so I don’t recommend using the same class names for the global switch html.pdf-render, and the individual switches html *.pdf-render 🙃)

Then you can use the utility classes like so:

<!-- Technically, we _could_ render this /contact link in the PDF, and it would work, but it felt weird so I just didn't do it -->
<p class="no-pdf-render">
    What's to lose? <LinkButton href={"/contact"}>👉🏻 Connect With Me</LinkButton>
</p>

These CSS classes are especially useful if you consider the end-user experience. If you’re on my site, there’s a good chance you’ve either talked to me, or at least received the link from a 2nd-order connection. (Who knows, maybe one day I’ll be famous and this will no longer be true). But if someone just sends you the download link for my resume then you may not even know my name. So my name, on the site, may be redundant information, but it’s quite pertinent to have that somewhere near the top of the PDF. Check out below to see how the selective adding / removing of information really makes the pdf-version feel more “official-resume” than the web-version. (Which was the whole goal)

Web Version This is the web version of the resume page (as of the time of this writing)

PDF Version This is the result generated from the previous image

With this solution, I can selectively render this page in whatever way I need to build a nice-looking resume for PDF format. The PDF format is a bit more traditional than the skills page, but still contains the general style of the site.

⚠️ Plans For Biggering & Bettering This ⚠️

I’m going to add a husky script on pre-commit to auto-detect when I change the resume page. This will relieve me of ever needing to remember the build script!

Surprise! I finally got around to this. The solution is pretty simple and keeps things nicely in sync!


Thanks for reading! If you enjoyed this, you may also enjoy following me on twitter!