Skip to content

fix: static SiteHeader (no hydration) + build-time guards against hydration regressions - #57

Merged
ronaldtse merged 1 commit into
mainfrom
fix/static-site-header
Sep 6, 2026
Merged

fix: static SiteHeader (no hydration) + build-time guards against hydration regressions#57
ronaldtse merged 1 commit into
mainfrom
fix/static-site-header

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

Summary

Fixes the broken pages after the Astro migration, and adds the requested tests to prevent this class of regression.

Root causes (two, compounding)

  1. <Teleport to="body"> in the SiteHeader Vue island. Vue SSR renders teleport content as anchor comments inside the island; on hydration the browser cannot match them, producing the hydration mismatches and a dead/broken header on every page.
  2. build.format: 'file' leaks file paths into URLs. Astro.url.pathname is /leadership.html at build, which was serialized into the island's current-path prop and — worse — emitted as <link rel=canonical> on every page (/index for home).

Fix

  • SiteHeader is now an Astro component with zero hydration: CSS-only hover dropdowns (plus focus-within), vanilla-script mobile drawer (rendered outside the backdrop-filtered header so fixed positioning holds), theme toggle whose icons swap purely via dark: classes, scroll state via one class toggle. Vue islands now exist only on the home hero, which has no Teleport and deterministic render.
  • BaseLayout normalizes the pathname (strips .html, collapses /index to /) for canonicals and nav state.

Tests (the requested guardrails)

  • scripts/islands.test.ts (vitest, runs in CI via npm test): islands must not use <Teleport>; must not import vue-router; must not touch browser globals (window/document/localStorage/…) outside lifecycle hooks; layout must strip the .html suffix.
  • scripts/verify-dist.ts (wired into npm run build, fails the build): every page's canonical equals its public URL; no .html in serialized island props; every island's component-url asset is emitted with non-empty SSR content. This verifier is what caught the /index home canonical and validated the fix — build now reports 67 pages, 65 canonicals, 2 islands — all invariants hold.

…ainst hydration regressions

The header island broke every page: <Teleport to="body"> renders SSR
teleport anchors that the browser cannot match on hydration (mismatch
errors, dead header), and build.format 'file' leaked '/leadership.html'
into the serialized current-path prop and the canonical URLs.

- SiteHeader is now an Astro component with zero hydration: CSS hover
  dropdowns, vanilla-script mobile drawer and theme toggle, scroll state
  via one class toggle. Vue islands remain only on the home hero
- BaseLayout strips the .html and /index suffixes for canonicals and nav
  state (home canonical was '/index')
- scripts/islands.test.ts: architectural guards — no Teleport, no
  vue-router, no browser globals in island render paths
- scripts/verify-dist.ts (wired into npm run build): post-build invariants
  over dist/ — canonical equals the public URL on every page, no .html in
  serialized island props, island assets emitted and SSR content non-empty
@ronaldtse
ronaldtse merged commit 6487738 into main Sep 6, 2026
3 checks passed
@ronaldtse
ronaldtse deleted the fix/static-site-header branch September 6, 2026 21:13
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