How it's built

Built like a small data product

Content lives in versioned files with a schema, every build validates it, and every change has to pass the same checks before it reaches production. Here is how that works, and the trade-offs behind it.

From notes to a deployed page

  1. Source notes

    Each role starts as a Markdown document. A Claude skill turns those notes into two JSON files: a controlled vocabulary of tags and the experience entries themselves.

  2. Contracts

    Zod schemas define both files, and the TypeScript types are inferred from them, so the data, the types and the validator can't drift apart.

  3. Cross-file rules

    A validator checks what a schema can't: every tag exists under the right type, ids are unique, and the lists in code (search aliases, starter tags, timeline lanes) point at real data.

  4. Static build

    Invalid content fails the build with a readable list of problems. Valid content becomes plain HTML in English and Spanish, with canonical links, hreflang, a sitemap and share images.

  5. Deploy and measure

    The site runs as static files on Render behind Cloudflare, with no server or database. Umami records a small set of typed events, like which skills visitors filter by.

What every change has to pass

  • Lint and types

    ESLint and strict TypeScript, including typed message keys, so a missing translation is a compile error.

  • Content validation

    The schemas and cross-file rules, run on their own before the build.

  • Unit tests

    Date math, overlap-merged years of experience, sorting, related-role scoring, search aliases and typo matching, and tests that break the data on purpose to prove the validator catches it.

  • End-to-end tests

    Every page in both languages returns 200 with the right language, canonical link and hreflang, and hydrates without errors, with and without reduced motion.

  • Accessibility

    Automated color-contrast checks in dark and light theme on every page. Their first run found real failures; the color tokens are now computed to clear 4.6:1 on every surface.

  • Lighthouse budget

    Accessibility and SEO at 95 or more and best practices at 90 or more fail the build if they drop; performance is tracked as a warning.

Decisions and their trade-offs

  1. 1. Static export, no server

    The content changes when I edit it, so every page is prerendered and served as files.

    Trade-off: no per-request logic; anything dynamic happens at build time or in the browser.

  2. 2. A small custom i18n layer

    English stays at /story and Spanish at /es/story. The common library does that with middleware, which a static site can't run, so the translator and path helpers are about 200 lines of typed code.

    Trade-off: no plural rules, and Spanish pages get their language attribute from a pre-paint script for now.

  3. 3. Files with contracts, not a CMS

    Two JSON files in git are the whole content model: diffable, reviewable, and validated on every build.

    Trade-off: an edit needs a pull request, and the step from raw JSON to typed data is one documented assumption backed by validation.

  4. 4. Server by default

    Pages render on the server; only the search, grid and drawer run in the browser. Anything that depends on today's date renders at build time, so the HTML and the hydrated page can't disagree.

    Trade-off: the interactive part still receives more data than it shows. Trimming that is the next step.

  5. 5. Résumé on request

    There is no PDF to download. The site carries what a screen needs, and the résumé comes by email, tailored to the role.

    Trade-off: one more step for a recruiter, in exchange for a conversation.

  6. 6. Accessibility is tested

    Contrast is measured, text never goes below 12px, touch targets are at least 24px, and motion respects the reduced-motion setting.

    Trade-off: a slightly less saturated tag palette.

Read the code

The repository is public: the data contracts, the tests and the CI workflow are all there, with an architecture overview in the README.

View the repository