@rangojs/router 0.16.0 → 0.18.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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
@@ -15,7 +15,7 @@ storefront homepage (shell HIT, production build, M-series laptop): a single
15
15
  was dead for ~11s. Fast paint, frozen page.
16
16
 
17
17
  The obvious fix — wrap the heavy subtree in `<Suspense>` so it hydrates later —
18
- trades the paint away: under shell capture that boundary postpones, and the
18
+ can trade the paint away: when that boundary postpones under shell capture, the
19
19
  frozen prelude ships an empty `<main>`. This recipe gets both: the full body
20
20
  HTML in the prelude AND its hydration off the initial task, released on first
21
21
  idle at retry-lane priority.
@@ -28,11 +28,14 @@ idle at retry-lane priority.
28
28
  | interactive (menu click works) | ~3.5s | ~1.6s | ~1.6s |
29
29
 
30
30
  Measure your own page before and after — the win depends on how much of the
31
- hydration cost lives under the boundary (see "Verifying and measuring").
31
+ hydration cost lives under the boundary (see "Verifying and measuring"). These
32
+ numbers were measured before PPR captures baked handler output; see the next
33
+ section for what that changed.
32
34
 
33
35
  ## The recipe
34
36
 
35
- ~40 lines, plain React, no rango imports — copy it into your app:
37
+ About 50 lines of plain React, no rango imports. Copy it into a client module in
38
+ your app:
36
39
 
37
40
  ```tsx
38
41
  "use client";
@@ -101,23 +104,39 @@ inside hydrates after first idle.
101
104
 
102
105
  ## Why `fallback={children}` is load-bearing
103
106
 
104
- This is not a style choice; it is the half of the recipe that makes it
105
- PPR-compatible.
106
-
107
- Shell capture aborts on flight byte-quiet (`src/rsc/shell-capture.ts`,
108
- `FLIGHT_QUIET_HOPS`): once the Flight source has been byte-silent for the
109
- quiet window, the fizz render freezes. A big HTML subtree under _any_
110
- `<Suspense>` boundary cannot finish inside that window, so the boundary always
111
- postpones — boundary placement cannot fix it. Verified both ways: wrapping the
112
- client island from outside AND placing the boundary inside the island both
113
- baked `<!--$?--><template id="B:…">` into `<main>`, i.e. an empty body in the
114
- frozen prelude.
115
-
116
- With the content as the fallback, the unavoidable postpone _becomes the
117
- delivery mechanism_: the shell bakes the fallback, and the fallback IS the
118
- body. In `/ppr` hole-doctrine terms, this is the PHYSICS class exploited
119
- deliberately — you cannot stop the boundary from becoming a hole, so you make
120
- the hole's baked fallback carry the real markup.
107
+ This is not a style choice. The recipe was built when the shell capture
108
+ postponed any big `<Suspense>` subtree: the capture froze on Flight byte-quiet
109
+ (`FLIGHT_QUIET_HOPS` in `src/rsc/shell-capture.ts`) while the handler output
110
+ was still rendering, so a boundary over the page body became a hole no matter
111
+ where it sat. Verified both ways at the time: wrapping the client island from
112
+ outside AND placing the boundary inside the island both baked
113
+ `<!--$?--><template id="B:…">` into `<main>`, i.e. an empty body in the frozen
114
+ prelude. With the content as the fallback, the postpone became the delivery
115
+ mechanism: the shell baked the fallback, and the fallback was the body.
116
+
117
+ PPR captures now wait for the whole handler layer before freezing anything:
118
+ the capture records the page's segments first (async server components
119
+ rendered, every promise in the handler output settled) and renders its Flight
120
+ payload from that record (`settleCaptureRecord` in `src/rsc/shell-capture.ts`).
121
+ A boundary whose content is only handler output is therefore expected to bake
122
+ its content as a completed boundary instead of postponing. What still
123
+ postpones at capture is a boundary over live data: a loader without
124
+ `ssr: false` read with `useLoader` under `loading()` or an inline
125
+ `<Suspense>`, or a promise nested in an `ssr: false` loader's value. Run the
126
+ prelude check in "Verifying and measuring" on your page rather than assuming
127
+ either shape.
128
+
129
+ `fallback={children}` stays the safe choice either way:
130
+
131
+ - If the boundary still postpones (live data inside it), the fallback is what
132
+ the prelude shows, so it must be the content.
133
+ - The sync-update trap below: a client render of the boundary shows the
134
+ fallback. With `fallback={children}` that is a visual no-op; with
135
+ `fallback={null}` it blanks the page.
136
+
137
+ The hydration deferral does not depend on which shape you get: `HydrationGate`
138
+ suspends during hydration, and React keeps the server DOM of the boundary in
139
+ place until the gate releases.
121
140
 
122
141
  ## Why the client gate is free
123
142
 
@@ -163,13 +182,14 @@ release on interaction instead (see Variations).
163
182
 
164
183
  ## Known cost: the body rides twice (measure it, don't guess)
165
184
 
166
- On a shell HIT the gated subtree's HTML is in the response twice — once as the
167
- baked fallback in the prelude, once as the resumed hole content (the resume
168
- re-renders and re-ships it; there is no bake-through). Homepage measurement:
169
- 234KB → 302KB gzipped (+68KB, +29%; raw +1.05MB). It is post-paint bandwidth,
170
- not render-blocking — the visible prelude streams first — but it is real bytes
171
- on every document GET. Weigh it per page; on a small body the recipe may not
172
- pay for itself.
185
+ When the boundary postpones at capture, a shell HIT carries the gated
186
+ subtree's HTML twice — once as the baked fallback in the prelude, once as the
187
+ resumed hole content (the resume re-renders and re-ships it; there is no
188
+ bake-through). Homepage measurement (postponing boundary): 234KB → 302KB
189
+ gzipped (+68KB, +29%; raw +1.05MB). It is post-paint bandwidth, not
190
+ render-blocking — the visible prelude streams first — but it is real bytes on
191
+ every document GET. Weigh it per page; on a small body the recipe may not pay
192
+ for itself. A boundary that bakes its content ships it once.
173
193
 
174
194
  ## Verifying and measuring
175
195
 
@@ -229,7 +249,8 @@ this exact space. The recipe survives that future; a primitive might not.
229
249
 
230
250
  ## Related
231
251
 
232
- - `/ppr` — the shell/hole mechanics this recipe rides on (hole doctrine:
233
- PHYSICS class), and why the capture postpones any big Suspense subtree
234
- - `src/rsc/shell-capture.ts` — the byte-quiet capture window
235
- (`FLIGHT_QUIET_HOPS`) that makes `fallback={children}` mandatory
252
+ - `/ppr` — the shell/hole mechanics this recipe rides on: what bakes into the
253
+ shell, and which boundaries stay holes
254
+ - `src/rsc/shell-capture.ts` — the record-first capture (`settleCaptureRecord`)
255
+ and the byte-quiet window (`FLIGHT_QUIET_HOPS`) that decide what the prelude
256
+ holds
@@ -10,6 +10,9 @@ Start with one question: **does the request reach Rango before shared bytes are
10
10
  served?** That boundary decides whether middleware runs, whether loaders stay
11
11
  live, and which invalidation system owns the result.
12
12
 
13
+ This skill is about where each cache sits relative to your deployment platform.
14
+ To choose between the in-app layers themselves, start at `/cache-guide`.
15
+
13
16
  ## The execution matrix
14
17
 
15
18
  | Mechanism | Stored artifact | Function/worker runs on a hit? | Rango middleware on a hit? | What stays live? |
@@ -53,8 +56,11 @@ There are two shell producers:
53
56
  Both producers feed the same in-function serve path. Build-time shells are
54
57
  content-hashed modules in the server bundle, not CDN-served HTML files.
55
58
 
56
- `ppr.ttl`, `ppr.swr`, and `ppr.tags` govern that shell entry. They do **not**
57
- emit HTTP `Cache-Control` and do not configure a platform CDN.
59
+ `ppr.ttl` and `ppr.swr` set the shell entry's lifetime, capped by the route
60
+ `cache()` entry it was captured from (the shorter wins). Its tags are
61
+ `ppr.tags` plus the tags its content recorded. A route `cache({ key })` or a
62
+ store `keyGenerator` stores one shell per partition value. None of this emits
63
+ HTTP `Cache-Control` or configures a platform CDN.
58
64
 
59
65
  ## Platform deployment shapes
60
66
 
@@ -173,4 +179,6 @@ provably public and shared.
173
179
  - `/document-cache` — store-backed complete-response middleware
174
180
  - `/vercel` — Vercel Build Output preset and Runtime Cache wiring
175
181
  - `/cloudflare` — Worker deployment and bindings
176
- - `/cache-guide` — function, loader, and segment cache selection
182
+ - `/cache-guide` — choosing between the in-app cache layers
183
+ - `/caching` — `cache()` segments, stores, and tag invalidation
184
+ - `/use-cache` — function-level `"use cache"`
@@ -6,13 +6,21 @@ argument-hint: [setup]
6
6
 
7
7
  # Store-backed Document Cache
8
8
 
9
- Caches complete HTTP responses (HTML/RSC) in the app-level cache store based on
10
- `Cache-Control`. Routes opt in by setting `s-maxage`.
9
+ Caches complete HTTP responses (HTML documents and RSC payloads) in the
10
+ app-level cache store. Routes opt in by sending a `Cache-Control` header with
11
+ `s-maxage`; `stale-while-revalidate` adds a stale window. On a hit, nothing
12
+ below the middleware runs — no route middleware, handlers, or loaders.
13
+
14
+ Use it for pages whose **whole** response is public and identical for every
15
+ visitor sharing a URL, when you want the reuse to happen inside the app (so
16
+ outer middleware still runs and invalidation stays in Rango's tag system).
11
17
 
12
18
  This middleware runs **inside** the worker/function. It is not itself a platform
13
- CDN cache. With `CFCacheStore` the response family can use Cloudflare's edge/KV
14
- tiers; with `VercelCacheStore` it uses Vercel Runtime Cache. The request still
15
- reaches Rango before the middleware can return a store hit.
19
+ CDN cache. It works with every built-in store: with `CFCacheStore` the response
20
+ family uses Cloudflare's Cache API (and KV when configured), with
21
+ `VercelCacheStore` it uses Vercel Runtime Cache, and `MemorySegmentCacheStore`
22
+ keeps responses in process memory. The request still reaches Rango before the
23
+ middleware can return a store hit.
16
24
 
17
25
  ## Not this skill if…
18
26
 
@@ -27,9 +35,9 @@ reaches Rango before the middleware can return a store hit.
27
35
  ## Setup
28
36
 
29
37
  Document caching is a middleware. Add `createDocumentCacheMiddleware()` to the
30
- router with `.use()`. The cache store it reads from is the app-level store you
31
- configure on `createRouter({ cache })` (available on the request context as
32
- `requestCtx._cacheStore`), not a store passed to the middleware.
38
+ router with `.use()`. It reads and writes the app-level store you configure on
39
+ `createRouter({ cache })`; the store is not a middleware option. Without an app
40
+ store, the middleware passes every request through.
33
41
 
34
42
  ```typescript
35
43
  import { createRouter } from "@rangojs/router";
@@ -39,7 +47,7 @@ import {
39
47
  } from "@rangojs/router/cache";
40
48
  import { urlpatterns } from "./urls";
41
49
 
42
- const router = createRouter<AppBindings>({
50
+ export const router = createRouter<AppBindings>({
43
51
  document: Document,
44
52
  urls: urlpatterns,
45
53
  // App-level cache store. The document cache middleware uses this store's
@@ -53,13 +61,14 @@ router.use(
53
61
  debug: process.env.NODE_ENV === "development",
54
62
  }),
55
63
  );
56
-
57
- export default router;
58
64
  ```
59
65
 
66
+ Middleware registered before it runs on every request, hits included; everything
67
+ after it (and all route middleware) runs only on a miss.
68
+
60
69
  ## Route Opt-In with Cache-Control
61
70
 
62
- Routes opt-in to document caching by setting a `Cache-Control` response header
71
+ Routes opt in to document caching by setting a `Cache-Control` response header
63
72
  with `s-maxage`. The middleware caches responses whose `Cache-Control` includes
64
73
  `s-maxage`; `stale-while-revalidate` enables background revalidation (SWR).
65
74
 
@@ -86,6 +95,21 @@ function BlogPostHandler(ctx) {
86
95
  // Dashboard sets no Cache-Control header, so it is never document-cached.
87
96
  ```
88
97
 
98
+ A handler inside a `cache()` boundary or on a `ppr` route cannot write response
99
+ headers (the write throws). Set the header from route middleware instead — it
100
+ runs on every request that reaches the route:
101
+
102
+ ```typescript
103
+ cache({ ttl: 60 }, () => [
104
+ path("/blog", BlogIndex, { name: "blog" }, () => [
105
+ middleware(async (ctx, next) => {
106
+ ctx.header("Cache-Control", "s-maxage=300, stale-while-revalidate=3600");
107
+ await next();
108
+ }),
109
+ ]),
110
+ ]);
111
+ ```
112
+
89
113
  ## Document Cache Options
90
114
 
91
115
  `createDocumentCacheMiddleware(options?)` accepts:
@@ -95,10 +119,11 @@ createDocumentCacheMiddleware({
95
119
  // Skip specific paths (matched by pathname prefix)
96
120
  skipPaths: ["/api", "/admin"],
97
121
 
98
- // Custom cache key generator
99
- keyGenerator: (url) => url.pathname,
122
+ // Custom cache key base. Replaces host + pathname + search entirely, so
123
+ // include the host yourself when one deployment serves several domains.
124
+ keyGenerator: (url) => `${url.host}${url.pathname}`,
100
125
 
101
- // Conditional caching, evaluated per request
126
+ // Conditional caching, evaluated per request (receives the middleware ctx)
102
127
  isEnabled: (ctx) => !ctx.request.headers.has("x-preview"),
103
128
 
104
129
  // Debug logging (HIT, MISS, STALE, REVALIDATED)
@@ -150,18 +175,26 @@ header or logs to identify an actual CDN hit.
150
175
 
151
176
  ## Cache Key Generation
152
177
 
153
- Default keys differentiate:
178
+ The default key is the host, pathname, and sorted search string (internal
179
+ `_rsc*` params removed, `cache.searchParams` filtering applied), followed by
180
+ suffixes that separate response variants:
154
181
 
155
- - HTML requests: `{pathname}:html`
156
- - RSC partials: `{pathname}:{segmentHash}:rsc`
182
+ - HTML document requests: `{host}{pathname}[?search]:html`
183
+ - RSC requests (client navigations and full-document Flight fetches):
184
+ `{host}{pathname}[?search]:rsc`
185
+ - Client navigations additionally add a hash of the segments the client already
186
+ has (`:{segmentHash}`), so navigations from pages with different layouts get
187
+ different cached payloads, and a `:fragments` marker when the client accepts
188
+ fragment envelopes.
157
189
 
158
- Segment hash ensures different cached responses for navigations from different source pages (with different layouts).
190
+ A custom `keyGenerator(url)` replaces only the `{host}{pathname}[?search]` part;
191
+ the variant suffixes are still appended.
159
192
 
160
193
  ## What Gets Cached
161
194
 
162
195
  - Full HTML responses (document requests)
163
196
  - RSC payloads (client navigation)
164
- - Only 200 OK responses whose `Cache-Control` includes `s-maxage`
197
+ - Only `GET` 200 responses whose `Cache-Control` includes `s-maxage`
165
198
 
166
199
  ## What's NOT Cached
167
200
 
@@ -169,7 +202,41 @@ Segment hash ensures different cached responses for navigations from different s
169
202
  - Loader requests (`_rsc_loader`)
170
203
  - Non-GET requests
171
204
  - Responses without an `s-maxage` `Cache-Control` directive
205
+ - Responses whose `Cache-Control` also contains `private`, `no-store`, or an
206
+ unqualified `no-cache` (those win over `s-maxage` in a shared cache)
207
+ - Responses carrying `Set-Cookie` (a per-client value must not be replayed to
208
+ everyone). That includes a response whose request called
209
+ `updateTag()`/`revalidateTag()` on a store that keeps per-isolate memos
210
+ (`CFCacheStore`, `VercelCacheStore`): it carries the fresh-reads cookie
211
+ (`rango-state-fresh` by default), so it is not stored.
172
212
  - Non-200 responses
213
+ - A 200 whose render reported an error: a component that threw after the
214
+ response started streaming, such as an async server component whose fetch
215
+ failed. The status is already 200, so the body carries the error (an error
216
+ row in the RSC payload, an errored Suspense boundary in the HTML). The write
217
+ is skipped and logged as `[DocumentCache] cache write`; the error goes to
218
+ `onError` as `cache-write` unless the router already reported that same
219
+ error during the render. The current request still gets the errored render,
220
+ the next request renders fresh, and on a stale refresh the stale entry keeps
221
+ serving.
222
+
223
+ ## Invalidation
224
+
225
+ Entries expire by `s-maxage` (+ `stale-while-revalidate`). They are also tagged
226
+ with the request's tag set, so `updateTag()`/`revalidateTag()` evict them:
227
+
228
+ - a render-callable `cacheTag("...")` in any server component on the page;
229
+ - tags of `cache()` segments, cached loaders, and `"use cache"` functions the
230
+ render read. A `cache()` segment HIT contributes the tags its content
231
+ recorded when it was written (render-callable calls and `"use cache"` reads
232
+ inside it), so a document stored over the HIT stays evictable by them. A
233
+ cached loader's HIT likewise contributes the tags its body recorded.
234
+
235
+ Tags are snapshotted after the response body has fully streamed, so tags from
236
+ Suspense-streamed content are included. A `Prerender()` route's build-time
237
+ segments contribute no tags (only its live loaders can), so such an entry
238
+ usually expires by TTL only.
239
+ See `/caching` → "Tag-Based Invalidation".
173
240
 
174
241
  ## Complete Example
175
242
 
@@ -179,7 +246,7 @@ import { createRouter } from "@rangojs/router";
179
246
  import { createDocumentCacheMiddleware, CFCacheStore } from "@rangojs/router/cache";
180
247
  import { urlpatterns } from "./urls";
181
248
 
182
- const router = createRouter<AppBindings>({
249
+ export const router = createRouter<AppBindings>({
183
250
  document: Document,
184
251
  urls: urlpatterns,
185
252
  cache: (_env, ctx) => ({ store: new CFCacheStore({ ctx: ctx! }) }),
@@ -192,7 +259,6 @@ router.use(
192
259
  }),
193
260
  );
194
261
 
195
- export default router;
196
262
 
197
263
  // urls.tsx
198
264
  import { urls } from "@rangojs/router";
@@ -225,14 +291,18 @@ function BlogPost(ctx) {
225
291
  }
226
292
  ```
227
293
 
294
+ Note that `BlogPostLoader` output is frozen into the cached response along with
295
+ everything else.
296
+
228
297
  ## Document Cache vs Segment Cache
229
298
 
230
- | Feature | Document Cache | Segment Cache |
231
- | ------------ | -------------------------- | --------------------- |
232
- | Granularity | Full response | Individual segments |
233
- | Opt-in | `Cache-Control` `s-maxage` | `cache({ ttl, swr })` |
234
- | Use case | Static pages | Dynamic compositions |
235
- | Key includes | URL + segment hash | Route params |
299
+ | Feature | Document Cache | Segment Cache (`cache()`) |
300
+ | ------------ | ------------------------------------- | --------------------------------- |
301
+ | Granularity | Full response | Individual segments |
302
+ | Opt-in | `Cache-Control` `s-maxage` | `cache({ ttl, swr })` |
303
+ | Loaders | Frozen with the response | Always run fresh |
304
+ | Use case | Static pages | Dynamic compositions |
305
+ | Key includes | host + URL + variant (+ segment hash) | host + pathname + params + search |
236
306
 
237
307
  Use document cache for mostly-static pages. Use segment cache when different parts of a page have different cache requirements.
238
308
 
@@ -6,7 +6,10 @@ argument-hint: [provider]
6
6
 
7
7
  # Fonts
8
8
 
9
- Load web fonts in the Document component with `<link rel="preload">` for optimal performance. Fonts are declared in `<head>` alongside your stylesheet.
9
+ Three ways to load web fonts in a Rango app: a hosted provider (Google Fonts),
10
+ self-hosted files in `public/`, or Fontsource packages bundled by Vite (the
11
+ recommended default). Font links go in the Document `<head>` next to your
12
+ stylesheet, which follows the `?url` + `precedence` pattern from `/css`.
10
13
 
11
14
  ## Google Fonts
12
15
 
@@ -15,7 +18,7 @@ Load web fonts in the Document component with `<link rel="preload">` for optimal
15
18
  "use client";
16
19
 
17
20
  import type { ReactNode } from "react";
18
- import { MetaTags } from "@rangojs/router/client";
21
+ import { Html } from "@rangojs/router/client";
19
22
  import styles from "./index.css?url";
20
23
 
21
24
  export function Document({ children }: { children: ReactNode }) {
@@ -30,16 +33,17 @@ export function Document({ children }: { children: ReactNode }) {
30
33
  crossOrigin="anonymous"
31
34
  />
32
35
 
33
- {/* Load font stylesheet */}
36
+ {/* Font stylesheet first, so app styles can override it */}
34
37
  <link
35
38
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
36
39
  rel="stylesheet"
40
+ precedence="default"
37
41
  />
38
42
 
39
43
  {/* App styles */}
40
44
  <link rel="preload" href={styles} as="style" />
41
- <link rel="stylesheet" href={styles} />
42
- <MetaTags />
45
+ <link rel="stylesheet" href={styles} precedence="default" />
46
+ <Html.Meta />
43
47
  </head>
44
48
  <body>{children}</body>
45
49
  </html>
@@ -60,6 +64,8 @@ Or with Tailwind (see `/tailwind`):
60
64
 
61
65
  ```css
62
66
  /* src/index.css */
67
+ @import "tailwindcss";
68
+
63
69
  @theme {
64
70
  --font-sans: "Inter", sans-serif;
65
71
  }
@@ -92,7 +98,9 @@ body {
92
98
  }
93
99
  ```
94
100
 
95
- Preload the most critical weight in the Document:
101
+ Preload the most critical weight in the Document (same imports as the Google
102
+ Fonts example above). Font preloads need `crossOrigin` even for same-origin
103
+ files, or the browser fetches the font twice:
96
104
 
97
105
  ```tsx
98
106
  export function Document({ children }: { children: ReactNode }) {
@@ -107,8 +115,8 @@ export function Document({ children }: { children: ReactNode }) {
107
115
  crossOrigin="anonymous"
108
116
  />
109
117
  <link rel="preload" href={styles} as="style" />
110
- <link rel="stylesheet" href={styles} />
111
- <MetaTags />
118
+ <link rel="stylesheet" href={styles} precedence="default" />
119
+ <Html.Meta />
112
120
  </head>
113
121
  <body>{children}</body>
114
122
  </html>
@@ -6,7 +6,7 @@ argument-hint: "[handler]"
6
6
 
7
7
  # Handler-Attached `.use`
8
8
 
9
- A handler function (or branded `Static`/`Prerender`/`Passthrough` definition) can carry its own defaults via a `.use` callback that returns an array of `use` items (loader, middleware, parallel, intercept, layout, loading, etc.). The mount-site DSL (`path()`, `layout()`, `parallel()`, `intercept()`) merges those defaults with any explicit `use()` callback supplied at the registration site.
9
+ A handler function (or branded `Static`/`Prerender`/`Passthrough` definition) can carry its own defaults via a `.use` callback that returns an array of `use` items (loader, middleware, parallel, intercept, layout, loading, etc.). The mount-site DSL (`path()`, `layout()`, `parallel()`, `intercept()`) merges those defaults with any explicit `use()` callback supplied at the registration site. Loader definitions from `createLoader()` accept a `.use` too, limited to `revalidate` and `cache` (see "Loaders" below).
10
10
 
11
11
  This lets handlers be **self-contained, reusable units** — a page brings its own loader, a layout brings its own middleware, a parallel slot brings its own data + skeleton — without forcing every caller to wire the same items at every mount site.
12
12
 
@@ -51,13 +51,16 @@ Now `ProductPage` carries its loader, loading state, and response-header middlew
51
51
 
52
52
  `handler.use()` is the same callback shape regardless of where the handler runs, but the runtime validates that the items it returns are valid for the mount site. Driven by `MOUNT_SITE_ALLOWED_TYPES` in [resolve-handler-use.ts](../../src/route-definition/resolve-handler-use.ts):
53
53
 
54
- | Mount site | Allowed item types |
55
- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56
- | `path()` / `route()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
57
- | `layout()` | All of the above, plus `route`, `include` |
58
- | `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
59
- | `intercept()` | `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `layout`, `route`, `transition` (conditional activation is config-only: `intercept(..., { when })`) |
60
- | Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
54
+ | Mount site | Allowed item types |
55
+ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56
+ | `path()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
57
+ | `layout()` | All of the above, plus `route` (nested `path()` items) and `include` |
58
+ | `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
59
+ | `intercept()` | `middleware`, `loader`, `loading`, `layout` (modal chrome, without its own `use()` items), `route`, `transition`. Conditional activation is config-only: `intercept(..., { when })`. Anything else throws with a pointer: `revalidate` goes on the intercept's loader (`loader(Def, () => [revalidate(...)])`), `errorBoundary`/`notFoundBoundary` on the enclosing layout, `cache` on the target route |
60
+ | Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
61
+ | `loader()` (a `createLoader()` definition) | `revalidate`, `cache` |
62
+
63
+ `route` is the item type a `path()` call produces.
61
64
 
62
65
  For per-item semantics see the dedicated skills: [middleware](../middleware/SKILL.md), [loader](../loader/SKILL.md), [parallel](../parallel/SKILL.md), [intercept](../intercept/SKILL.md), [layout](../layout/SKILL.md), [view-transitions](../view-transitions/SKILL.md).
63
66
 
@@ -68,7 +71,13 @@ handler.use() returned middleware() which is not valid inside parallel().
68
71
  Allowed types: revalidate, loader, loading, errorBoundary, notFoundBoundary, transition.
69
72
  ```
70
73
 
71
- The narrowest contract is `parallel()` — slots cannot bring their own middleware or layout; only data, loading, error/notFound boundaries, revalidation, and transitions.
74
+ The narrowest contract for a handler is `parallel()` — slots cannot bring their own middleware or layout; only data, loading, error/notFound boundaries, revalidation, and transitions.
75
+
76
+ This runtime check applies to items returned by `handler.use()`. Items in an explicit mount-site `use()` callback are constrained by the TypeScript use-item types (e.g. a `parallel()` callback is typed to parallel items), not by this runtime allow-list. `intercept()` is the exception: it checks its merged items, explicit `use()` and `handler.use()` together, against the same list at runtime (`validateInterceptUseItems`), and a rejected item throws with a pointer instead:
77
+
78
+ ```
79
+ errorBoundary() is not valid inside intercept("@modal", "product") use() (including the handler's .use): put it on the enclosing layout or path; it handles the intercept's handler and loader errors.
80
+ ```
72
81
 
73
82
  ## Composition with explicit `use()`
74
83
 
@@ -175,6 +184,8 @@ parallel({
175
184
  });
176
185
  ```
177
186
 
187
+ On a `ppr` route, a slot's `loading()` over handler output does not make a hole. A slot handler that awaits `ctx.use(CartLoader)` is handler output: it runs at capture, its value is baked into the shared shell, and every HIT replays it without running the handler. A `cookies()`/`headers()` read in that loader refuses the capture, so the route stays on MISS. For a per-request slot, read the loader with `useLoader()` in a `"use client"` component under the slot's `loading()` (see `/ppr` → "The layout-with-loaders playbook").
188
+
178
189
  ### Two scopes for explicit `use` at the mount site: shared (broadcast) and slot-local
179
190
 
180
191
  `parallel()` accepts an explicit `use()` callback that **broadcasts** to every slot in the call ([dsl-helpers.ts](../../src/route-definition/dsl-helpers.ts)). That's the right behavior for the items the parallel allow-list permits and that accumulate (`loader`, `revalidate`, `errorBoundary`, `notFoundBoundary`, `transition`) — every slot gets them. (Note: `middleware` is not allowed inside `parallel()`; see the allowed-types table above.)
@@ -281,7 +292,7 @@ AdminLayout.use = () => [
281
292
 
282
293
  ### Intercepts (`intercept()`)
283
294
 
284
- Intercept handlers can carry their own middleware chain, loaders, and even nested layouts/routes for the modal shell.
295
+ Intercept handlers can carry their own middleware chain, loaders, `loading()`, and the modal chrome: a `layout(Chrome)` with no `use()` items of its own. Boundaries, `cache()` and `revalidate()` throw here (see the table above and [intercept](../intercept/SKILL.md)).
285
296
 
286
297
  ```typescript
287
298
  const QuickViewModal: Handler = async (ctx) => {
@@ -295,6 +306,30 @@ QuickViewModal.use = () => [
295
306
  ];
296
307
  ```
297
308
 
309
+ ### Loaders (`loader()`)
310
+
311
+ A loader definition can carry its own cache and revalidation defaults. They are
312
+ merged into every `loader(Def)` registration, before the explicit
313
+ `loader(Def, () => [...])` items:
314
+
315
+ ```typescript
316
+ import { cache, createLoader, revalidate } from "@rangojs/router";
317
+ import * as CatalogActions from "./actions/catalog";
318
+
319
+ export const CategoriesLoader = createLoader(async () => fetchCategories());
320
+ CategoriesLoader.use = () => [
321
+ cache({ ttl: 300 }),
322
+ // Loaders re-run after every action by default. Re-run this one only for
323
+ // catalog actions; on navigation, undefined keeps the default.
324
+ revalidate((ctx) =>
325
+ ctx.isAction() ? ctx.isAction(CatalogActions) : undefined,
326
+ ),
327
+ ];
328
+
329
+ // Mount sites get the cache + revalidation defaults with no extra wiring.
330
+ layout(<ShopLayout />, () => [loader(CategoriesLoader) /* … */]);
331
+ ```
332
+
298
333
  ## `loading()` is a single-assignment item — scope it correctly
299
334
 
300
335
  Most `use` items accumulate when merged: `handler.use` `middleware()` runs _and_ explicit `middleware()` runs; both `loader()` registrations apply. `loading()` is different — it mutates `entry.loading` directly, last call wins ([dsl-helpers.ts `loading`](../../src/route-definition/dsl-helpers.ts)).
@@ -310,7 +345,7 @@ Cart.use = () => [loader(CartLoader), loading(<CartSkeleton />)];
310
345
  const Notifs: Handler = async (ctx) => { /* … */ };
311
346
  Notifs.use = () => [loader(NotifsLoader), loading(<NotifsSkeleton />)];
312
347
 
313
- // ✅ @cart gets a custom skeleton; @notifs keeps its handler.use default.
348
+ // @cart gets a custom skeleton; @notifs keeps its handler.use default.
314
349
  parallel({
315
350
  "@cart": {
316
351
  handler: Cart,
@@ -319,7 +354,7 @@ parallel({
319
354
  "@notifs": Notifs,
320
355
  });
321
356
 
322
- // ✅ Opt one slot out of streaming while siblings still stream the broadcast.
357
+ // Opt one slot out of streaming while siblings still stream the broadcast.
323
358
  parallel(
324
359
  {
325
360
  "@cart": { handler: Cart, use: () => [loading(false)] },
@@ -333,7 +368,7 @@ Per-slot merge order is **handler.use → shared use → slot-local use**. Slot-
333
368
 
334
369
  Other things to keep in mind about `loading()`:
335
370
 
336
- - Any `loading()` (regardless of source) makes the segment a streaming unit. A handler that includes `loading()` in its `.use` opts every mount site into streaming by default. To opt back out, pass `loading(false)` at the mount site (`loading: false` handling in [match-middleware/segment-resolution.ts](../../src/router/match-middleware/segment-resolution.ts)) — use the slot descriptor form for parallel slots so the opt-out doesn't broadcast.
371
+ - Any `loading()` (regardless of source) makes the segment a streaming unit. A handler that includes `loading()` in its `.use` opts every mount site into streaming by default. To opt back out, pass `loading(false)` at the mount site (`loading === false` handling in [segment-resolution/fresh.ts](../../src/router/segment-resolution/fresh.ts)) — use the slot descriptor form for parallel slots so the opt-out doesn't broadcast.
337
372
 
338
373
  Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every mount site to stream by default. Use the slot descriptor's `use` for any per-slot intent at a `parallel()` call.
339
374
 
@@ -343,10 +378,13 @@ Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every
343
378
  - **Branded handlers** — `Static()`, `Prerender()`, and `Passthrough()` are positional constructors (not object-arg). Construct first, then attach `.use` to the returned definition:
344
379
 
345
380
  ```typescript
346
- const ProductPage = Prerender(async (ctx) => {
347
- const product = await fetchProduct(ctx.params.slug);
348
- return <ProductView product={product} />;
349
- });
381
+ const ProductPage = Prerender(
382
+ async () => [{ slug: "shoes" }, { slug: "jacket" }], // getParams
383
+ async (ctx) => {
384
+ const product = await fetchProduct(ctx.params.slug);
385
+ return <ProductView product={product} />;
386
+ },
387
+ );
350
388
  ProductPage.use = () => [loader(ProductLoader)];
351
389
  ```
352
390