Typeset-Perfect CV: JSON → LaTeX → PDF (without fighting CI)

August 10, 2025 (3d ago)

For years my CV lived in two worlds: a web page people browse on phones, and a PDF recruiters print or feed into ATS systems. I tried “one file to rule them all” with print CSS and browser exports, but it was constant whack-a-mole: hyphenation off, widows/orphans, fonts shifting between machines, A4 vs Letter page breaks, and the web/PDF drifting out of sync. I wanted the web version fast, accessible, and SEO-friendly—and the PDF to look like a grown-up typesetter touched it. Most of my updates are tiny anyway: tweak the text of my current role or add a new role when I change jobs.

So I split the job by purpose:

  • Web: a normal /cv (React/MDX), accessible and easy to update.
  • Print: a LaTeX PDF with real control over typography (hyphenation, ligatures, predictable breaks).
  • Build reality: Vercel’s CI doesn’t include a LaTeX toolchain, so I run pdflatex locally (or Overleaf for theme changes), commit the hashed PDF under /public/files, and Vercel just reads a build-time CV_VERSION to link the current file.

Architecture

  • Hashed artifacts kept: old PDFs remain in /public/files (cv-.pdf history).
  • Linking: the site imports CV_VERSION and links directly to /files/cv-${CV_VERSION}.pdf (no query param).
  • No LaTeX in CI: Vercel skips TeX completely; the PDF is already in the repo.

Source of truth & drafting

  • Day to day, content lives in resume.json; generate-latex.js turns it into cv-source/main.tex with proper escaping and section stitching.
{
  "company": "NewCo",
  "position": "Senior Software Engineer",
  "dates": "2025 – present",
  "location": "Manchester",
  "description": [
    "Led X to deliver Y.",
    "Improved Z by N% via A/B testing."
  ],
  "technologies": ["C#", ".NET", "Azure"],
  "iconId": "newco",
  "url": "https://newco.example"
}
  • For small layout tweaks, I'll export cv-source/main.tex to Overleaf, experiment, then pull the updated TeX back and keep generating from JSON.
  • For big theme changes, I start from a new Overleaf template, paste in JSON content manually, then adjust the generator/JSON schema so future edits are back to "change JSON → regenerate."

Build flow (local → commit → deploy)

  • Local: npm run update-cv (or equivalent) which:
    1. regenerates main.tex from JSON,
    2. runs pdflatex to produce public/files/cv-.pdf,
    3. optionally cleans aux files.
  • Commit the PDF(s) and push.
  • Vercel build runs generate-version.js which hashes cv-source/main.tex (and the class file) and writes CV_VERSION to lib/version.js. The front end reads that constant.
// /app/cv/page.tsx (excerpt)
import { CV_VERSION } from '@/lib/version';
const cvUrl = `/files/cv-${CV_VERSION}.pdf`;
// ...
<a href={cvUrl} target="_blank" rel="noopener noreferrer">Open PDF Version</a>

Why this approach won

  • Print quality you can feel: LaTeX handles line breaks, spacing, ligatures, and avoids orphan/widow lines. Your class file centralises the rules.
  • Deterministic builds: pdflatex runs on your machine, so the PDF you proof is the PDF you ship.
  • Zero CI pain: no TeX in Vercel; builds stay fast.
  • Instant cache correctness: the filename carries the content hash; the link updates as soon as CV_VERSION changes.

Notes from the trenches

  • Escaping: generating TeX from JSON means escaping _ % & # everywhere. The generator handles this—extend it when adding new fields.
  • Don't edit TeX post-gen: if you hand-tweak main.tex, your hash and data will drift. Fix the generator or JSON instead.
  • Template evolution: it's fine to treat Overleaf as an R&D lab. For small tweaks, round-trip the single file; for big theme overhauls, adopt the new template, then bring the generator back into the loop.

Future niceties (on the backlog)

  • Pre-push guard: fail the push if cv-source changed but the committed PDF didn't.
  • One-shot "rebuild CV" action: a local script that opens Overleaf export, drops it into cv-source, regenerates, builds, and stages everything.
© Sam Ainsworth 2024 - 2025. All Rights Reserved.
Typeset-Perfect CV: JSON → LaTeX → PDF (without fighting CI) | Sam Ainsworth