Routes are code-split, so every build names its chunks by content hash and a deploy replaces the whole .output. A tab opened before the deploy still holds the previous build's HTML, asks for /assets/*-<oldhash>.js, and the rejected dynamic import takes the page down — the blank page reported for the docs site, the console and the marketing site alike. defaultPreload: "intent" widens the window: a hover is enough to trip it.
Vite raises vite:preloadError for exactly this, and its preload helper wraps both the dependency preloads and the module import itself, so the event fires before the router ever sees the rejection. A full page load is the whole fix, because the fresh HTML names the new chunks.
@unom/ui had already shipped this.reloadOnStaleChunk has been exported the whole time and nothing had ever imported it, so the first version of this branch was a fourth implementation of a solved problem. Both routers now call the library instead. @unom/ui@0.10.0 grew the two things that using it turned up:
an optional resolveTarget, so the recovery lands on the page the user was navigating to instead of reloading in place;
a try/catch around the sessionStorage guard — blocked storage throws on getItem, and that throw escaping the listener leaves the tab on exactly the dead page the helper exists to prevent.
It installs once, so the callback reads a module-level ref rather than closing over a router: hydration builds a second router and discards the first, the same reason the console keeps its QueryClient in one.
Verified, with a control
Built the docs site, served it, and used Playwright to 404 the chunks an open tab requests while letting the "new build" answer normally after the recovery load. Without the handler the page collapses to 32 characters and no <main> — the reported symptom. With it: full load, lands on /docs, 4791 characters, zero page errors. Re-confirmed after the Vite 8 bump and again after switching to the library helper.
Dependencies
Both apps move to Vite 8, TypeScript 7, plugin-react 6, vite-tsconfig-paths 6, @types/node 26, plus everything in range. @unom/ui 0.10.0 widened its motion peer, which is what had pinned the console to motion 12 — it is now on 13.1. The console's lucide-react 0.469→1.34 and tailwind-merge 2.6→3.6 were fixing real peer mismatches rather than chasing numbers.
biome 2.5 in web/
biome is not in ci.yml's web gate, so formatting and import order had drifted; biome check --write accounts for most of that diff. Three findings were real:
Library lifted the whole entry list into state for a providers-card consumer that no longer exists — written on every grid load, read by nothing. The state, the onEntries prop threading it up and the effect firing it are gone.
flatten in SourceSettings spread its accumulator per allOf branch; Object.assign onto an accumulator that already starts as a fresh copy is the same result without the O(n²).
LogsCard's newestVisible dep carried a comment begging the next person not to "fix" it. It is a trigger, not a read, and removing it silently breaks follow-mode — now a biome-ignore, so the tool enforces it instead of prose hoping to.
public/ and storybook-static/ are excluded: biome was linting the favicon as inline JSX and asking it for a <title>.
Companion changes
@unom/ui@0.10.0 and @unom/app-ui@0.3.0 published; both repos' main also reconciled, because each had its published version sitting on an unmerged branch.
punktfunk-website carries the same fix (with locale-aware re-localization) on its own main.
Routes are code-split, so every build names its chunks by content hash and a deploy replaces the whole `.output`. A tab opened before the deploy still holds the previous build's HTML, asks for `/assets/*-<oldhash>.js`, and the rejected dynamic import takes the page down — the blank page reported for the docs site, the console and the marketing site alike. `defaultPreload: "intent"` widens the window: a hover is enough to trip it.
Vite raises `vite:preloadError` for exactly this, and its preload helper wraps both the dependency preloads and the module import itself, so the event fires before the router ever sees the rejection. A full page load is the whole fix, because the fresh HTML names the new chunks.
**`@unom/ui` had already shipped this.** `reloadOnStaleChunk` has been exported the whole time and nothing had ever imported it, so the first version of this branch was a fourth implementation of a solved problem. Both routers now call the library instead. `@unom/ui@0.10.0` grew the two things that using it turned up:
- an optional `resolveTarget`, so the recovery lands on the page the user was navigating to instead of reloading in place;
- a try/catch around the sessionStorage guard — blocked storage throws on `getItem`, and that throw escaping the listener leaves the tab on exactly the dead page the helper exists to prevent.
It installs once, so the callback reads a module-level ref rather than closing over a router: hydration builds a second router and discards the first, the same reason the console keeps its QueryClient in one.
### Verified, with a control
Built the docs site, served it, and used Playwright to 404 the chunks an open tab requests while letting the "new build" answer normally after the recovery load. **Without the handler the page collapses to 32 characters and no `<main>`** — the reported symptom. With it: full load, lands on `/docs`, 4791 characters, zero page errors. Re-confirmed after the Vite 8 bump and again after switching to the library helper.
### Dependencies
Both apps move to Vite 8, TypeScript 7, plugin-react 6, vite-tsconfig-paths 6, `@types/node` 26, plus everything in range. `@unom/ui` 0.10.0 widened its motion peer, which is what had pinned the console to motion 12 — it is now on 13.1. The console's lucide-react 0.469→1.34 and tailwind-merge 2.6→3.6 were fixing real peer mismatches rather than chasing numbers.
### biome 2.5 in `web/`
biome is not in ci.yml's web gate, so formatting and import order had drifted; `biome check --write` accounts for most of that diff. Three findings were real:
- Library lifted the whole entry list into state for a providers-card consumer that no longer exists — written on every grid load, read by nothing. The state, the `onEntries` prop threading it up and the effect firing it are gone.
- `flatten` in SourceSettings spread its accumulator per `allOf` branch; `Object.assign` onto an accumulator that already starts as a fresh copy is the same result without the O(n²).
- LogsCard's `newestVisible` dep carried a comment begging the next person not to "fix" it. It is a trigger, not a read, and removing it silently breaks follow-mode — now a `biome-ignore`, so the tool enforces it instead of prose hoping to.
`public/` and `storybook-static/` are excluded: biome was linting the favicon as inline JSX and asking it for a `<title>`.
### Companion changes
- `@unom/ui@0.10.0` and `@unom/app-ui@0.3.0` published; both repos' `main` also reconciled, because each had its published version sitting on an unmerged branch.
- `punktfunk-website` carries the same fix (with locale-aware re-localization) on its own `main`.
docs-site: build + tsc clean. web: build + tsc + 22 tests + biome clean, `bun.nix` regenerated.
Routes are code-split, so every build names its chunks by content hash and a deploy
replaces the whole `.output`. A tab opened before the deploy still holds the previous
build's HTML, so the first navigation asks for `/assets/*-<oldhash>.js` — which the new
server has never heard of. The dynamic import rejects, nothing catches it, and the page
goes blank. `defaultPreload: "intent"` widens the window: a hover is enough to trip it.
Vite raises `vite:preloadError` for exactly this, and its preload helper wraps both the
dependency preloads and the module import itself, so the event fires before the router
ever sees the rejection. A full page load is the entire fix — the fresh HTML names the
new chunks. Navigating to `latestLocation` rather than reloading in place means a click
that tripped this still lands on the page the user asked for.
Deliberately not `preventDefault()`: that suppresses Vite's rethrow and resolves the
import with `undefined`, handing the router a broken module on the way out. A
sessionStorage stamp keeps a genuinely broken deploy from spinning in a reload loop, and
its read is wrapped because storage can be blocked outright.
Verified on the built docs site under Playwright, serving 404s for the chunks an open tab
asks for: without the handler the page collapses to 32 characters and no <main>, with it
the tab reloads onto /docs and renders in full. Re-verified after the Vite 8 bump.
Everything in range moves to current — TanStack router/start/query, fumadocs, radix,
recharts, orval, storybook, biome, paraglide, playwright — and the majors that were
holding both apps a generation back go with them: Vite 7 to 8, @vitejs/plugin-react 5 to
6, vite-tsconfig-paths 5 to 6, @types/node 22 to 26, TypeScript 5.9 to 7. The marketing
site already ran the Vite 8 line against TanStack Start, so this closes a split the
monorepo was carrying for no reason.
Two of the console's majors were fixing a mismatch rather than chasing a number:
@unom/ui@0.9.2 asks for lucide-react ^1.17 and depends on tailwind-merge ^3.6, while the
console was pinned to lucide-react 0.469 and tailwind-merge 2.6.
motion is deliberately left on 12.43. 13.1 is available, but @unom/ui declares motion
^12.40 as a peer and inlines framer-motion 12.38, so moving the app alone would put two
motion runtimes in one tree — and a split motion context is exactly the failure that
neither a typecheck nor a build can see. It moves when @unom/ui does.
docs-site: build + tsc clean. web: build + tsc + 22 tests clean, bun.nix regenerated.
@unom/ui has exported `reloadOnStaleChunk` for exactly this the whole time and nothing had
ever imported it — the copy added a few commits ago was a fourth implementation of a solved
problem. Both routers now call the library.
0.10.0 grew the two things using it turned up: an optional `resolveTarget`, so the recovery
lands on the page the user was navigating to rather than reloading in place, and a try/catch
around the sessionStorage guard, because blocked storage throws on `getItem` and that throw
escaping the listener leaves the tab on exactly the dead page the helper exists to prevent.
The helper installs once, so the callback reads a module-level ref rather than closing over a
router: hydration builds a second router and discards the first, which is the same reason the
console keeps its QueryClient in one.
Deps: @unom/ui 0.10.0 and motion 13.1 (0.10.0 widened the peer, which is what had pinned us to
12), plus @unom/app-ui 0.3.0 for the docs site.
docs-site: build + tsc clean, and the deploy simulation still recovers onto /docs with the
library helper in place. web: build + tsc + 22 tests clean, bun.nix regenerated.
biome is not in ci.yml's web gate, so formatting and import order had drifted; `biome check
--write` accounts for most of this diff. `biome migrate` moved the deprecated
linter.rules.recommended to rules.preset. public/ and storybook-static/ are excluded — biome
was linting the favicon as if it were inline JSX and asking it for a <title>.
Three findings were real:
- Library lifted the whole entry list into state so the providers card could count owners.
That consumer is gone: `entries` was written on every grid load and read by nothing. The
state, the `onEntries` prop threading it up, and the effect firing it are all removed —
including from LibraryGrid, where the callback had no callers left.
- `flatten` in SourceSettings spread the accumulator per `allOf` branch, rebuilding the object
each time. Object.assign onto an accumulator that already starts as a fresh copy of the node
is the same result without the O(n²).
- LogsCard's `newestVisible` dep already carried a comment begging the next person not to
"fix" it — it is a trigger, not a read, and removing it silently breaks follow-mode. That
warning is now a biome-ignore, so the tool enforces it instead of prose hoping to.
Build, tsc, 22 tests and biome all clean.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Routes are code-split, so every build names its chunks by content hash and a deploy replaces the whole
.output. A tab opened before the deploy still holds the previous build's HTML, asks for/assets/*-<oldhash>.js, and the rejected dynamic import takes the page down — the blank page reported for the docs site, the console and the marketing site alike.defaultPreload: "intent"widens the window: a hover is enough to trip it.Vite raises
vite:preloadErrorfor exactly this, and its preload helper wraps both the dependency preloads and the module import itself, so the event fires before the router ever sees the rejection. A full page load is the whole fix, because the fresh HTML names the new chunks.@unom/uihad already shipped this.reloadOnStaleChunkhas been exported the whole time and nothing had ever imported it, so the first version of this branch was a fourth implementation of a solved problem. Both routers now call the library instead.@unom/ui@0.10.0grew the two things that using it turned up:resolveTarget, so the recovery lands on the page the user was navigating to instead of reloading in place;getItem, and that throw escaping the listener leaves the tab on exactly the dead page the helper exists to prevent.It installs once, so the callback reads a module-level ref rather than closing over a router: hydration builds a second router and discards the first, the same reason the console keeps its QueryClient in one.
Verified, with a control
Built the docs site, served it, and used Playwright to 404 the chunks an open tab requests while letting the "new build" answer normally after the recovery load. Without the handler the page collapses to 32 characters and no
<main>— the reported symptom. With it: full load, lands on/docs, 4791 characters, zero page errors. Re-confirmed after the Vite 8 bump and again after switching to the library helper.Dependencies
Both apps move to Vite 8, TypeScript 7, plugin-react 6, vite-tsconfig-paths 6,
@types/node26, plus everything in range.@unom/ui0.10.0 widened its motion peer, which is what had pinned the console to motion 12 — it is now on 13.1. The console's lucide-react 0.469→1.34 and tailwind-merge 2.6→3.6 were fixing real peer mismatches rather than chasing numbers.biome 2.5 in
web/biome is not in ci.yml's web gate, so formatting and import order had drifted;
biome check --writeaccounts for most of that diff. Three findings were real:onEntriesprop threading it up and the effect firing it are gone.flattenin SourceSettings spread its accumulator perallOfbranch;Object.assignonto an accumulator that already starts as a fresh copy is the same result without the O(n²).newestVisibledep carried a comment begging the next person not to "fix" it. It is a trigger, not a read, and removing it silently breaks follow-mode — now abiome-ignore, so the tool enforces it instead of prose hoping to.public/andstorybook-static/are excluded: biome was linting the favicon as inline JSX and asking it for a<title>.Companion changes
@unom/ui@0.10.0and@unom/app-ui@0.3.0published; both repos'mainalso reconciled, because each had its published version sitting on an unmerged branch.punktfunk-websitecarries the same fix (with locale-aware re-localization) on its ownmain.docs-site: build + tsc clean. web: build + tsc + 22 tests + biome clean,
bun.nixregenerated.