Back in January, I was laid off unexpectedly from my software engineering job. Naturally, this meant it was time to update my resume and my personal website. As so often happens, I fell down a huge rabbit hole of automating things. Eventually, this culminated in my current website and resume, which are now built dynamically from a single JSON file. I’m really happy with the final result, so I figured I should talk about it!
The problem
Like any good programmer, I like to reduce duplicated work whenever I can. When I first sat down to start updating my resume and website, I quickly found that the process of updating both separately was extremely tedious. Not only did I have to update each one manually, I also had to double check that the information in both matched. It was clear I needed to find a way to keep both my website and resume perfectly in sync, and to generate both automatically from some sort of data file so that I didn’t have to manually edit a PDF or write new HTML for any future updates.
This gave me a pretty good idea of what I needed from my new setup:
Single source of truth
Updating my website and resume separately was error-prone and time-consuming, and just generally annoying. Ideally, I wanted to be able to just change things in one place, and have those changes propagate from there.
Data-driven
In order to have a single source of truth for both things, it really makes the most sense to boil down all my information into a single condensed data file, and then use that to derive everything else. I use NixOS as my daily driver, so this particular idea of declaratively building things was really appealing to me. Another plus: data files are really easy to version control.
Static site generation
The previous iteration of my blog used static site generation via Jekyll, and while Jekyll specifically didn’t quite work for me, the underyling concept worked quite well for my needs. It gives you a lot of power for templating and data-derived content, while still resulting in a blob of vanilla HTML + CSS.
Options for resume appearance
I’d like to be able to tweak and customize the final PDF as I see fit, so the system needs to be flexible. Ideally, there’d also be a bit of an ecosystem around whatever I picked.
Plan of attack
Pretty early on, I had a good idea of my ideal final setup:
flowchart TD
A[/Make changes/] --> B[Data file]
B --> C[/Build step/]
C --> |Static Site Generator| D[HTML + CSS bundle]
C --> |PDF Generator| E[Resume PDF]
F([loganswartz.com]) --> D
Finding the right data format
With those goals in mind, I started exploring.
The nucleus of this entire setup would be the underlying data file. I figured I should start by looking for some sort of data-defined resume project, and go from there. Pretty quickly, I stumbled across 2 different projects: JSON Resume, and RenderCV. There are a couple other things out there, but those seemed to be the “big 2”, and they’re both data-driven. JSON Resume uses a JSON file (go figure), and rendercv uses a YAML file.
JSON Resume
JSON Resume is mainly focused on the underlying data file. In fact, it’s really just a resume-specific JSON Schema, with a small ecosystem that has emerged around it. Here’s a partial example of a JSON Resume file:
{
"$schema": "https://raw.githubusercontent.com/jsonresume/resume-schema/master/schema.json",
"basics": {
"name": "Logan Swartzendruber",
"label": "Software Engineer",
"image": "./headshot.jpg",
"url": "https://loganswartz.com",
"summary": "A Full-Stack Software Engineer with 5+ years of experience specializing in high-scale ERP systems and microservice architecture. Proven track record of leading dev teams, mentoring juniors, and optimizing internal workflows.",
"location": {
"city": "Goshen",
"countryCode": "US",
"region": "Indiana",
},
"profiles": [
{
"network": "github",
"username": "loganswartz",
"url": "https://github.com/loganswartz",
},
{
"network": "linkedin",
"username": "logan-swartzendruber",
"url": "https://www.linkedin.com/in/logan-swartzendruber",
},
],
},
// ...
}
RenderCV
RenderCV is similar to JSON Resume in that everything is defined in a data file, but there are differences in both philosophy and implementation. For one, RenderCV uses a YAML file instead of JSON. For another, it places most of its focus on the act of PDF generation, rather than aiming to be a standard data format for resume information.
Given these two options, I opted for JSON Resume for the underlying data file, for 2 main reasons.
First, I was really more interested in the data schema over all else. In my eyes, if I find a good schema for storing my information, I can transmute it into any other format I need.
Second, JSON is just generally better than YAML in every way. There are many articles like this one that explain all the pitfalls of YAML, and I think going with the tried and true JSON is a better move.
Figuring out PDF generation
Now that I’d settled on JSON Resume, it was time to make sure that I’d actually be able to generate an acceptable resume PDF from the file.
JSON Resume has a CLI for exporting to PDF, but according to their own site:
Note: The official CLI tool isn’t that actively maintained. There is an alternative that you might have more success with: @rbardini/resumed
I poked around with both options, but ultimately they didn’t seem promising. I ran into issues with both when exporting, and there really wasn’t enough customization available for my needs. As it turns out after reading a bit further on the JSON Resume homepage, they have a recommendation for people needing more customization, and that recommendation is… RenderCV?
…there is very well put together project called RenderCV. It has it’s own data format for resumes but we have tools to convert your resume.json to their format.
Using npx @jsonresume/jsonresume-to-rendercv resume.json, you can convert an
existing JSON Resume file to an equivalent RenderCV file, which means we can
have the best of both worlds. Since RenderCV focuses much more on the PDF
generation side of things, there’s a lot more customization available, and I was
pretty happy with a few of the pre-made templates they have.
To get a finished PDF built from my resume.json file, I need to run it through
jsonresume-to-rendercv, and then pass the resulting file into the RenderCV
CLI. To automate this a bit, I built out a simple generate-pdf-resume bash
script that does the conversion internally.
Now that the resume side of things was settled, it was time to figure out how to build the website.
Website non-starters
While I didn’t yet know what I wanted to do for the website, I had already decided on a few things that I didn’t want to do.
Dynamic generation with PHP
PHP is often the “traditional” way to create sites with dynamic content. In fact, it was initially designed as a scripting language for dynamic personal websites. When someone requests to see your website, the server evaluates your PHP scripts and returns HTML as a result. This is powerful, but it also means your server has to be configured to evaluate PHP with something like PHP-FPM.
My main responsibility at my previous job was actually working on an app that used a PHP + Laravel backend and a React (Typescript) frontend. Those technologies are great for SPAs and more interactive websites, but in my opinion, they’re way too complex for something like what I’m building. At the same time, if I was going to derive the entire site from a data file, it was clear I’d need some sort of dynamic site generation.
So, while my first inclination was to reach for PHP, I decided pretty quickly that I didn’t want to do that. I’ve used PHP-FPM with both Nginx and Apache, and it’s just a bother to deal with. It’d be really nice for the final generated site to be just a regular blob of HTML and CSS, so that any web server can serve it up without any extra configuration. Additionally, despite using it at my last job, PHP is probably one of my least favorite languages. It’s not the worst language out there, but vanilla PHP without any convenience libraries is pretty clunky and full of footguns.
A Single Page App (React, Vue, Svelte, etc…)
While single page apps are great for highly interactive and dynamic sites, they really aren’t worth it for small, static sites. Pages built with frontend JS frameworks often have slower loadtimes since the browser has to load the entire JS payload before rendering anything, and that also means that they don’t play as well with SEO. There are ways of mitigating those drawbacks (mainly SSR), but I’d rather go with an option that I don’t have to tweak dramatically to get what I need.
For a personal site, traditional templating / static site generation feels much more appropriate. With that in mind, I started looking for options.
The current state of content-driven frameworks
Jekyll was the first static site generator to really popularize the concept, but nowadays, there are quite a few different popular SSG frameworks. These include:
There’s also some SSG functionality in some of the more popular JS frameworks, including:
I would be remiss to not also mention popular content management systems (CMS), like:
Despite the plethora of options, I really felt like SSG was the way to go, so I only focused on those. After some further research, I found that most people had good experiences with Astro, and I liked their emphasis on “no JS, unless you absolutely need it”.
Trying out Astro
Astro uses its own special .astro file format for pages, which lets them do a
lot of cool things behind the scenes. Every .astro file has 2 parts to it: the
frontmatter, and the body. The frontmatter contains any Javascript you want to
use to template the page, and then the body is your templated HTML.
First, I put my complete resume.json file into Astro’s assets/ folder. Then,
it was trivial to import my resume.json as a Javascript value, and then I
could immediately start using that to dynamically build my layouts. I found I
could even save the JSON Resume schema, import that as a type, and then assign
that type to my imported resume to make it typesafe:
---
import type { ResumeSchema } from "../jsonresume";
import _resume from "../assets/resume.json";
const resume = _resume as ResumeSchema;
---
<div id="container">
<main>
<BasicsSection basics={resume.basics} />
<ExperienceSection title="EXPERIENCE" experiences={resume.work} />
<EducationSection education={resume.education} />
<SkillSection skills={resume.skills} />
<ProjectSection projects={resume.projects} />
<ExperienceSection title="VOLUNTEER" experiences={resume.volunteer} />
<InterestSection interests={resume.interests} />
</main>
</div>
Then, when building the site with yarn run build, I get a nice bundle of pure
HTML and CSS, since the Javascript in the frontmatter is only used for the build
step.
Sorting out a wrinkle
After some implementation, I realized there was one small problem with my setup: while I said I wanted both the website and resume to be built from the same data file, it turns out there were some small differences that I wanted to be able to only include in one or the other. So, I revised my plan:
flowchart TD
A[/Make changes/] --> C[Website-specific data]
A --> B[Shared data]
A --> D[Resume-specific data]
C --> E[Website data file]
B --> E
D --> F[Resume data file]
B --> F
E --> G[/Build step/]
F --> H[/Build step/]
G --> |Astro| I[HTML + CSS bundle]
H --> |RenderCV| J[Resume PDF]
K([loganswartz.com]) --> I
I split my data file into base.json, website.json, and resume.json, and
then wrote another helper bash script called generate-jsonresume-file. I can
then call generate-jsonresume-file website or generate-jsonresume-file resume to combine the partial JSON Resume files with jq/yq into its final
form.
Now, I could overlay base.json with website or resume specific changes, like
so:
// base.json
{
"$schema": "https://raw.githubusercontent.com/jsonresume/resume-schema/master/schema.json",
"basics": {
"name": "Logan Swartzendruber",
"label": "Software Engineer",
"image": "./headshot.jpg",
"url": "https://loganswartz.com",
"summary": "A Full-Stack Software Engineer with 5+ years of experience specializing in high-scale ERP systems and microservice architecture. Proven track record of leading dev teams, mentoring juniors, and optimizing internal workflows.",
},
"projects": [
{
"name": "BigRedGuy",
"startDate": "2022-11-02",
"description": "A web app for simplifying communication around \"Secret Santa\" / Christmas gift giving.",
"highlights": [
"Technologies: Rust, Tokio, SeaORM, Rocket, React, Chakra UI",
"Built for personal use, and to experiment with Rust in a full-stack web development context",
],
"url": "https://github.com/loganswartz/big_red_guy",
},
],
// ...
}
// resume.json
{
"projects": [
{
"name": "Open Source contributions",
"startDate": "2019-09-13",
"description": "In addition to my own projects, I've also contributed to a number of open source projects online.",
"highlights": [
"[github/combine-prs](https://github.com/github/combine-prs/pull/48) – Added an option to use a fresh base branch",
"[grokability/snipe-it](https://github.com/grokability/snipe-it/pull/7388) – Added the ability to print individual labels",
"[nuwave/lighthouse](https://github.com/nuwave/lighthouse/pull/2696) – Added a new CLI flag for deterministic outputs",
"[netromdk/vermin](https://github.com/netromdk/vermin/pull/81) – Added support for the 'pre-commit' framework",
"[mistweaverco/kulala.nvim](https://github.com/mistweaverco/kulala.nvim/pull/319) – Bugfix for GraphQL logic",
"[lewis6991/hover.nvim](https://github.com/lewis6991/hover.nvim/pull/75) – Added support for overridable highlight groups",
],
},
],
}
Which resulted in the final combined file:
{
"$schema": "https://raw.githubusercontent.com/jsonresume/resume-schema/master/schema.json",
"basics": {
"name": "Logan Swartzendruber",
"label": "Software Engineer",
"image": "./headshot.jpg",
"url": "https://loganswartz.com",
"summary": "A Full-Stack Software Engineer with 5+ years of experience specializing in high-scale ERP systems and microservice architecture. Proven track record of leading dev teams, mentoring juniors, and optimizing internal workflows.",
},
"projects": [
{
"name": "BigRedGuy",
"startDate": "2022-11-02",
"description": "A web app for simplifying communication around \"Secret Santa\" / Christmas gift giving.",
"highlights": [
"Technologies: Rust, Tokio, SeaORM, Rocket, React, Chakra UI",
"Built for personal use, and to experiment with Rust in a full-stack web development context",
],
"url": "https://github.com/loganswartz/big_red_guy",
},
{
"name": "Open Source contributions",
"startDate": "2019-09-13",
"description": "In addition to my own projects, I've also contributed to a number of open source projects online.",
"highlights": [
"[github/combine-prs](https://github.com/github/combine-prs/pull/48) – Added an option to use a fresh base branch",
"[grokability/snipe-it](https://github.com/grokability/snipe-it/pull/7388) – Added the ability to print individual labels",
"[nuwave/lighthouse](https://github.com/nuwave/lighthouse/pull/2696) – Added a new CLI flag for deterministic outputs",
"[netromdk/vermin](https://github.com/netromdk/vermin/pull/81) – Added support for the 'pre-commit' framework",
"[mistweaverco/kulala.nvim](https://github.com/mistweaverco/kulala.nvim/pull/319) – Bugfix for GraphQL logic",
"[lewis6991/hover.nvim](https://github.com/lewis6991/hover.nvim/pull/75) – Added support for overridable highlight groups",
],
},
],
}
Putting it all together
Now, everything works well, but we’re left with 2 manual build steps. I needed a way to combine the two steps into one:
flowchart TD
A[/Make changes/] --> C[Website-specific data]
A --> B[Shared data]
A --> D[Resume-specific data]
C --> E[Website data file]
B --> E
D --> F[Resume data file]
B --> F
E --> G[/Build step/]
F --> G[/Build step/]
G --> |Astro| H[HTML + CSS bundle]
G --> |RenderCV| J[Resume PDF]
I([loganswartz.com]) --> H
After some looking, I realized I could embed the RenderCV step in the Astro
build step, by using vite-plugin-run and my helper scripts:
// @ts-check
import { defineConfig } from "astro/config";
import { run } from "vite-plugin-run";
// https://astro.build/config
export default defineConfig({
// ...
vite: {
plugins: [
run([
{
name: "Website JSON file generation",
run: ["./generate-jsonresume-file", "website"],
pattern: ["data/base.json", "data/website.json"],
},
{
name: "PDF generation",
run: ["./generate-pdf-resume", "--dir", "public"],
pattern: ["data/base.json", "data/resume.json"],
},
]),
],
},
});
Now, whenever I build the site, it automatically updates both data files and generates the resume PDF, all in one go.