I maintain several versions of my resume for different kinds of work: acting, tech, childcare, food service, etc. This system lets me keep all the content in one place and generate tailored PDFs from it. It's built with Typst for typesetting and Nix flakes for reproducible builds, and it deploys to GitHub Pages on every versioned release.
Each variant is a thin entry file that merges base metadata with job-specific overrides, picks which content modules to include, and renders through a shared Typst layout. I only override what needs to change based on the job description: email address, header tagline, which personal details to show or hide, layout tweaks.
| Variant | File | Purpose |
|---|---|---|
| Acting CV | resumes/cv.typ |
Theatre and performance |
| Tech | resumes/tech.typ |
Technology and IT positions |
| Work | resumes/work.typ |
Events and operations |
| Nanny | resumes/nanny.typ |
Childcare |
| Salt & Straw | resumes/saltandstraw.typ |
Scooper |
| Salt & Straw SC | resumes/saltandstraw-sc.typ |
Shift coordinator |
| Cover Letter | resumes/cover-letter.typ |
General cover letter |
| Rep Sheet | resumes/rep-sheet.typ |
Musical theatre repertory |
| Title Pages | resumes/title-pages.typ |
Audition title pages |
The variant registry in variants.toml is the single source of truth β the Nix build, CI matrix, and quality checks all read from it. Every variant follows the same pattern:
resumes/<variant>.typ
β meta.typ: merge metadata/metadata.toml + metadata/<variant>-metadata.toml
β select modules from modules/
β render via src/lib.typ::cv()
All resume content (job history, skills, theatre credits, training) lives in TOML files under metadata/. Modules in modules/ load that data and format it into sections. The layout engine in src/ handles page structure, styling, and typography.
Requires a Nix environment with flakes enabled.
nix build '.#resume' # all resume PDFs
nix build '.#finn-rutis' # composite headshot PDF
nix build '.#website' # portfolio siteFor local iteration without a full Nix build:
nix develop # enter dev shell (typst, typstyle, tinymist, etc.)
typst watch resumes/cv.typ # live preview
typst compile --input commit="dev" --input version="2026-04-22" resumes/tech.typ-
Create a metadata override in
metadata/<variant>-metadata.toml. Only include fields that differ from the basemetadata/metadata.tomlβ everything else is inherited. -
Create an entry file at
resumes/<variant>.typthat imports the override, selects which modules to include, and passes the merged metadata to the shared layout. -
Create new modules in
modules/if the job needs a section that doesn't already exist. -
Register the variant in
variants.tomlwith the source filename, output name, expected page count, and whether metadata should be validated.
Builds run on every push. Deployments to GitHub Pages happen only when VERSION changes on main. I use just bump <patch|minor|major> to trigger releases, and git-cliff generates changelogs from conventional commits.
The Nix flake runs these checks automatically:
- typstyle β consistent Typst formatting
- TOML validity β all metadata files parse correctly
- Metadata completeness β merged metadata has all required fields
- Output files β every registered variant produces a PDF
- PDF page count β each variant matches its expected page count
Project structure
resumes/ # entry files per variant
src/ # layout templates and utilities
βββ lib.typ # main layout (cv, coverLetter)
βββ cv.typ # section/entry components
βββ meta.typ # metadata merging
βββ utils/ # styles, dict merge, language detection
modules/ # reusable resume sections
metadata/ # base + override TOML data files
packages/ # Nix derivations (resume, finn-rutis, website)
.github/ # CI workflows
variants.toml # variant registry (single source of truth)
I use conventional commits so git-cliff can parse them:
feat(content): add new theatre credit
fix(reformat): tighten section spacing
refactor(template): simplify module loading
Scopes: content for resume text, reformat for layout changes, template for structural updates.