My resume is a build artifact
My resume has lived in LaTeX for years, in a GitHub repo with CI that builds the PDF on every push. When I put up this site, I wanted omair.org/resume to show the same resume without me ever copying it by hand. Edit one file, push, and both the PDF and the web page update.
Here’s how that works, and the more ambitious version I talked myself out of.
The starting point
The resume repo already did a lot:
resume.texholds the content, with each bullet defined as a macro.- Variants in
variants/*.texoverride a few macros (summary, bullet order) and then\inputthe main file. I keep one per kind of role I’m applying to. - CI builds every variant in a pinned TeX Live image, fails if any PDF spills onto a second page, spell-checks the output, and publishes the PDFs to a GitHub release.
So the PDF was solved. The question was the web page.
The ideas I threw away
My first instinct was to render the LaTeX live, in the browser.
Compiling TeX in the browser. Projects like SwiftLaTeX run a real TeX engine in WebAssembly. It’s a great demo, but every visitor would download tens of megabytes of TeX files and wait several seconds to see a document that only changes when I push. Lighter converters like latex.js skip the engine, but they only support a slice of LaTeX, and my custom macros wouldn’t have survived.
Generating the PDF on the fly. The site runs on Cloudflare Workers, which can’t run pdflatex. I could have used a container, but then every request would rebuild a file that only changes on push.
Both ideas do work on every request that could be done once, at build time. Since the source changes a few times a month, the cheapest version of “live” is to rebuild whenever the source changes.
What I built instead
resume.tex ──push──▶ resume CI ──▶ PDF ─┐
│ ├──▶ site repo ──▶ Cloudflare
└──▶ web.py ──▶ JSON ─┘
On every push to main, the resume repo’s CI:
- builds the PDFs as before,
- converts
resume.texto JSON with a small Python script, - checks out the site repo, renders the JSON into
/resume, and commits the page and the PDF, - pushes, and the site deploys itself.
A converter that refuses to guess
web.py isn’t a LaTeX parser. It understands exactly the commands my resume uses (\resumeSubheading, \resumeItem, \href, \textbf, and a few more), expands my macros, and turns the result into sections, entries and bullets.
The important part is what it does with anything else:
else:
raise ValueError(f"unsupported inline command \\{name} in: {s[:80]!r}")
If I add a new command to the resume next year and forget about the website, the build fails and tells me which command. A converter that quietly dropped what it didn’t understand would make the web page drift from the PDF without anyone noticing, and that’s the problem I set out to fix.
Keeping my phone number off the internet
The PDF I send recruiters has my phone number. The public one shouldn’t. The header now reads the number from a macro:
\providecommand{\ResumePhone}{+1 (647) 555-0100 | }
…and a public variant overrides it with nothing. The site always gets the public variant. The converter checks its own output: after rewriting the source, it fails the build if the original number still appears anywhere.
“View the source”
The web page has a view the source button that cross-fades the resume into the LaTeX it was compiled from, syntax-highlighted. It’s the one part that’s there purely for fun. It also turned up the bug I liked most: the toggle sets #source in the URL so you can link to it, and the source panel’s element id was also source. Opening the link made the browser scroll past the header to that element. I renamed one of them.
Two repos, one key
The resume repo is private. The site repo is a separate project. For CI in one to push to the other, I used a deploy key: an SSH key that can write to the site repo and nothing else, with its private half stored as a secret in the resume repo. It’s narrower than a personal access token and doesn’t expire while I’m not looking.
Was it worth it?
For a one-page document? Probably not, on paper. But it took an afternoon, and now my resume can’t go stale on my own site. The parts I’d reuse anywhere:
- Prefer build time to request time when the input changes rarely.
- Make converters strict. Failing loudly is cheaper than drifting quietly.
- Check for the thing you’re scared of, like a phone number leaking, instead of trusting the code to be right.
If you want to see it working, the resume is here. Press view the source.