My résumé, built as a small React + TypeScript application. The same source renders the web version (responsive) and the one-page A4 PDF I send with my applications.
This repository contains the public, anonymised version: contact details are placeholders. My real details only exist on my machine and are never committed or bundled into the public build.
- Content separated from presentation — the résumé content lives in
src/data/cv.ts, typed bysrc/types.ts. Components insrc/componentsonly render it. - One layout, three outputs — desktop, mobile and A4 print. SCSS partials with variables and
print/mobilemixins keep the print rules next to the screen rules of each element. - Always up-to-date PDF — a custom Vite plugin (
vite-plugins/cvPdf.ts) renders the page with headless Chromium (Playwright). In development the PDF is regenerated on every change; at build time it is generated from the production bundle, and the build fails if the résumé no longer fits on a single A4 page. - Public / private variants — see below.
| Public (default) | Private | |
|---|---|---|
| Contact details | placeholders from src/data/identity.public.ts |
read from .env.local |
| Commands | pnpm build, pnpm dev:public |
pnpm dev (when .env.local exists), pnpm build:private |
| Output | dist/ (site + cv.pdf) |
private/ (site + named PDF) |
| Committed | yes | never (.env.local and private/ are git-ignored) |
How the separation is enforced:
- The app imports contact details from a virtual module,
virtual:cv-identity, provided byvite-plugins/cvIdentity.ts. In public mode it re-exports the placeholders; only in--mode privatedoes it read the real values. pnpm buildis always public. Only the local dev server switches to the private version automatically when.env.localis present.- Private variables use the
CV_prefix, notVITE_, so Vite never exposes them to client code. In public mode they are not even read, so they cannot end up in the public bundle. scripts/check-privacy.mjsscans every committable file and the public build for the private values (taken from.env.local), generic e-mail and phone patterns, tracked PDFs, ignore rules and commit e-mail addresses. It runs before everygit pushthrough a versioned hook (.githooks/pre-push).
Requirements: Node.js 20+, pnpm, and Chrome or Edge (or pnpm exec playwright install chromium) for PDF generation.
pnpm install # also enables the pre-push privacy hook
pnpm dev # http://localhost:5173 (public version until .env.local exists)To work on the private version:
cp .env.example .env.local # then fill in your real details
pnpm dev # live preview with your details, PDF kept up to date in private/
pnpm build:private # final private PDF in private/| Script | Description |
|---|---|
pnpm dev |
Dev server with live PDF regeneration — private version if .env.local exists, public otherwise |
pnpm dev:public |
Dev server forced to the anonymised version |
pnpm build |
Type-check, public build and PDF in dist/ |
pnpm build:private |
Type-check, private build and PDF in private/ |
pnpm preview |
Serve the public build |
pnpm check:privacy |
Public build, then privacy audit of the repository and dist/ |
src/
├── data/
│ ├── cv.ts # résumé content
│ └── identity.public.ts # placeholder contact details
├── components/ # Sidebar, MainContent, Section, EntryItem…
├── styles/ # SCSS partials: variables, mixins, layout…
└── types.ts
vite-plugins/
├── cvIdentity.ts # public / private contact details
└── cvPdf.ts # PDF generation and one-page check
scripts/check-privacy.mjs # pre-publication privacy audit
React 18 · TypeScript · Vite · SCSS · Playwright
