← posts

My resume is a JSON file that generates a website, a PDF, and a Word doc

5 July 2026
Next.jsTypeScriptresumeautomation

Often times when I'm applying to a job, it's just a LinkedIn job posting via my phone. In fact, that is exactly how I applied for my current job. Having my resume on my phone allows me to apply to jobs on-the-go and just get my job application numbers up.

However, I edit my resume on my laptop. Maybe it's a generational thing, but there are certain things that feel like a "laptop job" and not a phone job. Resume editing is one of them. The problem is that any time I edit my resume, I have to sync it over to my phone. Now if I just had a google drive account, this would not be a problem. However, I like to self-host everything and torture myself. So here we are.

I got tired of maintaining my resume in four places: a Word doc for recruiters who want a .docx, saving that word doc to pdf for everyone else, my phone for applying to jobs, and a webpage here that inevitably drifted out of sync with everything else. Every time I updated a bullet point I had to remember to update it everywhere else.

So I made the data the single source of truth and generated everything else from it.

The data

resume.json is the only file I actually edit. Every job, bullet point, skill, and link lives there. Nothing else in the resume system has content hardcoded into it, it's all just rendering.

The web page

/resume renders resume.json directly as a page styled to match the rest of the portfolio. Email and phone number are masked on the web view (y*****[email protected], 425-***-****) since this page is public and I'd rather not hand scrapers a clean copy of my contact info. I think only a human would think to then generate the resume as a docx/pdf and see the actual contact info in there.

The PDF

/resume/pdf spins up a headless Chrome instance via Puppeteer, renders the same resume data as plain HTML (Times New Roman, tight margins), and streams back a PDF. Launching a full browser per request isn't free, so the first request after a restart pays that cost and the result gets cached in memory from then on. Since resume.json only changes via a redeploy anyway, and a redeploy restarts the process, the cache invalidates itself exactly when it needs to, no extra logic required.

The Word doc

/resume/docx uses the docx npm package to build a .docx directly, no browser involved. This one was more annoying to get right than the PDF: the contact row uses right-aligned tab stops to line up email/LinkedIn/GitHub, and docx's built-in TabStopPosition.MAX doesn't actually match the page's usable width once you've accounted for margins. Had to compute the right tab stop manually from page width minus margins to get it to land in the right place. Also wired up real ExternalHyperlink runs for email/LinkedIn/GitHub instead of plain text, so links are actually clickable in Word.

The print preview

/resume/print is a plain HTML route handler, no client JS, meant for using the browser's own print dialog when you just want a clean printout that resembles a pdf without downloading anything.

Why bother

I am incredibly stubborn and when an idea enters my head, it rarely leaves. If this is something that works, I guess you'll see it on the /resume page. If not, it was really fun to work on a dumb problem that no one else thought was a problem.