Conversation
Replaces the Gatsby 3 / React 17 stack with a Vite 8 + React 19 + TypeScript 6 setup that prerenders every route to static HTML for S3 hosting. Stack decisions forced by compatibility ceilings, all verified against npm: - TypeScript pinned to 6.0.x: typescript-eslint declares `<6.1.0`, so TS 7 (the native port) has no typed-linting support yet. - ESLint 10 with eslint-plugin-jsx-a11y dropped: the plugin caps at ESLint 9, and ESLint 9 is EOL. Runtime a11y coverage moves to axe in Playwright. - vite-react-ssg dropped in favour of a ~40-line prerender script: it pinned react-router to a range carrying two unfixable moderate advisories (GHSA-wrjc-x8rr-h8h6, GHSA-337j-9hxr-rhxg). Now on react-router 7.18.3 with a clean `npm audit`. - Brand icons inlined: lucide-react v1 removed all brand glyphs. Design system is a strictly monochrome OKLCH ramp with Inter + JetBrains Mono, light/dark via a pre-paint inline script, and reduced-motion honoured. CV data is typed and centralised in src/data/cv.ts, including the new Senior DevOps Engineer role at Bank Vontobel AG (Jul 2026) and UBS closed at Jun 2026. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
45 Vitest tests and 18 Playwright tests, all green. Unit coverage focuses on invariants rather than implementation detail: CV data is checked for exactly one current role, newest-first ordering, no gap or overlap across the two UBS roles, Vontobel starting after UBS ends, and no phone number leaking into public data. E2E runs axe against every route in both light and dark themes, and the prerender specs run with JavaScript disabled so they can only pass if the HTML was genuinely prerendered. Two findings while writing these: - `vite preview` masks prerendering bugs. Its SPA fallback served the *home* page HTML for /about, /cv, /projects and /blog while still returning 200, so those routes looked fine and were not. E2E now runs against scripts/serve.mjs. - Clean URLs need an explicit rewrite. Prerendering emits /about/index.html but visitors request /about. The S3 *website* endpoint resolves this implicitly; the REST endpoint behind CloudFront OAC does not. The rule now lives once in scripts/rewrite.mjs, shared by the e2e server and the upcoming CloudFront Function, and is pinned by tests. This de-risks Phase 7. Also fixed: exactOptionalPropertyTypes rejected `workers: undefined`, unbound-method on Storage.prototype reassignment, and typed linting attempting to resolve a .d.mts outside the project. Coverage 98.5% statements / 88.5% branches against 70% thresholds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`vite preview` masks prerendering bugs: its SPA fallback serves index.html for any unmatched path, so /about, /cv and /blog returned the home page's HTML with a 200. Leaving it as the obvious local-preview command was a trap. `npm run preview` now builds and serves through scripts/serve.mjs, which applies the same rewrite rule as the CloudFront Function. Adds `npm run serve` for serving an existing build without rebuilding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Softer colours, computed against WCAG rather than eyeballed. Body text now lands ~8.2:1 in both themes instead of ~16:1, with muted text at ~5:1 — a real margin over the 4.5:1 AA floor. Light is a warm off-white (#f4f3f0) with charcoal text (#494843); dark is lifted off black (#201f1c) with soft light text (#b8b7b3). The ramp carries a little warmth (hue 95, chroma <= 0.008) so it reads as warm grey rather than a colour, keeping it monochrome. Collapsible sections are built on native <details>/<summary> rather than a JS disclosure widget, because this site is prerendered: the content is served and crawlable whether open or closed, the sections toggle with JavaScript disabled, and keyboard/screen-reader semantics come for free. A real <h2> sits inside each <summary> so the document outline survives. CV splits into Experience (open), Education and Skills. Home restructures into a bio, an Experience timeline and Elsewhere. Testing note: jsdom does not hide closed <details> content, so unit tests alone could not tell whether collapsing worked — the e2e suite is what verifies it. The prerender spec now distinguishes toBeAttached (in the HTML, for crawlers) from toBeVisible, which is the correct assertion for collapsed content. 52 unit + 24 e2e passing; axe clean on both themes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nests a disclosure per position inside the Experience section. The summary carries the organisation and its overall date range; roles, location and highlights live inside. Only the current position (Bank Vontobel AG) is open on arrival. Collapsible gains `level` and `variant`. `level` keeps the document outline valid — sections render an h2, nested entries an h3 — and `variant` separates a top-level band from an inner entry visually. Per-role dates now appear only where a position has more than one role. With a single role the summary's meta already shows the range, so it was rendering twice. Fixes a real bug the nesting exposed: unnamed Tailwind groups compile to a plain descendant selector, so `group-open:rotate-90` on a nested chevron matched *any* open ancestor `.group`. Every collapsed position rendered a rotated chevron whenever Experience was open. Now uses named groups (group/section, group/entry), with an e2e regression guard. That guard initially read getComputedStyle().transform and reported no rotation in either state: Tailwind v4 emits the standalone `rotate` property, not a transform. The test reads `rotate` now. 55 unit + 30 e2e passing; axe clean on both themes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opening one position now collapses the others, via the native <details name> attribute rather than JavaScript state. Verified in the target runtime before adopting it, including with scripting disabled — exclusivity is browser behaviour, so it survives the JS-free path the prerender specs exercise. The expanded entry sits in a slightly lighter rounded box, driven by a new --highlight token. Dark is held at oklch(27%): 30% looked better but dropped muted text to 4.2:1, below the AA floor, whereas 27% keeps it at 4.7:1. Light sits at 98.5%, where muted stays at 5.3:1. Uses a dedicated token rather than reusing --raised, which is darker than the page in dark mode and is already spoken for by the skip link. Adds e2e coverage for exclusivity (with and without JavaScript), for the highlight following the open entry, and for the box being lighter than the page in both themes. 55 unit + 34 e2e passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.