@rangojs/router 0.24.3 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  21. package/dist/types/cache/types.d.ts +8 -0
  22. package/dist/types/client-urls/client-root.d.ts +4 -1
  23. package/dist/types/client-urls/client-urls.d.ts +28 -2
  24. package/dist/types/client-urls/navigation.d.ts +6 -8
  25. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  26. package/dist/types/client-urls/server-projection.d.ts +20 -5
  27. package/dist/types/client-urls/types.d.ts +35 -17
  28. package/dist/types/client.d.ts +2 -2
  29. package/dist/types/client.rsc.d.ts +2 -2
  30. package/dist/types/decode-loader-results.d.ts +28 -7
  31. package/dist/types/href-client.d.ts +3 -1
  32. package/dist/types/index.rsc.d.ts +1 -1
  33. package/dist/types/loader-redirect.d.ts +2 -3
  34. package/dist/types/outlet-context.d.ts +7 -6
  35. package/dist/types/prefetch-markers.d.ts +14 -5
  36. package/dist/types/route-content-wrapper.d.ts +17 -14
  37. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  38. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  39. package/dist/types/route-definition/rewrite.d.ts +48 -5
  40. package/dist/types/route-types.d.ts +2 -2
  41. package/dist/types/router/handler-context.d.ts +14 -2
  42. package/dist/types/router/intercept-resolution.d.ts +19 -46
  43. package/dist/types/router/lazy-includes.d.ts +7 -0
  44. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  45. package/dist/types/router/middleware.d.ts +0 -16
  46. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  47. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  48. package/dist/types/router/revalidation.d.ts +8 -1
  49. package/dist/types/router/router-context.d.ts +4 -16
  50. package/dist/types/router/router-interfaces.d.ts +0 -6
  51. package/dist/types/router/router-options.d.ts +4 -7
  52. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  53. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  54. package/dist/types/router/segment-resolution.d.ts +1 -1
  55. package/dist/types/router/segment-wrappers.d.ts +3 -5
  56. package/dist/types/router/substitute-pattern-params.d.ts +19 -2
  57. package/dist/types/router/tracing.d.ts +2 -2
  58. package/dist/types/rsc/full-payload.d.ts +14 -2
  59. package/dist/types/rsc/helpers.d.ts +8 -0
  60. package/dist/types/rsc/manifest-init.d.ts +20 -0
  61. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  62. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  63. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  64. package/dist/types/rsc/server-action.d.ts +6 -0
  65. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  66. package/dist/types/rsc/shell-capture.d.ts +7 -0
  67. package/dist/types/rsc/types.d.ts +5 -0
  68. package/dist/types/segment-system.d.ts +15 -0
  69. package/dist/types/server/context.d.ts +64 -4
  70. package/dist/types/server/request-context.d.ts +41 -4
  71. package/dist/types/server/request-headers.d.ts +12 -0
  72. package/dist/types/server/rewrite-trail.d.ts +8 -1
  73. package/dist/types/testing/e2e/index.d.ts +1 -1
  74. package/dist/types/testing/flight.entry.d.ts +1 -1
  75. package/dist/types/testing/index.d.ts +1 -1
  76. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  77. package/dist/types/testing/render-route.d.ts +21 -11
  78. package/dist/types/testing/run-loader.d.ts +7 -0
  79. package/dist/types/testing/run-middleware.d.ts +7 -0
  80. package/dist/types/testing/run-revalidate.d.ts +5 -4
  81. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  82. package/dist/types/testing/shell-status.d.ts +17 -0
  83. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  84. package/dist/types/testing/vitest.d.ts +10 -1
  85. package/dist/types/thenable-status.d.ts +0 -6
  86. package/dist/types/types/handler-context.d.ts +12 -1
  87. package/dist/types/types/loader-types.d.ts +4 -3
  88. package/dist/types/types/route-entry.d.ts +2 -1
  89. package/dist/types/urls/path-helper-types.d.ts +4 -2
  90. package/dist/types/urls/path-helper.d.ts +5 -0
  91. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  92. package/dist/types/vite/discovery/state.d.ts +20 -0
  93. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  94. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  95. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  96. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  97. package/dist/vite/index.js +6816 -6547
  98. package/package.json +1 -1
  99. package/skills/app-structure/SKILL.md +255 -0
  100. package/skills/app-structure/content-site.md +333 -0
  101. package/skills/app-structure/e-commerce.md +435 -0
  102. package/skills/app-structure/large-app.md +885 -0
  103. package/skills/app-structure/multi-app.md +280 -0
  104. package/skills/breadcrumbs/SKILL.md +6 -1
  105. package/skills/caching/SKILL.md +3 -1
  106. package/skills/catalog.json +12 -0
  107. package/skills/client-urls/SKILL.md +195 -32
  108. package/skills/composability/SKILL.md +10 -8
  109. package/skills/handler-use/SKILL.md +48 -9
  110. package/skills/hooks/handle-and-actions.md +104 -0
  111. package/skills/hooks/navigation.md +24 -10
  112. package/skills/hooks/urls.md +1 -1
  113. package/skills/i18n/SKILL.md +2 -1
  114. package/skills/intercept/SKILL.md +60 -29
  115. package/skills/layout/SKILL.md +6 -3
  116. package/skills/links/SKILL.md +151 -17
  117. package/skills/loader/SKILL.md +32 -10
  118. package/skills/middleware/SKILL.md +111 -15
  119. package/skills/observability/SKILL.md +11 -0
  120. package/skills/parallel/SKILL.md +5 -5
  121. package/skills/ppr/SKILL.md +43 -8
  122. package/skills/prerender/SKILL.md +1 -2
  123. package/skills/rango/SKILL.md +3 -0
  124. package/skills/response-routes/SKILL.md +4 -1
  125. package/skills/scripts/SKILL.md +12 -1
  126. package/skills/server-actions/SKILL.md +60 -20
  127. package/skills/testing/SKILL.md +1 -0
  128. package/skills/testing/client-components.md +13 -10
  129. package/skills/testing/loader.md +25 -24
  130. package/skills/testing/middleware.md +11 -11
  131. package/skills/testing/response-routes.md +4 -1
  132. package/skills/testing/serve-shell-request.md +85 -21
  133. package/skills/ux-architecture/SKILL.md +980 -0
  134. package/skills/view-transitions/SKILL.md +43 -10
  135. package/src/__augment-tests__/augmented.check.ts +28 -0
  136. package/src/browser/event-controller.ts +44 -18
  137. package/src/browser/intercept-utils.ts +3 -2
  138. package/src/browser/merge-segment-loaders.ts +0 -85
  139. package/src/browser/navigation-bridge.ts +119 -98
  140. package/src/browser/navigation-client.ts +20 -15
  141. package/src/browser/navigation-store.ts +4 -2
  142. package/src/browser/navigation-transaction.ts +9 -8
  143. package/src/browser/partial-update.ts +90 -32
  144. package/src/browser/prefetch/cache.ts +8 -0
  145. package/src/browser/prefetch/fetch.ts +13 -3
  146. package/src/browser/rango-marker.ts +13 -0
  147. package/src/browser/react/NavigationProvider.tsx +25 -7
  148. package/src/browser/react/filter-segment-order.ts +13 -0
  149. package/src/browser/react/use-action.ts +9 -15
  150. package/src/browser/react/use-handle.ts +63 -71
  151. package/src/browser/react/use-href.tsx +24 -5
  152. package/src/browser/react/use-link-status.ts +1 -1
  153. package/src/browser/react/use-navigation.ts +30 -40
  154. package/src/browser/react/use-reverse.ts +4 -17
  155. package/src/browser/react/use-router.ts +37 -6
  156. package/src/browser/react/use-segments.ts +8 -12
  157. package/src/browser/response-adapter.ts +11 -5
  158. package/src/browser/rsc-router.tsx +3 -1
  159. package/src/browser/scroll-restoration.ts +34 -20
  160. package/src/browser/segment-reconciler.ts +11 -29
  161. package/src/browser/server-action-bridge.ts +52 -16
  162. package/src/browser/types.ts +40 -8
  163. package/src/build/generate-manifest.ts +12 -1
  164. package/src/build/route-types/codegen.ts +6 -2
  165. package/src/build/route-types/per-module-writer.ts +94 -61
  166. package/src/cache/cache-runtime.ts +9 -0
  167. package/src/cache/cache-scope.ts +2 -1
  168. package/src/cache/cf/cf-cache-store.ts +2 -0
  169. package/src/cache/cf/cf-shell-frame.ts +3 -0
  170. package/src/cache/types.ts +8 -0
  171. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  172. package/src/client-urls/client-root.tsx +10 -5
  173. package/src/client-urls/client-urls.ts +246 -166
  174. package/src/client-urls/navigation.ts +45 -38
  175. package/src/client-urls/revalidate-registry.ts +12 -3
  176. package/src/client-urls/server-projection.ts +286 -75
  177. package/src/client-urls/types.ts +40 -16
  178. package/src/client.rsc.tsx +11 -1
  179. package/src/client.tsx +9 -1
  180. package/src/decode-loader-results.ts +64 -31
  181. package/src/href-client.ts +3 -1
  182. package/src/index.rsc.ts +1 -0
  183. package/src/index.ts +12 -4
  184. package/src/loader-redirect.tsx +2 -3
  185. package/src/outlet-context.ts +7 -6
  186. package/src/prefetch-markers.ts +15 -5
  187. package/src/reverse.ts +3 -3
  188. package/src/route-content-wrapper.tsx +19 -17
  189. package/src/route-definition/dsl-helpers.ts +27 -7
  190. package/src/route-definition/helpers-types.ts +7 -2
  191. package/src/route-definition/resolve-handler-use.ts +23 -10
  192. package/src/route-definition/rewrite.ts +86 -19
  193. package/src/route-types.ts +1 -2
  194. package/src/router/find-match.ts +37 -1
  195. package/src/router/handler-context.ts +85 -40
  196. package/src/router/intercept-resolution.ts +161 -307
  197. package/src/router/lazy-includes.ts +43 -27
  198. package/src/router/loader-resolution.ts +21 -1
  199. package/src/router/manifest.ts +6 -8
  200. package/src/router/match-api.ts +11 -1
  201. package/src/router/match-middleware/background-revalidation.ts +2 -4
  202. package/src/router/match-middleware/cache-lookup.ts +16 -20
  203. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  204. package/src/router/match-result.ts +31 -2
  205. package/src/router/middleware.ts +6 -147
  206. package/src/router/navigation-snapshot.ts +38 -2
  207. package/src/router/prefetch-cache-ttl.ts +10 -18
  208. package/src/router/prerender-match.ts +2 -1
  209. package/src/router/revalidation.ts +24 -3
  210. package/src/router/router-context.ts +5 -18
  211. package/src/router/router-interfaces.ts +0 -7
  212. package/src/router/router-options.ts +4 -7
  213. package/src/router/segment-resolution/fresh.ts +6 -2
  214. package/src/router/segment-resolution/helpers.ts +7 -1
  215. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  216. package/src/router/segment-resolution/revalidation.ts +75 -35
  217. package/src/router/segment-resolution.ts +0 -1
  218. package/src/router/segment-wrappers.ts +8 -13
  219. package/src/router/substitute-pattern-params.ts +37 -2
  220. package/src/router/tracing.ts +2 -2
  221. package/src/router.ts +2 -9
  222. package/src/rsc/full-payload.ts +26 -4
  223. package/src/rsc/handler.ts +83 -41
  224. package/src/rsc/helpers.ts +13 -0
  225. package/src/rsc/manifest-init.ts +56 -3
  226. package/src/rsc/prefetch-render-error.ts +38 -0
  227. package/src/rsc/progressive-enhancement.ts +161 -211
  228. package/src/rsc/rewrite-passes.ts +56 -9
  229. package/src/rsc/rsc-rendering.ts +79 -23
  230. package/src/rsc/server-action.ts +25 -2
  231. package/src/rsc/shell-capture-constants.ts +22 -0
  232. package/src/rsc/shell-capture.ts +11 -0
  233. package/src/rsc/types.ts +5 -0
  234. package/src/segment-system.tsx +125 -119
  235. package/src/server/context.ts +96 -5
  236. package/src/server/handle-store.ts +3 -2
  237. package/src/server/request-context.ts +42 -3
  238. package/src/server/request-headers.ts +32 -0
  239. package/src/server/rewrite-trail.ts +55 -9
  240. package/src/ssr/ssr-root.tsx +6 -10
  241. package/src/testing/dispatch.ts +4 -5
  242. package/src/testing/e2e/index.ts +1 -0
  243. package/src/testing/flight.entry.ts +1 -0
  244. package/src/testing/index.ts +1 -0
  245. package/src/testing/internal/client-urls-projection.ts +31 -11
  246. package/src/testing/render-route.tsx +83 -40
  247. package/src/testing/run-loader.ts +26 -2
  248. package/src/testing/run-middleware.ts +10 -0
  249. package/src/testing/run-revalidate.ts +13 -6
  250. package/src/testing/serve-shell-request.ts +77 -4
  251. package/src/testing/shell-status.ts +40 -0
  252. package/src/testing/vitest-stubs/version.ts +1 -0
  253. package/src/testing/vitest.ts +74 -7
  254. package/src/thenable-status.ts +0 -11
  255. package/src/types/handler-context.ts +12 -1
  256. package/src/types/loader-types.ts +4 -3
  257. package/src/types/route-entry.ts +2 -1
  258. package/src/urls/include-helper.ts +38 -11
  259. package/src/urls/path-helper-types.ts +4 -2
  260. package/src/urls/path-helper.ts +16 -3
  261. package/src/vite/discovery/client-urls-projection.ts +205 -16
  262. package/src/vite/discovery/discover-routers.ts +2 -2
  263. package/src/vite/discovery/prerender-collection.ts +2 -6
  264. package/src/vite/discovery/state.ts +26 -0
  265. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  266. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  267. package/src/vite/plugins/hoist/source.ts +8 -7
  268. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  269. package/src/vite/plugins/version-plugin.ts +7 -0
  270. package/src/vite/plugins/version.d.ts +7 -0
  271. package/src/vite/plugins/virtual-entries.ts +20 -0
  272. package/src/vite/rango.ts +4 -1
  273. package/src/vite/router-discovery.ts +39 -7
  274. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -172,7 +172,7 @@ function ShopNav() {
172
172
  }
173
173
  ```
174
174
 
175
- Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths. The function `useHref()` returns is referentially stable within a mount, so it is safe as a hook dependency or memoized prop.
175
+ Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths. Under a parameterized mount (`include("/:accountId", ...)`) it fills the mount's params from the current route params (`/acme/cart` at `/acme/board`); a missing one makes the returned function throw `Missing param`. The function is referentially stable while the mount's own param values stay the same, so it is safe as a hook dependency or memoized prop.
176
176
 
177
177
  ### useMount()
178
178
 
@@ -162,7 +162,8 @@ ctx.reverse("menu.category", { slug: "breads" }); // → "/c/breads" on an unpre
162
162
  `ctx.reverse()` auto-fills missing params from the current request, so the
163
163
  last call returns `/en/c/breads` when the current URL is `/en/...`. That is
164
164
  what you want for in-locale links; to leave the locale, pass it explicitly
165
- (`{ locale: "fr" }`, or `undefined`/`""` for the bare default). Middleware and
165
+ (`{ locale: "fr" }`, or `undefined`/`""` for the bare default; under a
166
+ required `/:locale`, `undefined` throws `Missing param "locale"`). Middleware and
166
167
  response routes are the exception: their `ctx.reverse` fills nothing from the
167
168
  request, so pass `locale` there explicitly.
168
169
 
@@ -92,35 +92,39 @@ Only the first `layout()` in an intercept's `use()` is applied. It is modal
92
92
  chrome only: a `layout(Chrome, () => [...])` with its own `use()` items throws.
93
93
  Attach loaders and the rest to the intercept itself.
94
94
 
95
- ## Intercept Middleware
95
+ ## Middleware and an Intercept
96
96
 
97
- Intercepts support their own middleware chain via the use callback. The full chain for an intercept request is:
97
+ An intercept has no middleware of its own. The target route's middleware runs
98
+ for the modal on every request that renders it, a `cache()` hit and a
99
+ `Prerender()` target included. The chain for an intercept request is:
98
100
 
99
101
  ```
100
- global mw (router.use) -> route mw (urls middleware()) -> intercept mw -> intercept loaders + handler
102
+ global mw (router.use) -> route mw (the target route's middleware()) -> intercept loaders + handler
101
103
  ```
102
104
 
103
105
  The intercept's loaders are started before its handler is called, so they run
104
- in parallel with it. If intercept middleware returns a `Response` (e.g. a
105
- redirect), the intercept stops there.
106
+ in parallel with it. If route middleware returns a `Response` (e.g. a
107
+ redirect), the request stops there, the modal with it.
106
108
 
107
109
  ```typescript
108
- intercept(
109
- "@modal",
110
- "product",
111
- <ProductModal />,
112
- () => [
113
- middleware(async (ctx, next) => {
114
- // Runs only for this intercept, after global and route middleware
115
- ctx.set("interceptSource", "modal");
116
- await next();
117
- }),
118
- loader(ProductLoader),
119
- ]
120
- )
110
+ path("/product/:id", ProductPage, { name: "product" }, () => [
111
+ middleware(async (ctx, next) => {
112
+ // Runs for the full page and for the modal
113
+ ctx.set(Viewer, await getViewer(ctx.request));
114
+ await next();
115
+ }),
116
+ ]),
117
+ intercept("@modal", "product", <ProductModal />, () => [
118
+ loader(ProductLoader), // ctx.get(Viewer) is set here
119
+ ]),
121
120
  ```
122
121
 
123
- The intercept handler can read context variables set by all upstream middleware layers (global, route, and intercept-specific).
122
+ `middleware()` in an intercept's `use()`, or in its handler's `.use`, throws
123
+ when the routes are defined: move it to the route's `middleware()`, or do the
124
+ work in the intercept's loader.
125
+
126
+ The intercept handler and its loaders read the context variables global and
127
+ route middleware set.
124
128
 
125
129
  Handler/layout `ctx.set()` data follows the same rule as elsewhere:
126
130
  intercepts see data produced in the current render pass, but partial
@@ -136,7 +140,11 @@ re-render every time the intercept resolves live — each intercepted
136
140
  navigation, and each action submitted while the modal is open. When the
137
141
  segment cache or a prerendered intercept variant serves the modal, the stored
138
142
  handler output is reused and only the loaders run. Each loader decides for
139
- itself through its own `revalidate()`, with the normal loader defaults.
143
+ itself through its own `revalidate()`, with the normal loader defaults: an
144
+ action from the modal re-runs only the loaders whose predicate says so, and
145
+ the modal keeps the data of the others, as it does on a link to another
146
+ intercepted route that reads the same loader. In `clientUrls()` the predicate
147
+ runs on the server too (`/client-urls`).
140
148
 
141
149
  A `revalidate()` placed directly in the intercept's `use()` (or returned from
142
150
  its handler's `.use`) throws at definition time. Attach it to the loader:
@@ -144,13 +152,13 @@ its handler's `.use`) throws at definition time. Attach it to the loader:
144
152
  ```typescript
145
153
  // Throws: revalidate() is not valid inside intercept("@modal", "product") use()
146
154
  intercept("@modal", "product", <ProductModal />, () => [
147
- revalidate(() => false),
155
+ revalidate(false),
148
156
  loader(ProductLoader),
149
157
  ]);
150
158
 
151
159
  // Correct: the loader owns the decision
152
160
  intercept("@modal", "product", <ProductModal />, () => [
153
- loader(ProductLoader, () => [revalidate(() => false)]),
161
+ loader(ProductLoader, () => [revalidate(false)]),
154
162
  ]);
155
163
  ```
156
164
 
@@ -176,7 +184,7 @@ layout(ProductLayout, () => [
176
184
  intercept("@modal", "product", <ProductModal />, () => [
177
185
  loader(ProductLoader, () => [
178
186
  revalidate(revalidateProductShell), // reruns with the producer
179
- revalidate(() => false), // otherwise keep the loaded data
187
+ revalidate(false), // otherwise keep the loaded data
180
188
  ]),
181
189
  ]),
182
190
  ]);
@@ -197,7 +205,7 @@ export const revalidateProduct = () => [
197
205
  layout(ProductLayout, () => [
198
206
  revalidateProduct(),
199
207
  intercept("@modal", "product", <ProductModal />, () => [
200
- loader(ProductLoader, () => [revalidateProduct(), revalidate(() => false)]),
208
+ loader(ProductLoader, () => [revalidateProduct(), revalidate(false)]),
201
209
  ]),
202
210
  ]);
203
211
  ```
@@ -244,6 +252,29 @@ back to `Referer`, and only when that `Referer` is on the request's own origin
244
252
  (scheme, host and port). A request with no usable navigation context is not
245
253
  intercepted: it gets the full page.
246
254
 
255
+ ### `prefetch: false` on an intercept's loaders
256
+
257
+ `loader(Def, { prefetch: false })` inside `intercept()` keeps that loader out
258
+ of a prefetch of the target from the source page:
259
+
260
+ ```typescript
261
+ intercept("@modal", "product", <ProductModal />, () => [
262
+ loader(ProductLoader),
263
+ loader(ReviewsLoader, { prefetch: false }), // skipped in the prefetch
264
+ loading(<ModalSkeleton />),
265
+ ]);
266
+ ```
267
+
268
+ The click that adopts the prefetch opens the modal at once over the page, with
269
+ its `loading()` while one fill request, sent from the source page, runs the
270
+ flagged loader. The page under the modal is not re-rendered or remounted.
271
+ Without `loading()` the slot shows nothing until the fill answers, as a plain
272
+ click shows nothing until its response. The modal's loaders resolve together,
273
+ so its `loading()` shows until the last one arrives. A handler that reads the
274
+ flagged loader with `ctx.use()` before its first `await` (or one with no
275
+ `loading()`) gets it run in the prefetch instead: one run, no fill for it.
276
+ `loading(…, { prefetch: false })` inside `intercept()` defers nothing.
277
+
247
278
  A prefetch of a route an intercept targets is stored per source page, whether
248
279
  or not the intercept applies from that page (`rsc-rendering.ts`
249
280
  `x-rsc-prefetch-scope: source`, set from `MatchResult.interceptTargeted`). A
@@ -284,7 +315,7 @@ intercept(
284
315
  <ProductModal />,
285
316
  () => [
286
317
  loader(ProductLoader, () => [cache()]),
287
- loader(ProductCartLoader, () => [revalidate(() => true)]),
318
+ loader(ProductCartLoader, () => [revalidate(true)]),
288
319
  loader(RecommendationsLoader),
289
320
  ]
290
321
  )
@@ -295,15 +326,15 @@ intercept(
295
326
  An intercept's `use()` (and its handler's `.use`) accepts:
296
327
 
297
328
  - `loader()`, with its own `revalidate()` and `cache()` inside it
298
- - `middleware()`
299
329
  - `loading()`
300
330
  - `layout(Chrome)`: the modal chrome, without `use()` items of its own
301
331
  - `route`, the item type a `path()` call produces
302
332
  - `transition()`
303
333
 
304
334
  Everything else throws at definition time with a pointer to where it goes:
305
- `revalidate()` onto the loader, `errorBoundary()` / `notFoundBoundary()` onto
306
- the enclosing layout (see below), and `cache()` onto the target route.
335
+ `middleware()` onto the target route, `revalidate()` onto the loader,
336
+ `errorBoundary()` / `notFoundBoundary()` onto the enclosing layout (see
337
+ below), and `cache()` onto the target route.
307
338
 
308
339
  ## Caching an Intercept
309
340
 
@@ -526,7 +557,7 @@ export const shopPatterns = urls(({
526
557
 
527
558
  ## Handler-attached `.use`
528
559
 
529
- Intercept handlers can carry their own middleware, loaders, loading state, and even nested `layout`/`route` defaults via `.use` — useful for self-contained modal components that travel with their own data and chrome. (Conditional activation is set via the `when` config on the mount-site `intercept()` call, not inside `.use`; boundaries go on the enclosing layout, see [Errors in an Intercept](#errors-in-an-intercept).)
560
+ Intercept handlers can carry their own loaders, loading state, and even nested `layout`/`route` defaults via `.use` — useful for self-contained modal components that travel with their own data and chrome. (Conditional activation is set via the `when` config on the mount-site `intercept()` call, not inside `.use`; boundaries go on the enclosing layout, see [Errors in an Intercept](#errors-in-an-intercept).)
530
561
 
531
562
  ```typescript
532
563
  import { layout, loader, loading, type Handler } from "@rangojs/router";
@@ -234,7 +234,9 @@ among the path's children (`/caching`).
234
234
  A standalone `layout()` renders when it is first mounted and is then kept as-is:
235
235
 
236
236
  - **Navigation** between routes it already wraps does not re-render it — even
237
- when the URL params change.
237
+ when the URL params change. Links it rendered with `ctx.reverse` (mount
238
+ params auto-filled) keep the old params too; render param-dependent nav in a
239
+ client component with `useReverse(routes)` (`/links`).
238
240
  - **Actions** skip it too (the revalidation trace calls this
239
241
  `action:parent-chain-skip`).
240
242
 
@@ -264,9 +266,10 @@ layout(LocaleLayout, () => [
264
266
  ])
265
267
  ```
266
268
 
267
- `revalidate(() => false)` on a standalone layout only restates the default.
269
+ `revalidate(false)` on a standalone layout only restates the default.
268
270
  The return shapes (hard boolean, soft `{ defaultShouldRevalidate }`, or defer)
269
- are covered in `/loader` → "`revalidate()` return shapes".
271
+ and the `revalidate(true | false)` shorthand are covered in `/loader` →
272
+ "`revalidate()` return shapes".
270
273
 
271
274
  If child segments read data that was established by this layout or by a
272
275
  route handler above them, revalidate the outer segment too. Partial
@@ -24,14 +24,62 @@ links to JSON/text endpoints see `/response-routes`.
24
24
 
25
25
  Available on the handler context (route handlers, layouts, parallels), on
26
26
  middleware and loader contexts, and on `getRequestContext()` inside server
27
- actions. Resolves named routes using the full route map. This is the default
27
+ actions. In loaders and in actions (after the route match) params the matched
28
+ route carries are optional, as in handlers: `ctx.reverse("account.settings")`
29
+ fills `accountId` from the matched params. In global middleware before `next()`
30
+ nothing is matched, so a route with a required param throws `Missing param`.
31
+ Resolves named routes using the full route map. This is the default
28
32
  way to generate URLs on the server. Patterns in the map already include the
29
33
  router `basename`, so results are basename-prefixed.
30
34
 
31
35
  Middleware and response routes (`path.json()`, `path.text()`, …) get a
32
36
  global-only `ctx.reverse` (`GlobalReverseFunction`): no `include()` scope and no
33
37
  param auto-fill, so pass the fully qualified name and every param. A dot-local
34
- `.name` is a type error there. See `/middleware` and `/response-routes`.
38
+ `.name` is a type error there. A missing required param throws
39
+ `Missing param`, with or without a params object. Route
40
+ middleware can use `getRequestContext().reverse` instead: it is rebound after
41
+ the match, takes `.name` and global names, and fills mount params. In global
42
+ middleware (`router.use`) it fills only after `next()`. See `/middleware` and
43
+ `/response-routes`.
44
+
45
+ A server action's `getRequestContext().reverse("global.name")` fills mount
46
+ params from the page URL the action was called on, so
47
+ `throw redirect(getRequestContext().reverse("account.settings"))` needs no
48
+ hardcoded path. A `.name` there resolves in that page's module; an action
49
+ shared by pages in different modules should use global names.
50
+
51
+ ### The module never knows its mount
52
+
53
+ Inside a module, link by local name: `ctx.reverse(".x")` on the server,
54
+ `useReverse(routes)` with the module's own `.gen.ts` on the client. Both
55
+ resolve against wherever the module is mounted and fill the mount's params
56
+ from the current URL, so the same module can be mounted twice:
57
+
58
+ ```typescript
59
+ include("/:locale", shopPatterns, { name: "shop" });
60
+ include("/outlet", shopPatterns, { name: "outlet" });
61
+
62
+ // in shopPatterns, on /en/... and on /outlet/...
63
+ ctx.reverse(".product", { slug: "b" }); // "/en/product/b" and "/outlet/product/b"
64
+ ctx.reverse(".product", { slug: "b", locale: "de" }); // "/de/product/b" (switch)
65
+ ```
66
+
67
+ `/:locale?` and `/:locale` both auto-fill. `{ locale: undefined }` drops an
68
+ optional locale (`/product/b`) and throws `Missing param "locale"` for a
69
+ required one. Don't rebuild this by hand: `useParams().accountId` in a template
70
+ string, `ctx.params` threaded through props, or a "current account" context
71
+ all duplicate what the mount already knows.
72
+
73
+ - A server layout stays held across navigations, its own mount param
74
+ (`:projectId`) changing included: partial rendering by design (`/layout`,
75
+ "Layout Revalidation"). Links it rendered keep the old params. Render
76
+ param-dependent nav links in a client component with `useReverse` (it
77
+ re-renders from `useParams()`), or give the layout a `revalidate()` that
78
+ compares the param.
79
+ - A layout, a parallel slot and the loaders declared on them resolve `.name`
80
+ in the module that declares them, whichever route below them matched: a
81
+ parent module's layout links within its own module on every page (see
82
+ "Local names" below).
35
83
 
36
84
  ```typescript
37
85
  import { urls } from "@rangojs/router";
@@ -47,7 +95,7 @@ export const shopPatterns = urls(({ path, layout }) => [
47
95
 
48
96
  ### Resolution rules
49
97
 
50
- - **`.name`** — local route, resolved within the current `include()` scope
98
+ - **`.name`** — local route, resolved within the `include()` scope of the module that defines the calling route, layout, parallel slot or loader
51
99
  - **`name`** — global route, from the named-routes definition
52
100
 
53
101
  The include site decides which global names exist:
@@ -87,6 +135,24 @@ Prefix a name with `.` to resolve it within the current `include()` scope. The r
87
135
  };
88
136
  ```
89
137
 
138
+ The scope is the module that **defines** the code calling `ctx.reverse`. A route handler resolves from its own module. A `layout()`, a `parallel()` slot and the loaders declared on them resolve from the module where they are declared, whichever route below them matched. So a parent module's layout gives the same link on every page:
139
+
140
+ ```typescript
141
+ // mounted at include("/:accountId", accountPatterns, { name: "account" })
142
+ export const accountPatterns = urls(({ path, layout, include }) => [
143
+ layout(
144
+ (ctx) => <AccountNav settings={ctx.reverse(".settings")} />, // "/acme/settings" on every page below
145
+ () => [
146
+ path("/settings", AccountSettings, { name: "settings" }),
147
+ // projectPatterns also has a "settings" route
148
+ include("/:projectId", projectPatterns, { name: "project" }),
149
+ ],
150
+ ),
151
+ ]);
152
+ // On /acme/web/issues, the account layout still links to "/acme/settings".
153
+ // The project's issues handler's ctx.reverse(".settings") gives "/acme/web/settings".
154
+ ```
155
+
90
156
  ### Auto-fill of mount params
91
157
 
92
158
  When routes are mounted via a parameterized `include()`, `ctx.reverse()` automatically fills mount params from `ctx.params`. Inner handlers don't need to pass params that are already known from the current URL match. Explicitly passed params override auto-filled values.
@@ -109,7 +175,7 @@ export const tenantPatterns = urls(({ path }) => [
109
175
  ]);
110
176
  ```
111
177
 
112
- Auto-fill uses `{ ...ctx.params, ...hrefParams }` — current request params are defaults, explicit params win. Params not needed by the target route are silently ignored. Because every current param is a default, drop an optional one explicitly with `undefined` or `""` — under `include("/:locale?", ...)` on `/en/...`, a bare `ctx.reverse("menu.index")` keeps `/en`, while `ctx.reverse("menu.index", { locale: undefined })` returns `/` (see `/i18n`). A missing required param throws `Missing param "slug" for route "…"`; an unknown name throws `Unknown route: "…"`.
178
+ Auto-fill uses `{ ...ctx.params, ...hrefParams }` — current request params are defaults, explicit params win. Params not needed by the target route are silently ignored. Because every current param is a default, drop an optional one explicitly with `undefined` or `""` — under `include("/:locale?", ...)` on `/en/...`, a bare `ctx.reverse("menu.index")` keeps `/en`, while `ctx.reverse("menu.index", { locale: undefined })` returns `/` (see `/i18n`). A missing required param throws `Missing param "slug" for route "…"` (in middleware and response routes too, with or without a params object); an unknown name throws `Unknown route: "…"`.
113
179
 
114
180
  ### Global names (unprefixed)
115
181
 
@@ -213,6 +279,48 @@ function Nav() {
213
279
 
214
280
  `useLoader()` requires the loader to be attached to an active route. If you need on-demand fetching instead, use `useFetchLoader()`.
215
281
 
282
+ A loader on a layout is held on navigation, like the layout. When its links follow a param of the layout's own mount (`:projectId`), give it a `revalidate()` that compares that param; links that never change can come from the layout's RSC as props:
283
+
284
+ ```tsx
285
+ // urls/project.tsx (server): build the links where the names resolve
286
+ export const ProjectNavLoader = createLoader((ctx) => ({
287
+ issues: ctx.reverse(".issues"), // /acme/web/issues
288
+ settings: ctx.reverse(".settings"), // /acme/web/settings
289
+ account: ctx.reverse("account.settings"), // /acme/settings, another module
290
+ }));
291
+
292
+ export const projectUrls = urls(({ layout, path, loader, revalidate }) => [
293
+ layout(
294
+ // From the RSC: fine for links that do not follow :projectId.
295
+ (ctx) => <ProjectShell accountHref={ctx.reverse("account.settings")} />,
296
+ () => [
297
+ // From a loader: held like the layout, re-run when the project changes.
298
+ loader(ProjectNavLoader, () => [
299
+ revalidate(({ currentParams, nextParams }) =>
300
+ currentParams.projectId !== nextParams.projectId ? true : undefined,
301
+ ),
302
+ ]),
303
+ path("/issues", Issues, { name: "issues" }),
304
+ path("/settings", Settings, { name: "settings" }),
305
+ ],
306
+ ),
307
+ ]);
308
+ ```
309
+
310
+ ```tsx
311
+ "use client";
312
+ export function ProjectNav() {
313
+ const { data: links } = useLoader(ProjectNavLoader);
314
+ return (
315
+ <nav>
316
+ <Link to={links.issues}>Issues</Link>
317
+ <Link to={links.settings}>Settings</Link>
318
+ <Link to={links.account}>Account</Link>
319
+ </nav>
320
+ );
321
+ }
322
+ ```
323
+
216
324
  3. Return from a server action:
217
325
 
218
326
  ```tsx
@@ -286,9 +394,9 @@ function ShopNav() {
286
394
  }
287
395
  ```
288
396
 
289
- Use `useHref()` for local navigation within a mounted module. Use the bare `href()` function for absolute paths outside the current mount.
397
+ Use `useHref()` for local navigation within a mounted module (`include("/shop", ...)` or `include("/tenant/:tenantId", ...)`). Use the bare `href()` function for absolute paths outside the current mount.
290
398
 
291
- `useHref()` concatenates the mount **pattern** as-is and does not substitute params, and its argument is not checked against your routes. Under a parameterized mount (`include("/tenant/:tenantId", ...)`) it would produce `/tenant/:tenantId/cart` — use `useReverse(routes)` there, which substitutes mount params from `useParams()`.
399
+ Under a parameterized mount (`include("/tenant/:tenantId", ...)`, `/:locale`, `/:locale?`), `useHref()` fills the mount's params from `useParams()`, the same way `useReverse` does: at `/tenant/acme/board`, `href("/cart")` returns `/tenant/acme/cart`. A mount param missing from the current match makes the returned function throw `Missing param "tenantId" for route "/tenant/:tenantId"`; it never returns a URL with a literal `:tenantId`. Relative `useRouter().push("cart")` / `replace` / `prefetch` fill the mount from the committed route params at call time; a missing one rejects the `push`/`replace` promise and skips the `prefetch`. Only the mount is filled: the path argument is appended verbatim and is not checked against your routes, so for a link to a named route with its own params prefer `useReverse(routes)`.
292
400
 
293
401
  ## Client: useMount()
294
402
 
@@ -306,7 +414,7 @@ function MountInfo() {
306
414
  }
307
415
  ```
308
416
 
309
- `useMount()` reads from `MountContext`, which is automatically set by `include()` in the segment tree. Nested includes return the nearest mount's full URL prefix (e.g. `/shop/admin`), and a parameterized include returns its pattern (`/tenant/:tenantId`), not the matched URL. `href(path, mount)` is the non-hook equivalent of `useHref()`.
417
+ `useMount()` reads from `MountContext`, which is automatically set by `include()` in the segment tree. Nested includes return the nearest mount's full URL prefix (e.g. `/shop/admin`), and a parameterized include returns its pattern (`/tenant/:tenantId`), not the matched URL. `href(path, mount)` concatenates the mount it is given as-is; it is the non-hook equivalent of `useHref()` only under a static mount (`useHref()` also fills mount params).
310
418
 
311
419
  ## Client: useReverse(routes)
312
420
 
@@ -331,6 +439,27 @@ export function BlogNav() {
331
439
  }
332
440
  ```
333
441
 
442
+ ### One hook per module, in a userland file
443
+
444
+ Wrap the map once instead of importing it in every component:
445
+
446
+ ```ts
447
+ // src/lib/links.ts
448
+ "use client";
449
+ import { useReverse } from "@rangojs/router/client";
450
+ import { routes as blogRoutes } from "../urls/blog.gen.js";
451
+ import { routes as accountRoutes } from "../urls/account.gen.js";
452
+
453
+ export const useBlogReverse = () => useReverse(blogRoutes);
454
+ export const useAccountReverse = () => useReverse(accountRoutes);
455
+ ```
456
+
457
+ Components call `useAccountReverse()`; a renamed or moved module changes one
458
+ file. The wrapper resolves like `useReverse`: against the nearest `include()`
459
+ mount, so call a module's hook from components under that module's own mount.
460
+ Inside a nested module it builds a wrong URL with no error (dc-t57); pass
461
+ cross-module links from the server there.
462
+
334
463
  ### How it resolves
335
464
 
336
465
  1. Strips an optional leading `.` and looks up the name in the imported `routes` map.
@@ -413,24 +542,29 @@ Both happen synchronously during `reverse()` — wrap calls in try/catch (or an
413
542
 
414
543
  ### The leading dot is optional
415
544
 
416
- `reverse("post")` and `reverse(".post")` resolve **identically** — the leading dot is cosmetic. The map you import IS the scope, so there is no separate global namespace to disambiguate and the dot carries no meaning; it exists only as a readability convention and for parity with `ctx.reverse(".name")` on the server. To link into a different module, import that module's `routes`:
545
+ `reverse("post")` and `reverse(".post")` resolve **identically** — the leading dot is cosmetic. The map you import IS the scope, so there is no separate global namespace to disambiguate and the dot carries no meaning; it exists only as a readability convention and for parity with `ctx.reverse(".name")` on the server. A module's `routes` map is right only inside that module's own mount: it resolves against the current mount, so another module's map gives a wrong URL with no error, and `useReverse(NamedRoutes)` is correct only at the root mount (under a mount it double-prefixes). There is no client API for links that cross modules under a nested mount (dc-t57). Build them on the server, where global names resolve fully, and pass them down:
417
546
 
418
547
  ```tsx
419
- import { routes as blogRoutes } from "../urls/blog.gen.js";
420
- import { routes as shopRoutes } from "../urls/shop.gen.js";
548
+ // server layout or handler
549
+ const links = {
550
+ blog: ctx.reverse("blog.index"),
551
+ cart: ctx.reverse("shop.cart"),
552
+ };
553
+ return <CrossNav links={links} />;
421
554
 
422
- function CrossNav() {
423
- const blog = useReverse(blogRoutes);
424
- const shop = useReverse(shopRoutes);
555
+ // client component
556
+ function CrossNav({ links }: { links: { blog: string; cart: string } }) {
425
557
  return (
426
558
  <nav>
427
- <Link to={blog("index")}>Blog</Link>
428
- <Link to={shop("cart")}>Cart</Link>
559
+ <Link to={links.blog}>Blog</Link>
560
+ <Link to={links.cart}>Cart</Link>
429
561
  </nav>
430
562
  );
431
563
  }
432
564
  ```
433
565
 
566
+ Loader data is the better carrier for chrome that follows a param: a layout handler is kept across param changes, while a layout loader re-runs when its `revalidate()` compares that param (a layout loader is held on navigation by default).
567
+
434
568
  ### Codegen
435
569
 
436
570
  Each `urls()` module gets a sibling `.gen.ts` with the local route names and patterns, produced by `rango generate`:
@@ -512,10 +646,10 @@ a link or a whole section from prefetching at all, use the `prefetch` prop or
512
646
  | Client component | `useReverse(routes)` | Local names from an imported `routes` map | Typed in-module URL generation without round-tripping the server |
513
647
  | Client component | (URL passed as prop / loader data / action return) | Named routes | Cross-module URLs or one-off names you don't want to import |
514
648
  | Client component | `href("/path")` | Absolute paths (static strings) | Static navigation where no named-route lookup is needed |
515
- | Client component | `useHref()` | Mount-prefixed paths | Local navigation inside `include()` |
649
+ | Client component | `useHref()` | Mount-prefixed paths | Local paths inside `include()`; fills `:param` mount segments |
516
650
  | Client component | `useMount()` | Raw mount path | Custom mount-aware logic |
517
651
 
518
- > `ctx.reverse()` is server-only. On the client, either generate URLs on the server and pass them in, or import the `routes` map and use `useReverse(routes)` for in-module names.
652
+ > `ctx.reverse()` is server-only. On the client, either generate URLs on the server and pass them in, or import the `routes` map and use `useReverse(routes)` for in-module names, inside that module's own mount.
519
653
 
520
654
  ## Complete example: mounted module
521
655
 
@@ -241,7 +241,7 @@ so response-shaping belongs in middleware or the handler).
241
241
  | `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
242
242
  | `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
243
243
  | `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
244
- | `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers). |
244
+ | `reverse` | `AutofillAwareReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers; params the matched route carries are optional). |
245
245
 
246
246
  ### Example
247
247
 
@@ -266,9 +266,10 @@ export const ProductLoader = createLoader(async (ctx) => {
266
266
  // Variables set by middleware (from Rango.Vars augmentation)
267
267
  const user = ctx.get("user");
268
268
 
269
- // Type-checked URLs for payloads. `.name` resolves within the current
270
- // include() scope; a bare `name` resolves globally. See /route and
271
- // /typesafety for scope rules and route-name autocomplete.
269
+ // Type-checked URLs for payloads. `.name` resolves within the include()
270
+ // scope of the module that declares this loader (a layout's loader in a
271
+ // parent module links within that module); a bare `name` resolves
272
+ // globally. See /links and /typesafety for scope rules and autocomplete.
272
273
  const detailUrl = ctx.reverse(".detail", { slug });
273
274
 
274
275
  return {
@@ -321,7 +322,7 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
321
322
 
322
323
  // Loader with revalidation control
323
324
  loader(RelatedProductsLoader, () => [
324
- revalidate(() => false), // Never revalidate
325
+ revalidate(false), // Never revalidate
325
326
  ]),
326
327
 
327
328
  // Loader that re-runs only after cart actions. Loaders re-run after every
@@ -355,6 +356,7 @@ chain continues:
355
356
  ```typescript
356
357
  // 1) Hard decision — short-circuits the chain, used as the final answer.
357
358
  revalidate(() => true);
359
+ revalidate(true); // shorthand for () => true; revalidate(false) for () => false
358
360
  revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
359
361
 
360
362
  // 2) Soft decision — updates the running suggestion for downstream
@@ -377,6 +379,14 @@ revalidate(() => null); // explicit defer
377
379
  If every revalidator on a segment defers, the segment default is used
378
380
  (`defaultShouldRevalidate`, see the table below).
379
381
 
382
+ `revalidate(true)` and `revalidate(false)` are shorthand for the constant hard
383
+ decisions `revalidate(() => true)` and `revalidate(() => false)`: `true`
384
+ re-runs the segment on every navigation and action, `false` keeps the client's
385
+ copy once it holds one. A defer has no shorthand: write the function. The
386
+ shorthand works everywhere `revalidate()` does, `clientUrls()` loaders
387
+ included. There the build checks the argument: a literal at its call site,
388
+ any other expression when the server mounts the group.
389
+
380
390
  The callback must be **synchronous**. A returned Promise is ignored (dev
381
391
  warning) and the running suggestion is kept, so move async work into a loader.
382
392
  A callback that throws is logged and treated as a defer; a thrown `Response`
@@ -944,6 +954,8 @@ function ProductPage() {
944
954
 
945
955
  A skeleton that has appeared stays on screen for at least about 300 ms before React replaces it with what it was waiting for: React does not replace a fallback sooner than 300 ms after the last fallback committed ([#1078](https://github.com/rangojs/rango/issues/1078)). On a cold click that mounts the `loading()` boundary, a loader that finishes sooner than that still shows its skeleton for about 300 ms, while a slower loader is not delayed. The router leaves this to React: an early skeleton is preferred to holding the click, and a fully prefetched click commits complete and shows none. A nested `loading()` whose data lands after the outer one has been revealed shows its own fallback, and the 300 ms starts again. The loaders feature page on the docs site has the measured figures. Under `transition()` a related cost applies; see [view-transitions](../view-transitions/SKILL.md).
946
956
 
957
+ A loader that throws (anything but `notFound()` or `redirect()`) throws at its `useLoader` read on every client lane: a plain or fully prefetched click, Back/Forward restored from the history cache, and the rerun after a server action. A prefetch in which a loader threw is never used, in memory or from the browser's HTTP cache: the click asks the server again, so a loader that has recovered shows its data. The nearest boundary to the read catches it (your React error boundary around the read, else the route's `errorBoundary()`, else the router's root one), and the layouts above keep their instance. The background refresh after Back/Forward onto a stale page is the exception: a loader that fails there leaves the page on screen and logs `[Browser] Background revalidation failed`. Not covered: an `intercept()`'s loaders, and a parallel slot's loaders under its own `loading()`, which are decoded above the slot's content.
958
+
947
959
  ## Loader Authority: notFound() and redirect()
948
960
 
949
961
  A loader may **throw** `notFound()` and `redirect()` — data-dependent
@@ -955,7 +967,7 @@ import { createLoader, notFound, redirect } from "@rangojs/router";
955
967
 
956
968
  export const ProductLoader = createLoader(async (ctx) => {
957
969
  const moved = LEGACY_SLUGS[ctx.params.slug];
958
- if (moved) throw redirect(`/shop/product/${moved}`);
970
+ if (moved) throw redirect(ctx.reverse("shop.product", { slug: moved }));
959
971
 
960
972
  // Existence check BEFORE the expensive fetch: a near-instant rejection
961
973
  // usually wins the race to first flush (see the semantics below).
@@ -1012,6 +1024,7 @@ Delivery is async **by the race model**: pushes that settle before the handler
1012
1024
  barrier ride the SSR handle snapshot (in the SSR'd document — `<Html.Meta />`,
1013
1025
  `useHandle` reads); later pushes stream to the client and apply post-hydration
1014
1026
  on document loads (`metadata.handlesLate`) or progressively on navigations.
1027
+ Handle values land on their own channel and never hold the page, so an effect that needs a late push keys on the handle value, not the URL (see [hooks](../hooks/handle-and-actions.md#when-handle-values-change-by-design)).
1015
1028
  A push before your slow fetch usually beats the barrier; a push derived from
1016
1029
  the fetched data usually does not. When it MUST be in the document, use
1017
1030
  `ssr: false` below. A loader with its own `cache()` keeps its pushes on a hit —
@@ -1021,7 +1034,9 @@ there every push a loader makes on that request applies post-hydration (see
1021
1034
  `/ppr`, "Handles on a shell HIT"). A `useHandle` reader inside this loader's
1022
1035
  `loading()` or `<Suspense>` hydrates with the data its HTML was rendered
1023
1036
  from and shows a later push right after, so a push after an `await` causes
1024
- no hydration mismatch there.
1037
+ no hydration mismatch there. A loader body may push after an `await`; a server
1038
+ component that does throws `LateHandlePushError`, so it resolves a slot its
1039
+ handler reserved with `.defer()` instead (`/breadcrumbs`, "Deferred content").
1025
1040
 
1026
1041
  Under `cache()`, loader pushes are not stored with the cached segments: the
1027
1042
  loader re-runs on every hit and its live push is the only copy. (`ppr` keeps
@@ -1279,9 +1294,16 @@ The limits:
1279
1294
  data with the URL, and one fill runs the flagged loaders (see
1280
1295
  `/client-urls` → "`prefetch: false`"). A flagged `loading()` defers nothing
1281
1296
  on a group route, from the group or from a server layout above it.
1282
- - **No effect inside `intercept()`.** Its loaders accept the flag and ignore
1283
- it, in `urls()` and in `clientUrls()`: a prefetch that resolves an
1284
- intercept defers nothing.
1297
+ - **Inside `intercept()`.** In `urls()` and in `clientUrls()`, a prefetch of
1298
+ the intercept's target from its source page defers the flagged loader: the
1299
+ click opens the modal at once, with its `loading()`, and one fill, sent
1300
+ from the source, runs the loader. The page under the modal stays. With no
1301
+ `loading()` the slot is empty until the fill answers, as on a plain click
1302
+ until its response. The modal's loaders resolve together: it shows its
1303
+ `loading()` until every one has arrived. A flagged `loading()` inside
1304
+ `intercept()` defers nothing. Each intercept loader also runs its own
1305
+ `revalidate()`, so an action from the modal re-runs only those that say so
1306
+ (see `/intercept`).
1285
1307
  - **`ppr` and `ssr: false`.** On a `ppr` route an `ssr: false` loader is the
1286
1308
  bake lane and is served from the shell, so `prefetch: false` is ignored for
1287
1309
  it and development logs a warning. Under `cache()` and `Prerender` the flag