@rangojs/router 0.22.1 → 0.24.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 (259) hide show
  1. package/dist/bin/rango.js +10 -4
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  7. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  8. package/dist/types/browser/partial-update.d.ts +39 -8
  9. package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
  10. package/dist/types/browser/react/context.d.ts +3 -2
  11. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  12. package/dist/types/browser/response-adapter.d.ts +7 -0
  13. package/dist/types/browser/types.d.ts +24 -10
  14. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  15. package/dist/types/build/generate-route-types.d.ts +1 -1
  16. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  17. package/dist/types/build/route-types/scan-filter.d.ts +22 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  19. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  20. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  21. package/dist/types/cache/document-cache.d.ts +2 -10
  22. package/dist/types/client-urls/client-urls.d.ts +1 -1
  23. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  24. package/dist/types/client-urls/navigation.d.ts +0 -37
  25. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  26. package/dist/types/client-urls/server-projection.d.ts +29 -1
  27. package/dist/types/client-urls/types.d.ts +34 -69
  28. package/dist/types/client.d.ts +0 -1
  29. package/dist/types/context-var.d.ts +12 -0
  30. package/dist/types/errors.d.ts +15 -0
  31. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  32. package/dist/types/handles/pushed-by.d.ts +31 -0
  33. package/dist/types/index.d.ts +4 -0
  34. package/dist/types/index.rsc.d.ts +2 -0
  35. package/dist/types/loader-store.d.ts +8 -0
  36. package/dist/types/prefetch-markers.d.ts +11 -0
  37. package/dist/types/response-utils.d.ts +7 -0
  38. package/dist/types/reverse.d.ts +8 -8
  39. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  40. package/dist/types/route-definition/rewrite.d.ts +56 -0
  41. package/dist/types/route-map-builder.d.ts +7 -2
  42. package/dist/types/router/basename.d.ts +7 -0
  43. package/dist/types/router/handler-context.d.ts +1 -5
  44. package/dist/types/router/internal-params.d.ts +8 -0
  45. package/dist/types/router/is-action.d.ts +2 -1
  46. package/dist/types/router/middleware-types.d.ts +15 -0
  47. package/dist/types/router/revalidation.d.ts +1 -2
  48. package/dist/types/router/route-trie-builder.d.ts +10 -0
  49. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  50. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  51. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
  52. package/dist/types/rsc/full-payload.d.ts +11 -0
  53. package/dist/types/rsc/handler.d.ts +33 -0
  54. package/dist/types/rsc/helpers.d.ts +8 -0
  55. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  56. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  57. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  58. package/dist/types/server/context.d.ts +21 -1
  59. package/dist/types/server/handle-store.d.ts +9 -1
  60. package/dist/types/server/request-context.d.ts +52 -2
  61. package/dist/types/server/rewrite-trail.d.ts +49 -0
  62. package/dist/types/server/slot-accessors.d.ts +29 -0
  63. package/dist/types/server.d.ts +1 -0
  64. package/dist/types/testing/flight.entry.d.ts +1 -1
  65. package/dist/types/testing/index.d.ts +4 -4
  66. package/dist/types/testing/internal/action-id.d.ts +10 -1
  67. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  68. package/dist/types/testing/internal/test-action.d.ts +8 -0
  69. package/dist/types/testing/render-route.d.ts +8 -0
  70. package/dist/types/testing/run-loader.d.ts +2 -1
  71. package/dist/types/testing/run-middleware.d.ts +20 -0
  72. package/dist/types/testing/run-revalidate.d.ts +74 -0
  73. package/dist/types/testing/run-transition-when.d.ts +2 -3
  74. package/dist/types/testing/serve-shell-request.d.ts +40 -1
  75. package/dist/types/testing/vitest.d.ts +11 -0
  76. package/dist/types/thenable-status.d.ts +6 -0
  77. package/dist/types/types/boundaries.d.ts +9 -0
  78. package/dist/types/types/handler-context.d.ts +14 -0
  79. package/dist/types/types/loader-types.d.ts +30 -0
  80. package/dist/types/types/request-scope.d.ts +7 -0
  81. package/dist/types/types/segments.d.ts +21 -4
  82. package/dist/types/urls/path-helper-types.d.ts +25 -4
  83. package/dist/types/urls/type-extraction.d.ts +6 -2
  84. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  85. package/dist/types/vite/discovery/state.d.ts +14 -4
  86. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  87. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  88. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  89. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  90. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  91. package/dist/types/vite/router-discovery.d.ts +10 -0
  92. package/dist/vite/index.js +974 -379
  93. package/package.json +1 -1
  94. package/skills/caching/SKILL.md +54 -5
  95. package/skills/catalog.json +3 -3
  96. package/skills/client-urls/SKILL.md +197 -24
  97. package/skills/cloudflare/SKILL.md +14 -9
  98. package/skills/hooks/SKILL.md +16 -3
  99. package/skills/hooks/data.md +3 -1
  100. package/skills/links/SKILL.md +24 -0
  101. package/skills/loader/SKILL.md +199 -2
  102. package/skills/middleware/SKILL.md +183 -2
  103. package/skills/migrate-nextjs/SKILL.md +2 -2
  104. package/skills/migrate-react-router/SKILL.md +10 -7
  105. package/skills/migrate-react-router/data-and-actions.md +1 -1
  106. package/skills/ppr/SKILL.md +22 -0
  107. package/skills/prerender/SKILL.md +22 -8
  108. package/skills/rango/SKILL.md +13 -7
  109. package/skills/route/SKILL.md +139 -0
  110. package/skills/server-actions/SKILL.md +27 -0
  111. package/skills/testing/SKILL.md +3 -2
  112. package/skills/testing/client-components.md +2 -1
  113. package/skills/testing/middleware.md +31 -10
  114. package/skills/testing/predicates.md +67 -32
  115. package/skills/testing/response-routes.md +1 -0
  116. package/skills/testing/serve-shell-request.md +107 -19
  117. package/skills/testing/setup.md +2 -2
  118. package/skills/typesafety/route-types.md +2 -2
  119. package/skills/use-cache/SKILL.md +4 -2
  120. package/skills/view-transitions/SKILL.md +26 -0
  121. package/src/__augment-tests__/augmented.check.ts +20 -0
  122. package/src/browser/defer.ts +82 -0
  123. package/src/browser/document-revealed.ts +52 -0
  124. package/src/browser/event-controller.ts +46 -32
  125. package/src/browser/merge-segment-loaders.ts +31 -18
  126. package/src/browser/navigation-bridge.ts +82 -16
  127. package/src/browser/navigation-client.ts +48 -36
  128. package/src/browser/navigation-store.ts +13 -0
  129. package/src/browser/navigation-transaction.ts +25 -8
  130. package/src/browser/partial-update.ts +554 -146
  131. package/src/browser/prefetch/fetch.ts +2 -1
  132. package/src/browser/react/Link.tsx +7 -9
  133. package/src/browser/react/NavigationProvider.tsx +88 -49
  134. package/src/browser/react/context.ts +3 -2
  135. package/src/browser/react/filter-segment-order.ts +13 -2
  136. package/src/browser/react/use-router.ts +2 -4
  137. package/src/browser/response-adapter.ts +20 -0
  138. package/src/browser/rsc-router.tsx +17 -45
  139. package/src/browser/server-action-bridge.ts +25 -63
  140. package/src/browser/types.ts +24 -10
  141. package/src/browser/validate-redirect-origin.ts +16 -0
  142. package/src/build/generate-route-types.ts +1 -0
  143. package/src/build/route-types/router-processing.ts +3 -6
  144. package/src/build/route-types/scan-filter.ts +45 -11
  145. package/src/cache/cf/cf-cache-store.ts +59 -21
  146. package/src/cache/cf/cf-cache-types.ts +24 -2
  147. package/src/cache/document-cache-policy.ts +131 -0
  148. package/src/cache/document-cache.ts +21 -107
  149. package/src/client-urls/client-urls.ts +22 -6
  150. package/src/client-urls/hoisted-revalidate.ts +25 -0
  151. package/src/client-urls/navigation.ts +0 -96
  152. package/src/client-urls/revalidate-registry.ts +30 -0
  153. package/src/client-urls/server-projection.ts +151 -65
  154. package/src/client-urls/types.ts +39 -71
  155. package/src/client.tsx +9 -5
  156. package/src/context-var.ts +32 -2
  157. package/src/errors.ts +34 -0
  158. package/src/handles/deferred-resolution.ts +28 -5
  159. package/src/handles/pushed-by.ts +148 -0
  160. package/src/index.rsc.ts +8 -0
  161. package/src/index.ts +7 -0
  162. package/src/loader-store.ts +18 -7
  163. package/src/prefetch-markers.ts +13 -0
  164. package/src/response-utils.ts +19 -0
  165. package/src/reverse.ts +17 -14
  166. package/src/route-content-wrapper.tsx +18 -4
  167. package/src/route-definition/dsl-helpers.ts +27 -4
  168. package/src/route-definition/helpers-types.ts +27 -1
  169. package/src/route-definition/redirect.ts +2 -15
  170. package/src/route-definition/rewrite.ts +146 -0
  171. package/src/route-map-builder.ts +14 -11
  172. package/src/router/basename.ts +20 -0
  173. package/src/router/handler-context.ts +3 -13
  174. package/src/router/intercept-resolution.ts +35 -16
  175. package/src/router/internal-params.ts +16 -0
  176. package/src/router/is-action.ts +2 -1
  177. package/src/router/loader-resolution.ts +85 -4
  178. package/src/router/match-api.ts +36 -1
  179. package/src/router/match-middleware/background-revalidation.ts +16 -4
  180. package/src/router/match-middleware/cache-lookup.ts +35 -7
  181. package/src/router/match-middleware/cache-store.ts +15 -1
  182. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  183. package/src/router/match-result.ts +14 -2
  184. package/src/router/middleware-types.ts +16 -0
  185. package/src/router/middleware.ts +127 -49
  186. package/src/router/navigation-snapshot.ts +2 -1
  187. package/src/router/revalidation.ts +1 -1
  188. package/src/router/route-trie-builder.ts +28 -1
  189. package/src/router/segment-resolution/fresh.ts +22 -18
  190. package/src/router/segment-resolution/loader-cache.ts +2 -1
  191. package/src/router/segment-resolution/loader-mask.ts +5 -3
  192. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  193. package/src/router/segment-resolution/mask-nested.ts +8 -4
  194. package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
  195. package/src/router/segment-resolution/revalidation.ts +215 -82
  196. package/src/rsc/full-payload.ts +37 -5
  197. package/src/rsc/handler.ts +82 -55
  198. package/src/rsc/helpers.ts +24 -0
  199. package/src/rsc/loader-fetch.ts +79 -24
  200. package/src/rsc/response-cache-serve.ts +18 -2
  201. package/src/rsc/response-route-handler.ts +4 -7
  202. package/src/rsc/rewrite-passes.ts +203 -0
  203. package/src/rsc/rsc-rendering.ts +54 -9
  204. package/src/rsc/shell-capture.ts +2 -1
  205. package/src/rsc/ssr-setup.ts +11 -5
  206. package/src/segment-loader-promise.ts +23 -20
  207. package/src/segment-system.tsx +60 -26
  208. package/src/server/context.ts +39 -5
  209. package/src/server/cookie-parse.ts +36 -12
  210. package/src/server/cookie-store.ts +7 -3
  211. package/src/server/handle-store.ts +152 -25
  212. package/src/server/request-context.ts +175 -64
  213. package/src/server/request-headers.ts +4 -14
  214. package/src/server/rewrite-trail.ts +190 -0
  215. package/src/server/slot-accessors.ts +57 -0
  216. package/src/server.ts +4 -0
  217. package/src/ssr/index.tsx +118 -16
  218. package/src/testing/action-form.ts +2 -11
  219. package/src/testing/dispatch.ts +138 -67
  220. package/src/testing/e2e/page-helpers.ts +3 -1
  221. package/src/testing/flight.entry.ts +1 -0
  222. package/src/testing/index.ts +7 -3
  223. package/src/testing/internal/action-id.ts +16 -1
  224. package/src/testing/internal/client-urls-projection.ts +70 -0
  225. package/src/testing/internal/test-action.ts +25 -0
  226. package/src/testing/render-route.tsx +16 -3
  227. package/src/testing/run-loader.ts +4 -1
  228. package/src/testing/run-middleware.ts +56 -11
  229. package/src/testing/run-revalidate.ts +142 -0
  230. package/src/testing/run-transition-when.ts +7 -8
  231. package/src/testing/serve-shell-request.ts +131 -8
  232. package/src/testing/vitest.ts +67 -1
  233. package/src/thenable-status.ts +11 -0
  234. package/src/types/boundaries.ts +12 -0
  235. package/src/types/handler-context.ts +14 -0
  236. package/src/types/loader-types.ts +30 -0
  237. package/src/types/request-scope.ts +8 -0
  238. package/src/types/segments.ts +26 -4
  239. package/src/urls/path-helper-types.ts +27 -5
  240. package/src/urls/type-extraction.ts +30 -10
  241. package/src/use-loader.tsx +18 -13
  242. package/src/vite/discovery/client-urls-projection.ts +20 -21
  243. package/src/vite/discovery/discover-routers.ts +94 -26
  244. package/src/vite/discovery/state.ts +28 -4
  245. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  246. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  247. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  248. package/src/vite/plugins/hoist/literal.ts +417 -0
  249. package/src/vite/plugins/hoist/scope.ts +370 -0
  250. package/src/vite/plugins/hoist/source.ts +84 -0
  251. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  252. package/src/vite/rango.ts +2 -0
  253. package/src/vite/router-discovery.ts +116 -53
  254. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  255. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  256. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  257. package/src/client-urls/revalidate-chain.ts +0 -83
  258. package/src/client-urls/revalidation-protocol.ts +0 -56
  259. package/src/testing/run-client-revalidate.ts +0 -92
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.22.1",
3
+ "version": "0.24.0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -453,6 +453,28 @@ or a non-cacheable variable fails on a miss unless the loader `cache()` has a
453
453
  a `keyGenerator`. Either one switches the check off, so it must itself include
454
454
  the value (`/loader` → "Cache Key").
455
455
 
456
+ ### Loaders and `prefetch: false`
457
+
458
+ A loader registered with `{ prefetch: false }` is skipped in a `<Link>`
459
+ prefetch, and its own `cache()` is not read either, even when it would hit:
460
+ the fill request that the click sends afterwards reads the loader cache and
461
+ runs the loader on a miss. A route-level `cache()` boundary is unaffected: its
462
+ stored handler output is served and written as before, hit or miss, and
463
+ `loading(fallback, { prefetch: false })` under it defers only the loaders
464
+ behind the fallback, never the cached handler. A `cache()` route below a
465
+ layout with a flagged `loading()` keeps using its record whenever the browser
466
+ already holds that layout; only a prefetch that defers the layout itself (it
467
+ is new to the page) leaves the record unread and unwritten. A response that
468
+ carries deferred work is never reused: the document cache refuses it, and it
469
+ is sent `cache-control: private, no-cache` with `X-Rango-Prefetch` added to
470
+ `Vary`, whatever `Cache-Control` the route set, so no CDN keeps it and it
471
+ never answers a navigation. A browser may keep a copy and answer the same
472
+ prefetch from it while it reloads the page for back/forward; the click
473
+ completes it like any other. A fill is never answered from a stored body.
474
+ Every body the document cache does hold is complete, so one entry answers a
475
+ prefetch and a navigation, as without the flag. See `/loader` →
476
+ "`prefetch: false`".
477
+
456
478
  ## Global Cache Configuration
457
479
 
458
480
  Configure the app-level store on the router. `cache()` boundaries, cached
@@ -621,11 +643,26 @@ const router = createRouter<AppBindings>({
621
643
 
622
644
  **How the two layers work:**
623
645
 
624
- | Scenario | L1 (Cache API) | L2 (KV) | Result |
625
- | ------------ | -------------- | ------- | ----------------------------- |
626
- | Hot request | HIT | — | Serve from L1 (fast) |
627
- | Cold colo | MISS | HIT | Serve from KV, promote to L1 |
628
- | First render | MISS | MISS | Render, write to both L1 + KV |
646
+ | Scenario | L1 (Cache API) | L2 (KV) | Result |
647
+ | ------------------------------------ | ---------------- | --------------------------- | ----------------------------- |
648
+ | Hot request | HIT | — | Serve from L1 (fast) |
649
+ | Cold colo | MISS | HIT | Serve from KV, promote to L1 |
650
+ | First render | MISS | MISS | Render, write to both L1 + KV |
651
+ | After `updateTag`, `kvFallThrough` | copy invalidated | HIT, written after the call | Serve from KV, promote to L1 |
652
+ | After `updateTag`, `kvFallThrough` | copy invalidated | MISS, or the old entry | Render, write to both L1 + KV |
653
+ | After `updateTag`, the default (off) | copy invalidated | not read | Render, write to both L1 + KV |
654
+
655
+ By default a copy a tag invalidation rejected is a miss and the colo renders.
656
+ `new CFCacheStore({ ctx, kv, kvFallThrough: true })` makes it read KV like a
657
+ cold colo, so the colo serves the entry another colo already rebuilt (a
658
+ visitor's render there, or a `router.prerender()` warm) instead of rebuilding
659
+ it. The KV entry passes the same tag check: one written before the invalidation
660
+ is never served. The cost is one KV get per rejected copy per colo after each
661
+ invalidation (a `ppr` document reads a shell, a record and each `"use cache"`
662
+ result, every copy rejected and the newer entry in KV: 150 ms added, measured
663
+ in #1069 at 50 ms per KV read locally, not on a deployed worker).
664
+ Turn it on for several locations where a render costs more than a KV read;
665
+ leave it off otherwise. It needs `kv`.
629
666
 
630
667
  KV entries require `expirationTtl >= 60s`. Short-lived data entries (< 60s total
631
668
  TTL) are only cached in L1. A PPR shell is always written to KV, a short one
@@ -844,10 +881,22 @@ another, request-scoped APIs are guarded inside a cache scope:
844
881
  | Response writes: `ctx.headers.set()`, `setCookie()`, `setStatus()`, `onResponse()` | **throws** — response side effects lost on a hit |
845
882
  | `ctx.get(var)` where the var is `{ cache: false }` | **throws** on read |
846
883
  | `ctx.theme`, `getRequestContext().theme` | **throws** — the theme is the visitor's cookie |
884
+ | `ctx.rewrittenFrom` (any ctx, `getRequestContext().rewrittenFrom`) | **throws** — the visitor's URL of a rewritten request |
847
885
  | `ctx.set(var, value)` for a cacheable var | allowed (children are cached too) |
848
886
  | Any of the above **inside a registered loader** (`loader(...)`) | **allowed** — loaders always run fresh |
849
887
  | Any read in the boundary's own `key()`, `keyGenerator`, `condition()` or `tags()` | **allowed** — it picks or labels the entry, never rendered |
850
888
 
889
+ `ctx.rewrittenFrom` (the visitor's URL when middleware rewrote the request,
890
+ `/middleware` → "Rewrite") throws on every read inside a `cache()` boundary
891
+ or a `"use cache"` function, on a request nothing rewrote too, like
892
+ `cookies()`: the entry would hold whichever URL the first caller came from,
893
+ and a read that worked only on direct visits would break the first time a
894
+ rewritten request reached it. A PPR shell capture and a render the document
895
+ cache stores read it as `undefined` instead of throwing: those renders are
896
+ the target page as a direct visit renders it, and the rewritten visitor's
897
+ own render (outside them) still sees its URL. A response route under
898
+ `cache()` whose handler read it is not stored.
899
+
851
900
  A loader body invoked from a handler with `await ctx.use(Loader)` may read
852
901
  `cookies()`/`headers()`/`ctx.request.headers`, but its response writes still throw: on a hit the
853
902
  handler is skipped, so that loader never runs.
@@ -35,7 +35,7 @@
35
35
  },
36
36
  {
37
37
  "name": "client-urls",
38
- "description": "Define client-component route groups with clientUrls() in a \"use client\" module — no handlers, useLoader at read sites, client-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the ssr:false SSR-completeness opt-in. Use when a route group's pages are client components, when building high-navigation-speed UIs (dashboards, admin panels, settings) where transitions must feel instant, when moving data reads from handler ctx.use() to useLoader at consumption sites, or when asking how routes defined in a \"use client\" file work.",
38
+ "description": "Define client-component route groups with clientUrls() in a \"use client\" module — no handlers, useLoader at read sites, server-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the ssr:false SSR-completeness opt-in. Use when a route group's pages are client components, when building high-navigation-speed UIs (dashboards, admin panels, settings) where transitions must feel instant, when moving data reads from handler ctx.use() to useLoader at consumption sites, or when asking how routes defined in a \"use client\" file work.",
39
39
  "argumentHint": "[setup]",
40
40
  "path": "skills/client-urls/SKILL.md"
41
41
  },
@@ -137,13 +137,13 @@
137
137
  },
138
138
  {
139
139
  "name": "loader",
140
- "description": "Define data loaders with createLoader and register them on routes with loader(). Use when a route needs per-request data that stays fresh and streams while the page renders, a client component reads server data with useLoader, a loader should re-run after specific actions (revalidate), be cached (cache), be callable from the client (fetchable loaders), throw notFound()/redirect(), write page meta/breadcrumbs handles, or must be settled in the SSR'd document (ssr:false).",
140
+ "description": "Define data loaders with createLoader and register them on routes with loader(). Use when a route needs per-request data that stays fresh and streams while the page renders, a client component reads server data with useLoader, a loader should re-run after specific actions (revalidate), be cached (cache), be callable from the client (fetchable loaders), throw notFound()/redirect(), write page meta/breadcrumbs handles, must be settled in the SSR'd document (ssr:false), or must stay out of link prefetches (prefetch:false).",
141
141
  "argumentHint": "[loader]",
142
142
  "path": "skills/loader/SKILL.md"
143
143
  },
144
144
  {
145
145
  "name": "middleware",
146
- "description": "Define global (router.use) and route (middleware()) middleware in @rangojs/router — auth gates, logging, response headers, context variables, redirects. Use when gating routes behind auth checks, logging requests, setting headers or ctx variables before a handler runs, or deciding whether middleware wraps server actions.",
146
+ "description": "Define global (router.use) and route (middleware()) middleware in @rangojs/router — auth gates, logging, response headers, context variables, redirects, rewrites (throw rewrite() renders another route at the same URL). Use when gating routes behind auth checks, logging requests, setting headers or ctx variables before a handler runs, showing a maintenance page at the visitor's URL, or deciding whether middleware wraps server actions.",
147
147
  "argumentHint": "[middleware-name]",
148
148
  "path": "skills/middleware/SKILL.md"
149
149
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: client-urls
3
- description: Define client-component route groups with clientUrls() in a "use client" module — no handlers, useLoader at read sites, client-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the ssr:false SSR-completeness opt-in. Use when a route group's pages are client components, when building high-navigation-speed UIs (dashboards, admin panels, settings) where transitions must feel instant, when moving data reads from handler ctx.use() to useLoader at consumption sites, or when asking how routes defined in a "use client" file work.
3
+ description: Define client-component route groups with clientUrls() in a "use client" module — no handlers, useLoader at read sites, server-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the ssr:false SSR-completeness opt-in. Use when a route group's pages are client components, when building high-navigation-speed UIs (dashboards, admin panels, settings) where transitions must feel instant, when moving data reads from handler ctx.use() to useLoader at consumption sites, or when asking how routes defined in a "use client" file work.
4
4
  argument-hint: "[setup]"
5
5
  ---
6
6
 
@@ -10,7 +10,8 @@ argument-hint: "[setup]"
10
10
  components are client components, and there are **no handlers at all** — every
11
11
  data read is `useLoader` at the consumption site, waiting is scoped by the
12
12
  inline `<Suspense>` above each read, and per-loader `revalidate()` predicates
13
- run in the browser. The server shop pattern — `await ctx.use(ProductLoader)` in
13
+ run on the server (the build moves them out of the `"use client"` module).
14
+ The server shop pattern — `await ctx.use(ProductLoader)` in
14
15
  a handler — is pipeline-blocking by contract: no pending UI can ever cover that
15
16
  latency. `clientUrls()` makes that shape unrepresentable.
16
17
 
@@ -20,9 +21,9 @@ layout, mostly tab/param/filter switches. Three things compound: the
20
21
  definition also matches in the browser, so a soft navigation renders the
21
22
  destination component immediately (loader reads suspend into `loading()` or
22
23
  an inline `<Suspense>`; `useOutlet().pending` flips for chrome) with no server
23
- round-trip to start; browser-run `revalidate()` predicates HOLD data across
24
- navigations that don't invalidate it (a tab switch re-runs nothing — only the
25
- decision crosses the wire); and any read that does refresh streams behind its
24
+ round-trip to start; per-loader `revalidate()` predicates, run on the server
25
+ with the same arguments as in `urls()`, HOLD data across navigations that
26
+ don't invalidate it (a tab switch re-runs nothing); and any read that does refresh streams behind its
26
27
  own inline boundary instead of gating the commit. Add per-Link prefetch
27
28
  (`/links`) and a warmed click lands the complete page instantly.
28
29
 
@@ -131,9 +132,9 @@ flow into the generated route map, so `href()`, `ctx.reverse()` and
131
132
  | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
132
133
  | `path()` | Options are `name`, `search`, `trailingSlash`, `ppr` (shell caching — see `/ppr` → The loader lane rule); no response variants. Use callback may contain `loader()`, `loading()`, `transition()` |
133
134
  | `layout()` | `layout(Component, () => [...])` — children are required and must contain at least one `path()`; may also hold `loader()`, `loading()`, `intercept()`, nested `layout()` |
134
- | `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: false }, use?)` — see below |
135
+ | `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: false, prefetch: false }, use?)` — see below |
135
136
  | `loading()` | Route-level boundary around the optimistic render; inline `<Suspense>` at read sites keeps the destination's chrome visible while only the reads wait |
136
- | `revalidate()` | Valid **inside a loader() use callback only**; runs in the browser |
137
+ | `revalidate()` | Valid **inside a loader() use callback only**; runs on the server (the build hoists it out of the `"use client"` module) |
137
138
  | `transition()` | Inside a `path()` use callback only (at most one). ViewTransition config plus a browser-run `when` predicate (see `/view-transitions`); same-route navs in a group already hold without it |
138
139
  | `intercept()` | Dot-local named target in the SAME definition; use may contain `loader()`/`loading()` |
139
140
 
@@ -170,7 +171,8 @@ DIFFERENT schedules:
170
171
  - **Middleware runs on EVERY canonical request**, not just the first
171
172
  encounter. Every navigation inside the group — including a tab switch whose
172
173
  loaders all hold — still sends the canonical partial request (it commits
173
- URL/history and carries the revalidation decisions), and middleware wraps
174
+ URL/history, and the server runs each loader's `revalidate()` while it
175
+ handles it), and middleware wraps
174
176
  each one: `requireAdmin` re-authorizes every navigation, and its
175
177
  `ctx.set()` variables are fresh for the group's loaders on every pass. The
176
178
  instant feel comes from held data and optimistic presentation, not from
@@ -187,22 +189,62 @@ All three pinned by dev + production e2e in the router repository (a
187
189
  middleware-stamped header per request, a layout run-count stamp held across
188
190
  navigations).
189
191
 
190
- ## Revalidation runs in the browser
191
-
192
- A server-tree `revalidate()` predicate executes on the server. A `clientUrls()`
193
- predicate is client-module code, so it executes **in the browser** with
194
- client-computable args — `currentUrl`, `nextUrl`, `currentParams`,
195
- `nextParams`, `defaultShouldRevalidate`, `stale`, `isAction`, `actionId` — and
196
- only its _decision_ crosses the wire with the navigation request. Requests that
197
- carry no decisions (no-JS, progressive enhancement, prefetch, document loads)
198
- follow the locked server defaults.
199
-
200
- `isAction` is the same callable matcher as the server predicate, not a
201
- boolean. Action identity is `actionId`, whose FORM differs per environment:
202
- file-path `$id` (`path#export`) in the RSC env, hashed `$$id` in the
203
- browser — which is why the matcher (resolving an imported reference's
204
- `$id ?? $$id`) is the supported surface and a file-path substring on
205
- `actionId` is not:
192
+ ## Revalidation runs on the server
193
+
194
+ A `clientUrls()` loader's `revalidate()` runs only on the server, with exactly
195
+ the arguments and type of a `urls()` loader's (`ShouldRevalidateFn` from
196
+ `@rangojs/router`): `currentParams`, `currentUrl`, `nextParams`, `nextUrl`,
197
+ `defaultShouldRevalidate`, `context` (the handler context, so
198
+ `context.get(var)` works), `segmentType`, `actionId`, `isAction(...refs)`,
199
+ `actionUrl`, `actionResult`, `formData`, `method`,
200
+ `routeName`/`fromRouteName`/`toRouteName`, and `stale`. Nothing runs in the
201
+ browser before the request goes out; there is no client-computed decision and
202
+ no hybrid.
203
+
204
+ You still write the predicate in the `"use client"` module. The rango Vite
205
+ plugin hoists each `revalidate()` argument into a server-only module, leaves a
206
+ key in the browser and SSR copies, and registers the predicate when the
207
+ group's module loads on the server (a lazy `include()` stays lazy). The cost
208
+ is a rule: **the argument may reference imports and globals only.** Anything
209
+ else is a build error naming `file:line:column`, starting
210
+ `[rango] clientUrls() revalidate() at ...: revalidate() runs on the server; ...`.
211
+
212
+ - Accepted: an inline arrow or function expression, an imported predicate
213
+ (`revalidate(isCartAction)`), a namespace member (`P.cart`), a call of an
214
+ imported factory (`makePredicate("cart")`), and globals such as `URL`,
215
+ `Boolean` and `import.meta.env`.
216
+ - A binding declared in the same `"use client"` module is rejected: move the
217
+ predicate to its own module without `"use client"` and import it, or write it
218
+ inline. A binding of an enclosing function is rejected: read it from the
219
+ predicate's argument. So are `this`, `arguments`, `super`, `new.target`,
220
+ `import()` and `import.meta` other than `import.meta.env`.
221
+ - A browser-only global (`window`, `document`, `navigator`, `localStorage`,
222
+ `sessionStorage`, `location`, `history`) is rejected: put the input on the
223
+ request (a cookie or a search param) and read it from the argument, or
224
+ refresh with `useRefreshLoaders`.
225
+ - An `async` or generator function, a spread, a second argument, no argument,
226
+ or a non-function value (object, array or string literal) is rejected.
227
+ - An import of `client-only`, or of a module whose directive prologue is
228
+ `"use client"`, is rejected (on the server its exports are client
229
+ references). A `"use server"` import is fine: on the server it is the real
230
+ action, which `isAction(ref)` matches. `server-only` and `node:` imports work
231
+ on the server, but the import also stays in the `"use client"` module
232
+ (imports are not pruned from the browser bundle), so it must still load in
233
+ the browser.
234
+
235
+ Per request: a JS action POST runs the predicate with `method: "POST"`,
236
+ `actionId`, `actionResult` (the action's return value), `formData` (the last
237
+ `FormData` argument) and `actionUrl`; the default is true. An in-group
238
+ navigation GET defaults to true when params or search changed. A prefetch
239
+ partial runs the predicate, so a prefetched in-group navigation honours it. A
240
+ stale back/forward revalidation sees `stale: true`. Document requests and
241
+ no-JS form posts hold no segments, so every loader renders whatever a predicate
242
+ says. On the action-triggered refetch GET (consolidation) `isAction()` is false,
243
+ as for `urls()`. `currentParams`/`nextParams` are the server's route params,
244
+ including the `include()` mount's params, and `actionId` is the server's
245
+ `src/...#export` form; match actions with `isAction(ref)` rather than a
246
+ substring. Vitest does not run the plugin, so in unit tests the predicate stays
247
+ a plain function: test it with `runRevalidate` (`/testing`).
206
248
 
207
249
  ```ts
208
250
  import { addToCart, removeFromCart } from "./actions/cart";
@@ -211,6 +253,11 @@ import * as CartActions from "./actions/cart";
211
253
  // After an action, re-run only for cart actions; on navigation, keep the default.
212
254
  revalidate(({ isAction }) => (isAction() ? isAction(CartActions) : undefined));
213
255
 
256
+ // Decide on what the action returned (actionResult is visible on the server):
257
+ revalidate((ctx) =>
258
+ ctx.isAction(checkout) ? ctx.actionResult?.ok === true : undefined,
259
+ );
260
+
214
261
  // Matcher forms (each returns a boolean):
215
262
  isAction(); // any action
216
263
  isAction(addToCart); // one action
@@ -226,6 +273,11 @@ with the ternary above instead (same rule as a server-tree loader). A bare
226
273
  `isAction(refs)` is a hard `false` on navigation too, so the loader stops
227
274
  refetching when params change.
228
275
 
276
+ `intercept()` loaders inside `clientUrls()` do not apply their `revalidate()`
277
+ yet (a known gap, left for a follow-up). The build still checks those
278
+ predicates like any other, so one that captures a binding of the `"use client"`
279
+ module fails the build although nothing runs it: import it, or drop it.
280
+
229
281
  Two scars worth copying:
230
282
 
231
283
  - A blunt `() => false` keeps serving the OLD product on product→product
@@ -235,6 +287,80 @@ Two scars worth copying:
235
287
  actions (`isAction(CartActions)`) while product/related loaders hold —
236
288
  three freshness outcomes in a single commit, decided per loader.
237
289
 
290
+ ## After an action: what the server decides
291
+
292
+ The browser sends the action with the ids of the segments the page holds.
293
+ Everything after that is decided on the server, while it handles that request:
294
+
295
+ - **The action** runs there, and its return value is what the call resolves
296
+ with (`await action()`, `useActionState`).
297
+ - **Which loaders rerun.** Each group loader's own `revalidate()` answers, with
298
+ the action's `actionResult` and `formData` (above). A loader that holds keeps
299
+ the data the page shows.
300
+ - **The browser's caches.** When the response arrives the browser marks its
301
+ history cache stale and drops its prefetches, unless the action called
302
+ `keepClientCache()` (`/server-actions` → "Client Cache After an Action").
303
+ - **A loader that fails on its rerun** reaches your `onError` on the server and
304
+ the page as an error, below.
305
+
306
+ A form's React state survives an action's revalidation in a group, as on a
307
+ `urls()` route: the commit renders into the mounted page instead of remounting
308
+ it, so a controlled input keeps what the user typed.
309
+
310
+ ### A loader that fails on its rerun
311
+
312
+ The action has already run when its loaders rerun, so a loader that throws
313
+ there does not undo it. Where the error lands decides what the user sees:
314
+
315
+ | The route | Called directly (`await action()`) | From `<form action>`, `useActionState` or `useTransition` |
316
+ | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
317
+ | A group route, or a `urls()` route without `loading()` | The call rejects with the loader's error (`"An error occurred"` in a build), although the action returned. Nothing of that revalidation commits: the page keeps its data and its React state. | React hands the rejection to the nearest React error boundary above the form. With none of yours, the router's root error boundary replaces the page. |
318
+ | A `urls()` route with `loading()` | The call resolves with the action's result. The commit renders, the loader's read throws, and the nearest React error boundary catches it; with none of yours, the root one replaces the page. | The same. |
319
+ | Under an `errorBoundary()` (for a group, around the mount) | Its fallback replaces the route: the page and its form state unmount. | The same. |
320
+
321
+ A group route's `loading()` does not change the first row. For a failure the
322
+ user can retry (a flaky upstream, a rate limit), return it as data instead of
323
+ throwing, and render it where the data goes:
324
+
325
+ ```tsx
326
+ // stock.loader.ts
327
+ export const StockLoader = createLoader(async (ctx) => {
328
+ try {
329
+ return { ok: true as const, stock: await inventory.count(ctx.params.sku) };
330
+ } catch {
331
+ return { ok: false as const, reason: "Stock is unavailable. Try again." };
332
+ }
333
+ });
334
+
335
+ // in the group's "use client" module
336
+ function Stock() {
337
+ const { data } = useLoader(StockLoader);
338
+ return data.ok ? (
339
+ <p>{data.stock} in stock</p>
340
+ ) : (
341
+ <p role="status">{data.reason}</p>
342
+ );
343
+ }
344
+ ```
345
+
346
+ The action resolves with its result, the page and its form state stay, and the
347
+ next action or navigation that reruns the loader replaces the message. Keep
348
+ throwing for a failure that should stop the page, with an `errorBoundary()`
349
+ around the mount for it.
350
+
351
+ A failure returned as data is an ordinary value, so whatever stores the
352
+ loader's value stores it too: a `cache()` on the loader serves it for its TTL,
353
+ a `ppr` shell bakes an `ssr: false` loader's value, and a prerender stores it
354
+ at build time. A thrown error is never stored (a stale entry keeps serving).
355
+ To cache the expensive part, cache the call the loader makes (`"use cache"` on
356
+ that function: a call that throws stores nothing) and turn the throw into
357
+ data in a loader that is not cached itself.
358
+
359
+ Pinned by dev + production e2e in the router repository (a group, its `urls()`
360
+ twin, each with and without `loading()`, and the group under an
361
+ `errorBoundary()`), and by a `serveShellRequest({ action })` test of the
362
+ response.
363
+
238
364
  ## Loaders are full citizens: signals and handles
239
365
 
240
366
  Everything a DSL loader can do in the server tree works here — the loader body
@@ -321,6 +447,53 @@ need it, not the route.
321
447
 
322
448
  Pinned end to end by dev + production e2e in the router repository.
323
449
 
450
+ **Without JavaScript.** A boundary that is not complete at the first flush
451
+ ships its fallback and swaps the content in with a script, which a browser
452
+ without JavaScript never runs. That includes a client component a loader's
453
+ answer draws under an inline `<Suspense>`: a fast loader's answer usually
454
+ makes the first flush, and `ssr: false` guarantees the loader's data does. It
455
+ does not cover the client component's module: the first document a build's
456
+ fresh server renders can still be waiting for it. The dev server matches a
457
+ build here: when the shell is ready, it waits for the client components the
458
+ payload has named and that are still loading, and for nothing else.
459
+
460
+ ## `prefetch: false` — keep a loader out of prefetches
461
+
462
+ A `<Link>` prefetch of a group route runs its loaders. Flag the expensive one
463
+ and the prefetch skips it:
464
+
465
+ ```tsx
466
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
467
+ loader(PriceLoader),
468
+ loader(ReviewsLoader, { prefetch: false }), // kept out of prefetches
469
+ loading(<ProductSkeleton />),
470
+ ]),
471
+ ```
472
+
473
+ A click inside the group still shows the destination at once. With the
474
+ prefetch received, the URL and the other loaders' data move with the click
475
+ too, `useOutlet().pending` ends with that commit, and one follow-up request
476
+ (the fill) runs only the flagged loaders. Read the flagged loader inside its
477
+ own `<Suspense>` to show the page and that fallback at once; with no boundary
478
+ React holds the page being left until the fill answers, as it would with no
479
+ prefetch. Same rules as on a server route (`/loader` → "`prefetch: false`"):
480
+ only a segment new to the page is deferred, so a same-route click
481
+ (`/product/a` to `/product/b`) prefetches the loader as usual.
482
+
483
+ - On a `layout()` of the group the flag applies to every route below it.
484
+ - A flagged `loader()` on a server layout above the group is deferred while
485
+ that layout is new to the page (entering the group from outside).
486
+ - `loading()` in `clientUrls()` takes no options, and a flagged `loading()`
487
+ on a server layout above the group defers nothing for the group's routes.
488
+ - `intercept()` loaders accept the flag and ignore it, as in `urls()`.
489
+ - With `ssr: false` on a `ppr` group route the flag is ignored (the bake
490
+ lane), as on a server route.
491
+ - Test it with `serveShellRequest` in the Flight project: mount the module
492
+ itself, and `rangoUseClientTransform()` installs the group's projection
493
+ (`/testing` → `serve-shell-request.md`, "`clientUrls()` routes"). It also
494
+ registers the group's `revalidate()` predicates, so the server runs them
495
+ there as in your app.
496
+
324
497
  ## Intercepts
325
498
 
326
499
  ```tsx
@@ -292,20 +292,25 @@ traffic"). On Workers:
292
292
  the request it is called from.
293
293
 
294
294
  - **A warm fills the colo it runs in, and KV.** Another colo that already
295
- holds its own Cache API copy keeps serving it until it expires; an L1 hit
296
- never consults KV. After a deploy no colo holds a copy, so the warm's KV
297
- entry is what each one reads next. For a content change, invalidate and then
298
- warm, in this order:
295
+ holds its own Cache API copy keeps serving it until it expires or a tag
296
+ invalidation rejects it; a hit on a valid copy never consults KV. After a
297
+ deploy no colo holds a copy, so the warm's KV entry is what each one reads
298
+ next. For a content change, invalidate and then warm, in this order:
299
299
 
300
300
  ```typescript
301
- await updateTag(`product:${id}`); // no colo serves the old copy any more
301
+ await updateTag(`product:${id}`); // a colo the marker has reached refuses its old copy
302
302
  await router.prerender({ env, ctx })(`/products/${id}`); // writes the new one
303
303
  ```
304
304
 
305
- A colo with no copy of its own reads the warmed entry from KV. A colo that
306
- was still holding the old copy treats it as a miss and renders once for
307
- itself (it does not read the fresher KV entry). With `tagPurge` the purge
308
- evicts every colo's copy, so they all read the warmed entry.
305
+ A colo with no copy of its own then reads the warmed entry from KV and
306
+ renders nothing. A colo that was still holding the old copy renders once for
307
+ itself by default; with `kvFallThrough: true` on the store it reads KV as if
308
+ it held none and renders nothing (with `tagPurge` the purge evicts the copy
309
+ first). That option costs one KV get per rejected copy per colo after each
310
+ invalidation, so turn it on where a render costs more than that. The KV
311
+ entry is checked against the same tag markers as
312
+ the colo's own copy, so a colo that KV still hands the entry from before
313
+ the invalidation refuses it and renders for itself.
309
314
 
310
315
  - **Entries under 60 seconds stay in one colo.** KV's minimum `expirationTtl`
311
316
  is 60 s, so a segment, item or response entry whose `ttl + swr` is shorter
@@ -111,11 +111,24 @@ cannot mismatch its server HTML.
111
111
 
112
112
  - Navigation state, params and action state: the component shows the live
113
113
  value right after it hydrates.
114
- - Handle data a loader pushed late, and the entry's location state: every
115
- reader gets them once the whole document has streamed and hydrated, in one
116
- transition. A reader re-renders only when its selection changes.
114
+ - Handle data a loader pushed late, and the entry's location state: both wait
115
+ until the whole document has streamed and been revealed, and every boundary
116
+ has hydrated, including any boundary still suspended on the client (a lazy
117
+ chunk, a deferred-hydration gate; that wait has no upper bound). Then
118
+ location state lands in a transition and handle data in the release
119
+ notification. `useLocationState` readers re-render when the entry's
120
+ state object changes; `useHandle` readers when their selection changes.
121
+ - State set before then by a commit that renders no new page (a shallow
122
+ navigation with `revalidate: false`, Back/Forward or a revalidation that
123
+ keeps every segment, an action's location state) waits the same way: readers
124
+ keep `undefined` until every boundary has hydrated, then show the latest
125
+ entry's state.
117
126
  - A navigation that commits before then mounts its page with the live values
118
127
  from its first render. A boundary of the document that persists across that
119
128
  navigation and has not hydrated yet then hydrates against the live values;
120
129
  where they differ from its HTML, React reports the mismatch and renders that
121
130
  boundary on the client.
131
+ - A held navigation whose history entry commits during that window releases
132
+ the store for the whole hold. A boundary of the old page that hydrates during
133
+ the hold reads the destination's values and can mismatch its HTML; React
134
+ then renders that boundary on the client.
@@ -146,7 +146,9 @@ clears the shared error.
146
146
  **`load()` calls that stay local** (no broadcast, per-hook state, same
147
147
  semantics as the old per-component `useState`):
148
148
 
149
- - `load({ params: { ... } })` — explicit params.
149
+ - `load({ params: { ... } })` — explicit params. They are string values; a
150
+ `__proto__`, `constructor` or `prototype` key or a non-string value is
151
+ rejected and `load()` fails with `Invalid loader params`.
150
152
  - `load({ method: "POST", body })` — mutations.
151
153
  - Any `load()` on a `useFetchLoader(loader)` whose loader is **not**
152
154
  registered on the current route. Two unrelated components calling
@@ -479,6 +479,30 @@ mounted viewport/render Links. Links share one document-level scope observer,
479
479
  not one observer per Link or container. Adding a scope cannot recall work already
480
480
  queued or in flight; removing it re-arms both Link types.
481
481
 
482
+ ### What a prefetch renders
483
+
484
+ A prefetch is a navigation sent early (a partial request with an
485
+ `X-Rango-Prefetch` header), and by default the server renders everything for
486
+ it: every handler and every loader. That is the point, a click then commits
487
+ without a request, but it is also forty renders for a grid of forty links.
488
+ Keep the expensive part out of the prefetch on the route, not on the link:
489
+
490
+ ```tsx
491
+ // skip one loader
492
+ loader(ReviewsLoader, { prefetch: false }),
493
+ // skip what the fallback covers
494
+ loading(<OrdersSkeleton />, { prefetch: false }),
495
+ ```
496
+
497
+ A click that adopts such a prefetch commits what was prefetched, shows the
498
+ fallback for the deferred part, and sends one extra request for it. Only what
499
+ is new to the page is deferred: a segment already on screen (the layout of the
500
+ section you are in, the route itself on `/product/a` to `/product/b`) is
501
+ prefetched as usual. See
502
+ `/loader` → "`prefetch: false`" and `/route` → "`loading()` options". To stop
503
+ a link or a whole section from prefetching at all, use the `prefetch` prop or
504
+ `data-prefetch-scope` above.
505
+
482
506
  ## When to use what
483
507
 
484
508
  | Context | API | Resolves | Use for |