This portfolio
Live
The home page: who he is, what he wants and the two ways on 
Three featured in view; the rest one click away 
The assistant answers about the CV, streamed and with the dossier kept server-side 
A project page, in dark theme
A static Astro site on Cloudflare Pages, trilingual and with no UI framework. A single cv.json feeds the pages, each language's PDF, the JSON-LD and the machine-readable endpoints; the dynamic parts are Pages Functions.
- A hash-based CSP with no unsafe-inline, recomputed from the HTML on every build.
- HTML, DOM, CSS and JS budgets that fail the build; Lighthouse blocks when LCP goes over 2.5 s.
- The PDF is generated and checked on every build: one A4 sheet, text extractable by two engines.
- Atomic rate limits in D1, tested against a real database.
The problem
A PDF CV falls short for a technical application — you cannot link to it, search engines cannot read it and it proves nothing about how you work — yet it is still what people ask for. And keeping the site, the PDF and every job-board profile up to date by hand means all three end up saying different things. The idea was one source, with everything else generated from it.
My role
Everything: the design, the code, the infrastructure and the content. There is no starter template and no UI framework; what the browser downloads is my own code.
Decisions
One `cv.json`, everything else derived
The file follows the JSON Resume schema and the build validates against it. From it come the pages in three languages, a one-page PDF per language, the JSON-LD for search engines and `/cv.json`, `/cv.md` and `/llms.txt`. The alternative — maintaining the site and the PDF separately — is what produces CVs that contradict each other, and it means fixing everything three times.
Quality is checked by the build, not by review
The build fails when a page goes over its HTML, DOM, CSS or JavaScript budget, when a `<style>` has no hash in the security policy, when the PDF spills onto a second sheet or when its text cannot be extracted. And a Lighthouse job blocks the deployment when the home page goes over 2.5 s of LCP. Reviewing by eye works once; a ceiling that fails works every time, and it says what broke.
No measuring locally: only CI counts
The two measurements that matter — simulated LCP and the visual references — depend on text rendering and on the machine's CPU. Locally, an ad blocker also filters requests from any browser, the test one included. The references are generated inside the pinned Playwright container, and Lighthouse runs in that same image on CI: three runs and the median.
The assistant keeps its dossier on the server
It answers questions about the CV with a model, which opens two problems: whatever you send it can be extracted, and a paid API open to the internet can be drained. The first is solved by sending nothing I would not say in an interview, and by writing the rules positively: a rule saying «do not discuss X» publishes that X exists. The second, with per-IP limits in D1, atomic so that two simultaneous requests do not count as one.
Outcome
- The home page loads at 2.1 s of simulated LCP on CI, with 21 requests and 166 KB. Total JavaScript is 17 KB gzipped, across nine of my own modules.
- Zero axe accessibility violations across the six pages, and a security policy with no `unsafe-inline` that the build recomputes from the served HTML.
- The same CV is served in six formats without writing it twice: three languages of pages, three PDFs, `/cv.json`, `/cv.md`, `/llms.txt` and the JSON-LD.
What I would change
I built too much before looking at who was coming. There was an interactive console, a Markdown view and a command palette; across forty days of data the three of them saw 69 uses against 383 home-page visits, and they cost around 5,500 lines. I removed them, and half the JavaScript went with them. Today I would start with the measurement: find out what gets used first, and build the next thing with that answer in front of me.