Skip to content

Rebuild - #5

Open
eschanet wants to merge 6 commits into
mainfrom
rebuild
Open

Rebuild#5
eschanet wants to merge 6 commits into
mainfrom
rebuild

Conversation

@eschanet

@eschanet eschanet commented Sep 7, 2026

Copy link
Copy Markdown
Owner

No description provided.

eschanet and others added 6 commits September 2, 2026 22:10
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant