Update: Feb 15, 2025
I no longer support the PDF-version described below for a few reasons.
1. I now run my own business! (Hooray!) So I don't often need to refer anyone to my official resume.
2. I migrated my website from Astro to Rails. The process of generating PDFs, which was originally supported by Puppeteer, became a bit more convoluted and I didn't want to spend the time migrating the functionality.
There are few constants in life: death, taxes, and me trying to automate absolutely everything.
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!
Want to jump straight to the code? Check out the gist here!
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. Astro is that it's 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?
...
I started googling for "HTML to pdf" solutions and stumbled upon html-pdf-node. This package seemed to be exactly what I needed, and I got a PDF generated in a matter of minutes. However, the styles were just a bit off. I wasn't 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, 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. But, 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.
So what to do? đ¤
Puppeteer to the rescue
I found this idea at Bannerbear. But, it took a bit of tinkering to get working juuuuust right for my use case.
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. But if someone just sends you the download link for my resume then you may not even know my name. So, on the website, my name may be redundant information, but it's quite pertinent to have that somewhere near the top of the PDF.
With this solution, I can selectively render this page in whatever way I need to build a nice-looking resume in PDF format. The PDF format is a bit more traditional than the skills page but still contains the general style of the site.
If you think this was cool, I also wrote a nifty solution to automate generating my PDF at build-time when I modified the Skills page. The solution is pretty simple and keeps things nicely in sync without having to rely on my brain remembering to generate the resume!