Feb 20, 2023

How to Create CV using HTML

Based on my informal observation of my friends, it seems like most people create their CV in word processor software (e.g. Google Docs or MS Word) or if they’re younger, in Canva. Having a job producing web pages, I always think that creating and maintaining them using web technologies would be a good exercise, but I have yet to do that as there is something else more fun to do.

After seeing a frontend engineer embed a PDF in their website, I finally decided to do that, as it seems like perhaps it is not so straightforward to many. The following is my process, learning, and the final solution I settle with.

The Why: Problem Definition and Benefits

I want to create and maintain my CV with HTML/CSS and downloadable as PDF.

The benefits:

  1. I can include the CV directly in my webpage, which can be responsive and looks nice in all screen sizes
  2. I know all the tricks to bend the layout to my will, using a medium that I’ve been using in my job
  3. Some people (e.g. recruiters) prefer CV in PDF format so they can store it while keeping the CV layout, but I don’t want to maintain multiple formats of my CV.

The Basic Solution

Creating the CV using HTML/CSS is straightforward once you are familiar with CSS Flexbox and CSS Grid.

To make it downloadable as PDF, I relies on the “Save as PDF” options when printing a web page, which is supported by most Desktop browsers nowadays.

But there is one problem, when exporting to CV, there are stuffs that I want to tweak, e.g. remove icon, adjust the spacing.

CSS print styles allows us to style our web page for printing. It looks like this:

css
@media print {
.selector {
property: value;
}
}
css
@media print {
.selector {
property: value;
}
}

It is media queries that many of us familiar with, but instead of providing a value like (min-width: 500px), we provide the keyword print, and browser would use the CSS values nested inside when printing the page.

See it in action below (click the Play button on the right).

html
<style>
h1 {
font-size: 4rem;
font-weight: 600;
color: #9d174d;
text-align: center;
}
@media print {
h1 {
color: #27272a;
text-align: left;
}
button {
display: none;
}
}
</style>
<div>
<div>
<button onclick="window.print()" type="button">Print</button>
</div>
<h1>My Name</h1>
</div>
html
<style>
h1 {
font-size: 4rem;
font-weight: 600;
color: #9d174d;
text-align: center;
}
@media print {
h1 {
color: #27272a;
text-align: left;
}
button {
display: none;
}
}
</style>
<div>
<div>
<button onclick="window.print()" type="button">Print</button>
</div>
<h1>My Name</h1>
</div>

The Enhancement

While I can now export PDF a page created with HTML/CSS, using print with “Save as PDF” can only be used by me (as the developer who created the page), and not by site visitors who might want to download it themselves with a Download/Print button, like the example above. This is due to the following issues:

  1. It may surprise user when “Download” action prompt a print dialog, and it requires them to manually change the Print option.
  2. The option is not available in all mobile browsers

To add a nice download button for site visitors, I need to include a link <a href="/cv.pdf" download> and I also need to export the page as PDF upfront and place it at /cv.pdf on my website.

But how do I export the page as PDF upfront? There are two options:

  1. Manually export the PDF and deploy it together with my website every time I deploy my website. However, this is not a practical solution as it requires a lot of manual work.
  2. Automatically export to PDF using library (which is what I did) as part of the deployment steps.

To do this, I wrote a NodeJS script that is similar to the one below, which runs after the website complete its build process. This script launches a Chromium browser, navigates to the CV page, and saves the page as a PDF.

js
import { chromium } from 'playwright';
(async function exportCvToPdf() {
const browser = await chromium.launch();
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto('http://localhost:3000/cv'); /* The website is served locally
at http://localhost:3000, and the CV page is served at /cv path. */
await page.pdf({ path: 'build/cv.pdf' }); // `build` is the output folder of my website
await browser.close();
})();
js
import { chromium } from 'playwright';
(async function exportCvToPdf() {
const browser = await chromium.launch();
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto('http://localhost:3000/cv'); /* The website is served locally
at http://localhost:3000, and the CV page is served at /cv path. */
await page.pdf({ path: 'build/cv.pdf' }); // `build` is the output folder of my website
await browser.close();
})();

The Over-Engineering

Now that I’ve achieved my initial plan of creating a CV in HTML/CSS that can also be downloaded by site visitors, I realized that the build process becomes slower due to the need to download Chromium to run the export script.

To address this, I decided to extract the CV export functionality into a separate web service, called page-to-pdf, which receives HTTP requests and returns a PDF file.

With this approach, I was able to simplify the export script on my website, as shown below:

js
import fs from 'node:fs';
import { stream } from 'undici';
(async function exportCvToPdf() {
const writeStream = fs.createWriteStream('build/cv.pdf');
await stream('https://page-to-pdf-service.com/screenshot', {}, () => writeStream);
})();
js
import fs from 'node:fs';
import { stream } from 'undici';
(async function exportCvToPdf() {
const writeStream = fs.createWriteStream('build/cv.pdf');
await stream('https://page-to-pdf-service.com/screenshot', {}, () => writeStream);
})();

This script uses the undici library to send an HTTP request to the page-to-pdf service, which returns the CV as a PDF file. The file is then written to a write stream and saved in the “build” output folder of my website.

Conclusion

Exporting HTML CV as PDF may seems like a simple goal, but it can quickly becomes complex as you consider making it available to more users.

In my case, this meant taking into account of user experience and browser support. However, with modern JavaScript tools, I managed to create an automated solution. This solution comes with the tradeoffs, including slower build steps, which I’ve addressed by creating another web service.

At the end of the day, is all of these work really necessary? Probably not, but for me it’s a hell of fun.

Thanks for reading!

Love what you're reading? Sign up for my newsletter and stay up-to-date with my latest contents and projects.

    I won't send you spam or use it for other purposes.

    Unsubscribe at any time.