@rangojs/router 0.16.0 → 0.17.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 (245) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +7 -4
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +1 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/types/cache-types.d.ts +14 -10
  67. package/dist/types/types/handler-context.d.ts +14 -0
  68. package/dist/types/types/loader-types.d.ts +8 -6
  69. package/dist/types/urls/response-types.d.ts +3 -12
  70. package/dist/types/use-loader.d.ts +12 -3
  71. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  72. package/dist/types/vite/rango.d.ts +1 -1
  73. package/dist/vite/index.js +1381 -1346
  74. package/package.json +2 -2
  75. package/skills/api-client/SKILL.md +22 -10
  76. package/skills/breadcrumbs/SKILL.md +65 -22
  77. package/skills/bundle-analysis/SKILL.md +22 -22
  78. package/skills/cache-guide/SKILL.md +207 -97
  79. package/skills/caching/SKILL.md +295 -93
  80. package/skills/catalog.json +16 -16
  81. package/skills/client-urls/SKILL.md +53 -31
  82. package/skills/cloudflare/SKILL.md +72 -20
  83. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  84. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  85. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  86. package/skills/comparison/SKILL.md +5 -0
  87. package/skills/comparison/references/framework-comparison.md +39 -33
  88. package/skills/composability/SKILL.md +38 -29
  89. package/skills/css/SKILL.md +10 -13
  90. package/skills/debug-manifest/SKILL.md +74 -31
  91. package/skills/defer-hydration/SKILL.md +2 -1
  92. package/skills/deployment-caching/SKILL.md +6 -1
  93. package/skills/document-cache/SKILL.md +92 -28
  94. package/skills/fonts/SKILL.md +16 -8
  95. package/skills/handler-use/SKILL.md +53 -17
  96. package/skills/hooks/SKILL.md +28 -20
  97. package/skills/hooks/data.md +23 -3
  98. package/skills/hooks/handle-and-actions.md +31 -16
  99. package/skills/hooks/navigation.md +39 -10
  100. package/skills/hooks/outlets.md +13 -6
  101. package/skills/hooks/state.md +37 -15
  102. package/skills/hooks/urls.md +7 -5
  103. package/skills/host-router/SKILL.md +64 -12
  104. package/skills/i18n/SKILL.md +60 -34
  105. package/skills/intercept/SKILL.md +161 -26
  106. package/skills/layout/SKILL.md +106 -50
  107. package/skills/links/SKILL.md +51 -19
  108. package/skills/loader/SKILL.md +242 -106
  109. package/skills/middleware/SKILL.md +119 -23
  110. package/skills/migrate-nextjs/SKILL.md +100 -45
  111. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  112. package/skills/migrate-react-router/SKILL.md +49 -38
  113. package/skills/migrate-react-router/component-migration.md +25 -6
  114. package/skills/migrate-react-router/data-and-actions.md +18 -5
  115. package/skills/migrate-react-router/route-mapping.md +12 -6
  116. package/skills/mime-routes/SKILL.md +12 -7
  117. package/skills/observability/SKILL.md +28 -14
  118. package/skills/parallel/SKILL.md +75 -39
  119. package/skills/ppr/SKILL.md +345 -150
  120. package/skills/prerender/SKILL.md +189 -100
  121. package/skills/rango/SKILL.md +65 -34
  122. package/skills/react-compiler/SKILL.md +6 -5
  123. package/skills/response-routes/SKILL.md +133 -25
  124. package/skills/route/SKILL.md +165 -60
  125. package/skills/router-setup/SKILL.md +418 -240
  126. package/skills/scripts/SKILL.md +31 -20
  127. package/skills/server-actions/SKILL.md +220 -59
  128. package/skills/shell-manifest/SKILL.md +12 -7
  129. package/skills/streams-and-websockets/SKILL.md +32 -14
  130. package/skills/tailwind/SKILL.md +37 -33
  131. package/skills/testing/SKILL.md +68 -32
  132. package/skills/testing/cache-prerender.md +55 -29
  133. package/skills/testing/client-components.md +59 -6
  134. package/skills/testing/e2e-parity.md +12 -2
  135. package/skills/testing/flight.md +19 -11
  136. package/skills/testing/handles.md +8 -5
  137. package/skills/testing/loader.md +8 -5
  138. package/skills/testing/middleware.md +8 -3
  139. package/skills/testing/predicates.md +120 -0
  140. package/skills/testing/render-handler.md +70 -22
  141. package/skills/testing/response-routes.md +11 -4
  142. package/skills/testing/reverse-and-types.md +20 -14
  143. package/skills/testing/server-actions.md +12 -3
  144. package/skills/testing/server-tree.md +9 -0
  145. package/skills/testing/setup.md +9 -5
  146. package/skills/theme/SKILL.md +106 -29
  147. package/skills/typesafety/SKILL.md +20 -9
  148. package/skills/typesafety/env-and-bindings.md +126 -47
  149. package/skills/typesafety/generated-files-and-cli.md +26 -14
  150. package/skills/typesafety/params-and-search.md +44 -19
  151. package/skills/typesafety/route-types.md +50 -14
  152. package/skills/use-cache/SKILL.md +200 -82
  153. package/skills/vercel/SKILL.md +21 -11
  154. package/skills/view-transitions/SKILL.md +41 -20
  155. package/src/__augment-tests__/augmented.check.ts +26 -1
  156. package/src/browser/index.ts +2 -2
  157. package/src/browser/navigation-bridge.ts +3 -4
  158. package/src/browser/navigation-transaction.ts +22 -2
  159. package/src/browser/partial-update.ts +35 -25
  160. package/src/browser/react/ScrollRestoration.tsx +6 -6
  161. package/src/browser/rsc-router.tsx +1 -1
  162. package/src/browser/scroll-restoration.ts +53 -6
  163. package/src/cache/cache-exec-scope.ts +37 -4
  164. package/src/cache/cache-key-utils.ts +1 -6
  165. package/src/cache/cache-runtime.ts +230 -61
  166. package/src/cache/cache-scope.ts +76 -12
  167. package/src/cache/document-cache.ts +15 -0
  168. package/src/cache/handle-capture.ts +90 -10
  169. package/src/cache/handle-snapshot.ts +132 -28
  170. package/src/cache/segment-codec.ts +26 -7
  171. package/src/cache/tag-invalidation.ts +4 -3
  172. package/src/cache/taint.ts +1 -1
  173. package/src/cache/types.ts +16 -3
  174. package/src/client.rsc.tsx +10 -9
  175. package/src/client.tsx +5 -4
  176. package/src/components/DefaultDocument.tsx +7 -2
  177. package/src/context-var.ts +6 -5
  178. package/src/escape-script.ts +1 -1
  179. package/src/handles/MetaTags.tsx +13 -11
  180. package/src/handles/Scripts.tsx +11 -10
  181. package/src/handles/meta.ts +2 -2
  182. package/src/handles/script.ts +17 -16
  183. package/src/href-client.ts +1 -1
  184. package/src/html.ts +12 -0
  185. package/src/index.rsc.ts +5 -0
  186. package/src/index.ts +1 -0
  187. package/src/loader-store.ts +7 -4
  188. package/src/prerender/build-shell-capture.ts +8 -11
  189. package/src/reverse.ts +44 -4
  190. package/src/route-definition/dsl-helpers.ts +117 -30
  191. package/src/route-definition/helpers-types.ts +33 -9
  192. package/src/route-definition/resolve-handler-use.ts +121 -6
  193. package/src/route-types.ts +0 -3
  194. package/src/router/debug-manifest.ts +10 -2
  195. package/src/router/error-handling.ts +51 -15
  196. package/src/router/handler-context.ts +34 -25
  197. package/src/router/intercept-resolution.ts +125 -39
  198. package/src/router/loader-resolution.ts +56 -11
  199. package/src/router/match-api.ts +12 -17
  200. package/src/router/match-middleware/background-revalidation.ts +161 -96
  201. package/src/router/match-middleware/cache-lookup.ts +93 -10
  202. package/src/router/match-middleware/cache-store.ts +17 -90
  203. package/src/router/middleware-types.ts +3 -5
  204. package/src/router/middleware.ts +52 -9
  205. package/src/router/prerender-match.ts +53 -19
  206. package/src/router/route-snapshot.ts +11 -1
  207. package/src/router/router-context.ts +2 -0
  208. package/src/router/router-interfaces.ts +25 -2
  209. package/src/router/router-options.ts +8 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -12
  211. package/src/router/segment-resolution/loader-cache.ts +205 -45
  212. package/src/router/segment-resolution/loader-mask.ts +12 -22
  213. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  214. package/src/router/segment-resolution/mask-nested.ts +7 -7
  215. package/src/router/segment-resolution/revalidation.ts +34 -6
  216. package/src/router/segment-wrappers.ts +4 -0
  217. package/src/rsc/render-pipeline.ts +3 -0
  218. package/src/rsc/response-route-handler.ts +11 -3
  219. package/src/rsc/rsc-rendering.ts +8 -45
  220. package/src/rsc/shell-capture-constants.ts +13 -0
  221. package/src/rsc/shell-capture.ts +111 -90
  222. package/src/rsc/ssr-setup.ts +14 -8
  223. package/src/rsc/types.ts +11 -0
  224. package/src/server/context.ts +88 -11
  225. package/src/server/cookie-store.ts +5 -4
  226. package/src/server/handle-store.ts +232 -7
  227. package/src/server/request-context.ts +35 -30
  228. package/src/ssr/index.tsx +40 -7
  229. package/src/static-handler.ts +4 -1
  230. package/src/testing/flight-runtime.d.ts +21 -0
  231. package/src/testing/generated-routes.ts +2 -2
  232. package/src/testing/internal/context.ts +1 -1
  233. package/src/testing/internal/flight-client-globals.ts +7 -5
  234. package/src/testing/render-route.tsx +85 -24
  235. package/src/testing/run-loader.ts +5 -1
  236. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  237. package/src/testing/vitest.ts +62 -1
  238. package/src/theme/ThemeScript.tsx +3 -3
  239. package/src/types/cache-types.ts +14 -10
  240. package/src/types/handler-context.ts +14 -0
  241. package/src/types/loader-types.ts +8 -6
  242. package/src/urls/response-types.ts +3 -23
  243. package/src/use-loader.tsx +12 -3
  244. package/src/vite/plugins/use-cache-transform.ts +118 -74
  245. package/src/vite/rango.ts +7 -3
@@ -8,6 +8,18 @@ argument-hint: [@slot-name] [route-to-intercept]
8
8
 
9
9
  Intercept routes render a different component during soft navigation (client-side) while preserving the background route. Hard navigation (direct URL) shows the full page.
10
10
 
11
+ `intercept(slot, routeName, handler, config?, use?)`:
12
+
13
+ - `slot` — the `@name` slot the intercept renders into; the layout places it
14
+ with `<ParallelOutlet name="@modal" />`.
15
+ - `routeName` — the target route. A dot prefix (`".product"`) resolves inside
16
+ the current `include()` scope; a bare name (`"product"`) is the global route
17
+ name. Declare the intercept in a layout that wraps the target route — the
18
+ router finds intercepts by walking up the target route's own layout chain.
19
+ - `handler` — the component or handler rendered in the slot.
20
+ - `config` — optional `{ when }` (see "Conditional Intercept"); pass the `use`
21
+ callback as the 4th argument when there is no config.
22
+
11
23
  ## Not this skill if…
12
24
 
13
25
  - You want a slot that ALWAYS renders alongside the page (sidebar, multi-column
@@ -60,7 +72,8 @@ export const urlpatterns = urls(({ path, layout, intercept, loader, loading }) =
60
72
 
61
73
  ## Intercept with Layout
62
74
 
63
- Wrap intercept content in a modal layout:
75
+ Wrap intercept content in a modal layout. The layout renders the intercept
76
+ content through `<Outlet />` (not a `children` prop):
64
77
 
65
78
  ```typescript
66
79
  intercept(
@@ -68,21 +81,29 @@ intercept(
68
81
  "product",
69
82
  <ProductModalContent />,
70
83
  () => [
71
- layout(<ModalWrapper />), // Wraps the modal content
84
+ layout(<ModalWrapper />), // Wraps the modal content via <Outlet />
72
85
  loader(ProductLoader),
73
86
  loading(<ProductModalSkeleton />),
74
87
  ]
75
88
  )
76
89
  ```
77
90
 
91
+ Only the first `layout()` in an intercept's `use()` is applied. It is modal
92
+ chrome only: a `layout(Chrome, () => [...])` with its own `use()` items throws.
93
+ Attach loaders and the rest to the intercept itself.
94
+
78
95
  ## Intercept Middleware
79
96
 
80
97
  Intercepts support their own middleware chain via the use callback. The full chain for an intercept request is:
81
98
 
82
99
  ```
83
- global mw (router.use) -> route mw (urls middleware()) -> intercept mw -> intercept handler -> intercept loaders
100
+ global mw (router.use) -> route mw (urls middleware()) -> intercept mw -> intercept loaders + handler
84
101
  ```
85
102
 
103
+ The intercept's loaders are started before its handler is called, so they run
104
+ in parallel with it. If intercept middleware returns a `Response` (e.g. a
105
+ redirect), the intercept stops there.
106
+
86
107
  ```typescript
87
108
  intercept(
88
109
  "@modal",
@@ -108,22 +129,55 @@ If an intercept depends on data established by an outer layout/handler,
108
129
  revalidate that outer segment too or reload/guard the data inside the
109
130
  intercept.
110
131
 
132
+ ### Revalidating an Intercept
133
+
134
+ An intercept has no segment-level `revalidate()`. Its handler and layout
135
+ re-render every time the intercept resolves live — each intercepted
136
+ navigation, and each action submitted while the modal is open. When the
137
+ segment cache or a prerendered intercept variant serves the modal, the stored
138
+ handler output is reused and only the loaders run. Each loader decides for
139
+ itself through its own `revalidate()`, with the normal loader defaults.
140
+
141
+ A `revalidate()` placed directly in the intercept's `use()` (or returned from
142
+ its handler's `.use`) throws at definition time. Attach it to the loader:
143
+
144
+ ```typescript
145
+ // Throws: revalidate() is not valid inside intercept("@modal", "product") use()
146
+ intercept("@modal", "product", <ProductModal />, () => [
147
+ revalidate(() => false),
148
+ loader(ProductLoader),
149
+ ]);
150
+
151
+ // Correct: the loader owns the decision
152
+ intercept("@modal", "product", <ProductModal />, () => [
153
+ loader(ProductLoader, () => [revalidate(() => false)]),
154
+ ]);
155
+ ```
156
+
111
157
  ### Revalidation Contracts for Intercept Dependencies
112
158
 
113
- Use named revalidation contracts on both the outer producer and the intercept
114
- consumer when they share `ctx.set()` data:
159
+ When an outer layout produces `ctx.set()` data that the intercept reads, the
160
+ producer is the side that needs a contract: outer layouts are skipped during
161
+ partial action revalidation unless they opt in. The intercept side needs none
162
+ by default — its handler re-renders on every action while the modal is open,
163
+ and its loaders re-run after every action. If you narrow an intercept loader
164
+ with its own `revalidate()`, compose the producer's contract into it so the
165
+ two stay in step:
115
166
 
116
167
  ```typescript
168
+ import type { Revalidate } from "@rangojs/router";
117
169
  import * as ProductActions from "./actions/product";
118
170
 
119
- export const revalidateProductShell = (ctx) =>
171
+ export const revalidateProductShell: Revalidate = (ctx) =>
120
172
  ctx.isAction(ProductActions) || undefined;
121
173
 
122
174
  layout(ProductLayout, () => [
123
175
  revalidate(revalidateProductShell), // producer reruns
124
176
  intercept("@modal", "product", <ProductModal />, () => [
125
- revalidate(revalidateProductShell), // consumer reruns
126
- loader(ProductLoader),
177
+ loader(ProductLoader, () => [
178
+ revalidate(revalidateProductShell), // reruns with the producer
179
+ revalidate(() => false), // otherwise keep the loaded data
180
+ ]),
127
181
  ]),
128
182
  ]);
129
183
  ```
@@ -143,17 +197,16 @@ export const revalidateProduct = () => [
143
197
  layout(ProductLayout, () => [
144
198
  revalidateProduct(),
145
199
  intercept("@modal", "product", <ProductModal />, () => [
146
- revalidateProduct(),
147
- loader(ProductLoader),
200
+ loader(ProductLoader, () => [revalidateProduct(), revalidate(() => false)]),
148
201
  ]),
149
202
  ]);
150
203
  ```
151
204
 
152
205
  ## Conditional Intercept with the `when` config
153
206
 
154
- Only intercept based on navigation context. `when` is the 4th argument
155
- (an `InterceptConfig` object); the other use-items go in the 5th-argument
156
- callback.
207
+ Only intercept based on navigation context. `when` goes in the config object
208
+ (4th argument); the use-items then move to the 5th-argument callback. The
209
+ config type is not exported — pass an object literal.
157
210
 
158
211
  ```typescript
159
212
  intercept(
@@ -168,9 +221,19 @@ intercept(
168
221
  )
169
222
  ```
170
223
 
171
- `when` is a match-time selector receiving `{ from, to, params, segments, ... }`.
224
+ `when` is a synchronous match-time selector. It receives `from` / `to` (URLs),
225
+ `fromRouteName` / `toRouteName` (named routes only), `params` (the target's),
226
+ `segments` (the client's current segment path and ids), `request`, and `env`.
172
227
  Pass an array of predicates for AND logic (all must return true). Omit `when`
173
- entirely and the intercept always activates.
228
+ entirely and the intercept always activates. `when` is not re-evaluated during
229
+ action revalidation, so an open modal stays open after an action.
230
+
231
+ ```typescript
232
+ // Intercept only when opened from the shop index
233
+ intercept("@modal", "product", <ProductModal />, {
234
+ when: ({ fromRouteName }) => fromRouteName === "index",
235
+ })
236
+ ```
174
237
 
175
238
  ```typescript
176
239
  intercept(
@@ -204,22 +267,84 @@ intercept(
204
267
  )
205
268
  ```
206
269
 
270
+ ## What an Intercept's `use()` Accepts
271
+
272
+ An intercept's `use()` (and its handler's `.use`) accepts:
273
+
274
+ - `loader()`, with its own `revalidate()` and `cache()` inside it
275
+ - `middleware()`
276
+ - `loading()`
277
+ - `layout(Chrome)`: the modal chrome, without `use()` items of its own
278
+ - `route`, the item type a `path()` call produces
279
+ - `transition()`
280
+
281
+ Everything else throws at definition time with a pointer to where it goes:
282
+ `revalidate()` onto the loader, `errorBoundary()` / `notFoundBoundary()` onto
283
+ the enclosing layout (see below), and `cache()` onto the target route.
284
+
285
+ ## Caching an Intercept
286
+
287
+ To cache a modal, put `cache()` on the target route (or a layout above it).
288
+ An intercept navigation is cached under that scope with its own `intercept:`
289
+ key, so it never collides with the full-page render of the same URL. For
290
+ finer control, use `"use cache"` in the intercept handler, or cache the
291
+ intercept's loader: `loader(ProductLoader, () => [cache()])`.
292
+
293
+ ## Errors in an Intercept
294
+
295
+ An intercept has no boundary of its own. When the intercept's handler or one
296
+ of its loaders throws, or calls `notFound()`, the modal slot renders the
297
+ `errorBoundary()` / `notFoundBoundary()` of the layout or path that declares the
298
+ intercept, or the nearest ancestor that has one. That holds when the declaring
299
+ layout has no routes of its own, too: the lookup continues at the layout that
300
+ holds it and that layout's ancestors. The intercepted route's own
301
+ boundaries are not consulted: the modal renders in the declaring layout's slot,
302
+ not in the route. On a handler error the modal layout and the background page
303
+ stay mounted, and the status is 500 / 404. A thrown `Response` (`redirect()`)
304
+ still short-circuits the navigation.
305
+
306
+ With `loading()`, an async handler streams after the response has started, so
307
+ its rejection is not caught on the server. Wrap the modal layout's `<Outlet />`
308
+ in a client error boundary.
309
+
310
+ An `errorBoundary()` or `notFoundBoundary()` placed directly in the intercept's
311
+ `use()` (or returned from its handler's `.use`) throws at definition time. Put
312
+ it on the enclosing layout:
313
+
314
+ ```typescript
315
+ // Throws: errorBoundary() is not valid inside intercept("@modal", "product") use()
316
+ intercept("@modal", "product", <ProductModal />, () => [
317
+ loader(ProductLoader),
318
+ errorBoundary(<ModalError />),
319
+ ]);
320
+
321
+ // Correct: the declaring layout's boundaries handle the intercept's handler and loaders
322
+ layout(<ShopLayout />, () => [
323
+ errorBoundary(<ShopError />),
324
+ notFoundBoundary(<ProductNotFound />),
325
+ intercept("@modal", "product", <ProductModal />, () => [
326
+ loader(ProductLoader),
327
+ ]),
328
+ ]);
329
+ ```
330
+
207
331
  ## Closing the Modal
208
332
 
209
333
  Use navigation to close:
210
334
 
211
335
  ```typescript
212
336
  "use client";
213
- import { useRouter } from "@rangojs/router/client";
337
+ import { Outlet, useRouter } from "@rangojs/router/client";
214
338
 
215
- function ModalWrapper({ children }) {
339
+ // Used as the intercept's layout: layout(<ModalWrapper />)
340
+ export function ModalWrapper() {
216
341
  const router = useRouter();
217
342
 
218
343
  return (
219
344
  <div className="modal-overlay" onClick={() => router.back()}>
220
345
  <div className="modal" onClick={(e) => e.stopPropagation()}>
221
346
  <button onClick={() => router.back()}>Close</button>
222
- {children}
347
+ <Outlet />
223
348
  </div>
224
349
  </div>
225
350
  );
@@ -305,10 +430,10 @@ segments.
305
430
  ## Complete Example
306
431
 
307
432
  ```typescript
308
- // components/ProductModal.tsx
433
+ // components/shop-layout.tsx
309
434
  import { Outlet, ParallelOutlet } from "@rangojs/router/client";
310
435
 
311
- function ShopLayout() {
436
+ export function ShopLayout() {
312
437
  return (
313
438
  <div className="shop">
314
439
  <ParallelOutlet name="@promoBanner" />
@@ -320,16 +445,23 @@ function ShopLayout() {
320
445
  );
321
446
  }
322
447
 
323
- function ModalWrapper({ children }) {
448
+ // components/modal-wrapper.tsx (see "Closing the Modal" for a closable version)
449
+ import { Outlet } from "@rangojs/router/client";
450
+
451
+ export function ModalWrapper() {
324
452
  return (
325
453
  <div className="modal-overlay">
326
- <div className="modal">{children}</div>
454
+ <div className="modal">
455
+ <Outlet />
456
+ </div>
327
457
  </div>
328
458
  );
329
459
  }
330
460
 
331
- // urls/shop.tsx
461
+ // urls/shop.tsx — mounted with include("/shop", shopPatterns, { name: "shop" })
332
462
  import { urls } from "@rangojs/router";
463
+ import { ShopLayout } from "../components/shop-layout";
464
+ import { ModalWrapper } from "../components/modal-wrapper";
333
465
 
334
466
  export const shopPatterns = urls(({
335
467
  path,
@@ -338,6 +470,7 @@ export const shopPatterns = urls(({
338
470
  intercept,
339
471
  loader,
340
472
  loading,
473
+ cache,
341
474
  }) => [
342
475
  layout(<ShopLayout />, () => [
343
476
  parallel({
@@ -347,7 +480,7 @@ export const shopPatterns = urls(({
347
480
  // Intercept product detail into modal
348
481
  intercept(
349
482
  "@modal",
350
- "product", // Route name (without prefix)
483
+ ".product", // dot-local: resolves to "shop.product" in this include
351
484
  <ProductModalContent />,
352
485
  { when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
353
486
  () => [
@@ -370,9 +503,11 @@ export const shopPatterns = urls(({
370
503
 
371
504
  ## Handler-attached `.use`
372
505
 
373
- Intercept handlers can carry their own middleware, loaders, loading state, error/notFound boundaries, and even nested `layout`/`route` defaults via `.use` — useful for self-contained modal components that travel with their own data and chrome. (Conditional activation is set via the `when` config on the mount-site `intercept()` call, not inside `.use`.)
506
+ Intercept handlers can carry their own middleware, loaders, loading state, and even nested `layout`/`route` defaults via `.use` — useful for self-contained modal components that travel with their own data and chrome. (Conditional activation is set via the `when` config on the mount-site `intercept()` call, not inside `.use`; boundaries go on the enclosing layout, see [Errors in an Intercept](#errors-in-an-intercept).)
374
507
 
375
508
  ```typescript
509
+ import { layout, loader, loading, type Handler } from "@rangojs/router";
510
+
376
511
  const QuickViewModal: Handler = async (ctx) => {
377
512
  const product = await ctx.use(ProductLoader);
378
513
  return <QuickView product={product} />;
@@ -380,7 +515,7 @@ const QuickViewModal: Handler = async (ctx) => {
380
515
  QuickViewModal.use = () => [
381
516
  loader(ProductLoader),
382
517
  loading(<QuickViewSkeleton />),
383
- layout(<ModalChrome />),
518
+ layout(<ModalChrome />), // ModalChrome renders <Outlet />
384
519
  ];
385
520
 
386
521
  intercept("@modal", "product", QuickViewModal);
@@ -6,7 +6,15 @@ argument-hint: [component]
6
6
 
7
7
  # Layouts with layout()
8
8
 
9
- Layouts wrap child routes and persist during navigation within their scope.
9
+ `layout(handler, use?)` wraps its child routes in shared UI (nav, sidebar,
10
+ shell). The child content renders where the layout places `<Outlet />`, and the
11
+ layout stays mounted while you navigate between the routes it wraps. This skill
12
+ covers layout forms, outlets, orphan layouts (a layout inside a `path()`), and
13
+ layout revalidation.
14
+
15
+ Related skills: `/parallel` (named slots rendered by a layout), `/intercept`
16
+ (modal slots), `/route` (the routes a layout wraps), `/router-setup` (the
17
+ `document` shell that renders `<html>`).
10
18
 
11
19
  ## Basic Layout
12
20
 
@@ -50,7 +58,12 @@ layout(ShopLayout, () => [path("/shop", ShopIndex, { name: "shop" })]);
50
58
 
51
59
  ### Handler with Context
52
60
 
61
+ A function layout receives the same handler context as a route (`ctx.params`,
62
+ `ctx.get()`, `ctx.set()`, `ctx.use()`):
63
+
53
64
  ```typescript
65
+ import { Breadcrumbs } from "@rangojs/router";
66
+
54
67
  layout((ctx) => {
55
68
  const push = ctx.use(Breadcrumbs);
56
69
  push({ label: "Shop", href: "/shop" });
@@ -97,7 +110,8 @@ layout(<ShopLayout />, () => [
97
110
 
98
111
  ## The Outlet Component
99
112
 
100
- `<Outlet />` renders child content. Import from `@rangojs/router/client`:
113
+ `<Outlet />` renders child content. Import from `@rangojs/router/client`
114
+ (it works in server and client components):
101
115
 
102
116
  ```typescript
103
117
  import { Outlet } from "@rangojs/router/client";
@@ -119,7 +133,9 @@ A layout's `transition()` config wraps the content that flows through `<Outlet /
119
133
 
120
134
  ## Named Outlets
121
135
 
122
- For parallel routes, use named outlets:
136
+ For parallel routes, use named outlets (`<Outlet name="@sidebar" />` is
137
+ equivalent to `<ParallelOutlet name="@sidebar" />`). Slot content is defined
138
+ with `parallel()` — see `/parallel`.
123
139
 
124
140
  ```typescript
125
141
  import { Outlet, ParallelOutlet } from "@rangojs/router/client";
@@ -143,40 +159,40 @@ function DashboardLayout() {
143
159
 
144
160
  ## Orphan Layout (inside route)
145
161
 
146
- A layout as a child of `path()` wraps the route content and can read
147
- data set by the route handler via `ctx.get()`. The handler always
148
- executes before its children.
149
-
150
- This is the recommended way to pass handler data downward, and it is
151
- safe under partial action revalidation with zero configuration: orphan
152
- layouts (and their parallels) belong to the route entry, and on an
153
- action the whole entry re-runs together by default — route segment,
154
- loaders, and `belongsToRoute` children all seed revalidate-true, with
155
- handler-first ordering preserved. Producer and consumer cannot desync
156
- unless you narrow one side with a predicate that returns a hard `false`
157
- (then put the same contract on both — see "Revalidation Contracts").
158
-
159
- Data from an **outer** handler or layout entry is the opposite case:
160
- outer entries do not revalidate on actions by default (parent-chain
161
- skip). If an orphan layout depends on data established above its own
162
- route entry, that outer segment must share a revalidation contract, or
163
- the orphan must guard/reload the data independently. See `/rango` →
162
+ A layout declared inside a `path()` (an "orphan" layout) wraps that route's
163
+ content and can read what the route handler stored with `ctx.set()`. The
164
+ handler always executes before its orphan layouts and their parallels.
165
+
166
+ This is the recommended way to pass handler data downward. It needs no
167
+ revalidation setup: orphan layouts and their parallels belong to the route
168
+ entry, so on an action the route segment, its loaders, and these children all
169
+ re-run together, handler first. They can only desync if you narrow one side
170
+ with a predicate that returns a hard `false` — then put the same contract on
171
+ both (see "Revalidation Contracts").
172
+
173
+ Data from an **outer** handler or layout entry is the opposite case: outer
174
+ entries are skipped on actions by default. If an orphan layout depends on data
175
+ set above its own route entry, give that outer segment a shared revalidation
176
+ contract, or have the orphan load/guard the data itself. See `/rango` →
164
177
  "Passing data down the tree" for the full safest-first ladder.
165
178
 
166
179
  ```typescript
180
+ import { createVar } from "@rangojs/router";
167
181
  import { Outlet, ParallelOutlet } from "@rangojs/router/client";
168
182
 
183
+ const CurrentProduct = createVar<Product>();
184
+
169
185
  urls(({ path, layout, parallel }) => [
170
- path("/product/:slug", (ctx) => {
186
+ path("/product/:slug", async (ctx) => {
171
187
  const product = await fetchProduct(ctx.params.slug);
172
- ctx.set("product", product);
188
+ ctx.set(CurrentProduct, product);
173
189
  return <ProductPage product={product} />;
174
190
  }, { name: "product" }, () => [
175
191
  layout((ctx) => {
176
- const product = ctx.get("product");
192
+ const product = ctx.get(CurrentProduct); // Product | undefined
177
193
  return (
178
194
  <div>
179
- <Breadcrumb name={product?.name} />
195
+ <ProductHeader name={product?.name} />
180
196
  <Outlet />
181
197
  <ParallelOutlet name="@related" />
182
198
  </div>
@@ -184,7 +200,7 @@ urls(({ path, layout, parallel }) => [
184
200
  }, () => [
185
201
  parallel({
186
202
  "@related": (ctx) => {
187
- const product = ctx.get("product");
203
+ const product = ctx.get(CurrentProduct);
188
204
  return <RelatedProducts category={product?.category} />;
189
205
  },
190
206
  }),
@@ -193,36 +209,65 @@ urls(({ path, layout, parallel }) => [
193
209
  ])
194
210
  ```
195
211
 
196
- Orphan layouts can call `ctx.get()` to read data set by their parent
197
- handler. They can also call `ctx.set()`, though the primary pattern is
198
- for route handlers and middleware to write context variables and for
199
- orphan layouts to read them.
212
+ Orphan layouts can also call `ctx.set()` for their own children, but the usual
213
+ split is: middleware and route handlers write, orphan layouts and parallels
214
+ read.
215
+
216
+ Keep orphan layouts flat. A layout with no routes cannot contain another
217
+ `layout()`, directly or inside a `middleware(fn, () => [...])` wrapper;
218
+ `router.routes()` throws. Sibling layouts nest in declaration order, and a
219
+ sibling `middleware(fn)` runs for the whole route either way:
220
+
221
+ ```typescript
222
+ path("/account", AccountPage, { name: "account" }, () => [
223
+ middleware(requireAuth),
224
+ layout(<AccountShell />), // wraps AccountNav
225
+ layout(<AccountNav />), // wraps AccountPage
226
+ ]);
227
+ ```
228
+
229
+ A `cache()` inside an orphan layout caches the whole path, like a `cache()`
230
+ among the path's children (`/caching`).
200
231
 
201
232
  ## Layout Revalidation
202
233
 
203
- Standalone `layout()` entries don't revalidate by default — on an action,
204
- parent-chain segments are skipped unless a `revalidate()` opts them in.
205
- (Orphan layouts inside a `path()` are the opposite: they ride along with
206
- the route entry by default.) Control with `revalidate()`:
234
+ A standalone `layout()` renders when it is first mounted and is then kept as-is:
207
235
 
208
- ```typescript
209
- layout(<ShopLayout />, () => [
210
- // Never revalidate (default behavior)
211
- revalidate(() => false),
236
+ - **Navigation** between routes it already wraps does not re-render it — even
237
+ when the URL params change.
238
+ - **Actions** skip it too (the revalidation trace calls this
239
+ `action:parent-chain-skip`).
212
240
 
213
- path("/shop", ShopIndex, { name: "shop" }),
214
- ])
241
+ Orphan layouts inside a `path()` are the opposite: they belong to the route
242
+ entry and re-render with it (on param/search changes and on every action).
243
+
244
+ Opt a standalone layout in with `revalidate()`. Return `true` to re-render,
245
+ or `undefined` to defer to the default:
215
246
 
216
- // Or revalidate based on conditions
247
+ ```typescript
217
248
  import * as CartActions from "./actions/cart";
218
249
 
250
+ // Re-render after any cart action
219
251
  layout(<CartLayout />, () => [
220
252
  revalidate((ctx) => ctx.isAction(CartActions) || undefined),
221
253
 
222
254
  path("/cart", CartPage, { name: "cart" }),
223
255
  ])
256
+
257
+ // Re-render when a param the layout displays changes
258
+ layout(LocaleLayout, () => [
259
+ revalidate(({ currentParams, nextParams }) =>
260
+ currentParams.locale !== nextParams.locale || undefined,
261
+ ),
262
+
263
+ path("/:locale/shop", ShopIndex, { name: "shop" }),
264
+ ])
224
265
  ```
225
266
 
267
+ `revalidate(() => false)` on a standalone layout only restates the default.
268
+ The return shapes (hard boolean, soft `{ defaultShouldRevalidate }`, or defer)
269
+ are covered in `/loader` → "`revalidate()` return shapes".
270
+
226
271
  If child segments read data that was established by this layout or by a
227
272
  route handler above them, revalidate the outer segment too. Partial
228
273
  revalidation does not re-run non-revalidated ancestors just to rebuild
@@ -240,9 +285,11 @@ consumer segments:
240
285
 
241
286
  ```typescript
242
287
  // revalidation-contracts.ts
288
+ import type { Revalidate } from "@rangojs/router";
243
289
  import { addToCart } from "./actions/cart";
244
290
 
245
- export const revalidateCartData = (ctx) => ctx.isAction(addToCart) || undefined;
291
+ export const revalidateCartData: Revalidate = (ctx) =>
292
+ ctx.isAction(addToCart) || undefined;
246
293
  ```
247
294
 
248
295
  ```typescript
@@ -254,6 +301,10 @@ layout(<CartLayout />, () => [
254
301
  ]);
255
302
  ```
256
303
 
304
+ On the layout the contract adds the re-render the default skips after an
305
+ action. The route segment already re-runs after every action, so there it
306
+ changes nothing by default; the shared name documents the dependency.
307
+
257
308
  If a segment depends on multiple upstream domains, compose multiple
258
309
  contracts (`revalidateAuthData`, `revalidateCartData`, and so on).
259
310
 
@@ -261,10 +312,10 @@ You can also package them as importable handoff helpers:
261
312
 
262
313
  ```typescript
263
314
  // revalidation-contracts.ts
264
- import { revalidate } from "@rangojs/router";
315
+ import { revalidate, type Revalidate } from "@rangojs/router";
265
316
  import * as AuthActions from "./actions/auth";
266
317
 
267
- export const revalidateAuthData = (ctx) =>
318
+ export const revalidateAuthData: Revalidate = (ctx) =>
268
319
  ctx.isAction(AuthActions) || undefined;
269
320
  export const revalidateAuth = () => [revalidate(revalidateAuthData)];
270
321
  ```
@@ -281,8 +332,8 @@ layout(<ShellLayout />, () => [
281
332
  ## Complete Example
282
333
 
283
334
  ```typescript
284
- import { urls } from "@rangojs/router";
285
- import { Outlet, ParallelOutlet } from "@rangojs/router/client";
335
+ import { urls, Breadcrumbs } from "@rangojs/router";
336
+ import { Link, Outlet, ParallelOutlet } from "@rangojs/router/client";
286
337
  import * as CartActions from "./actions/cart";
287
338
 
288
339
  function ShopLayout() {
@@ -290,8 +341,8 @@ function ShopLayout() {
290
341
  <div className="shop">
291
342
  <ParallelOutlet name="@promoBanner" />
292
343
  <nav>
293
- <a href="/shop">Home</a>
294
- <a href="/shop/cart">Cart</a>
344
+ <Link to="/shop">Home</Link>
345
+ <Link to="/shop/cart">Cart</Link>
295
346
  </nav>
296
347
  <div className="content">
297
348
  <aside>
@@ -311,9 +362,12 @@ export const shopPatterns = urls(({ path, layout, parallel, loader, revalidate }
311
362
  push({ label: "Shop", href: "/shop" });
312
363
  return <ShopLayout />;
313
364
  }, () => [
314
- // Layout loaders
365
+ // Layout loaders: after an action, re-run only for cart actions;
366
+ // on navigation, undefined keeps the default
315
367
  loader(CartLoader, () => [
316
- revalidate((ctx) => ctx.isAction(CartActions) || undefined),
368
+ revalidate((ctx) =>
369
+ ctx.isAction() ? ctx.isAction(CartActions) : undefined,
370
+ ),
317
371
  ]),
318
372
 
319
373
  // Parallel routes
@@ -335,6 +389,8 @@ export const shopPatterns = urls(({ path, layout, parallel, loader, revalidate }
335
389
  Layout handlers can carry their own middleware, default parallels, and includes via `.use` so a layout becomes a self-contained unit reusable across mount sites.
336
390
 
337
391
  ```typescript
392
+ import { middleware, parallel, type Handler } from "@rangojs/router";
393
+
338
394
  const AdminLayout: Handler = (ctx) => {
339
395
  const user = ctx.get(CurrentUser);
340
396
  return <Admin user={user} />;