@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
@@ -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} />;
@@ -6,9 +6,12 @@ argument-hint: [ctx.reverse|href|useHref|useMount|useReverse|scopedReverse]
6
6
 
7
7
  # Links & URL Generation
8
8
 
9
- @rangojs/router provides different href APIs for server and client contexts.
9
+ Use this skill to generate URLs to your own routes by name (server) or by
10
+ typed path (client), and to keep links correct when a module is mounted under
11
+ a different `include()` prefix. For route typing setup see `/typesafety`; for
12
+ links to JSON/text endpoints see `/response-routes`.
10
13
 
11
- **Default server API: `ctx.reverse()`.** Generate URLs from the handler context — it's typed, auto-fills mount params, and resolves local (`.name`) and absolute (`name.sub`) names.
14
+ **Default server API: `ctx.reverse()`.** Generate URLs from the handler context — it's typed, auto-fills params from the current request, and resolves local (`.name`) and global (`name`) names. In middleware and response routes it takes global names only (see below).
12
15
 
13
16
  **On the client, two patterns:**
14
17
 
@@ -19,10 +22,19 @@ argument-hint: [ctx.reverse|href|useHref|useMount|useReverse|scopedReverse]
19
22
 
20
23
  ## Server: ctx.reverse()
21
24
 
22
- Available in route handlers via HandlerContext. Resolves named routes using the full route map. This is the default way to generate URLs on the server.
25
+ Available on the handler context (route handlers, layouts, parallels), on
26
+ middleware and loader contexts, and on `getRequestContext()` inside server
27
+ actions. Resolves named routes using the full route map. This is the default
28
+ way to generate URLs on the server. Patterns in the map already include the
29
+ router `basename`, so results are basename-prefixed.
30
+
31
+ Middleware and response routes (`path.json()`, `path.text()`, …) get a
32
+ global-only `ctx.reverse` (`GlobalReverseFunction`): no `include()` scope and no
33
+ param auto-fill, so pass the fully qualified name and every param. A dot-local
34
+ `.name` is a type error there. See `/middleware` and `/response-routes`.
23
35
 
24
36
  ```typescript
25
- import { urls, scopedReverse } from "@rangojs/router";
37
+ import { urls } from "@rangojs/router";
26
38
 
27
39
  export const shopPatterns = urls(({ path, layout }) => [
28
40
  layout(<ShopLayout />, () => [
@@ -63,7 +75,7 @@ path("/product/:slug", (ctx) => {
63
75
 
64
76
  ### Local names (dot-prefixed)
65
77
 
66
- Prefix a name with `.` to resolve it within the current `include()` scope. The route is looked up using the include's mount namespace.
78
+ Prefix a name with `.` to resolve it within the current `include()` scope. The route is looked up using the include's mount namespace. If the name is not found there, lookup walks up the enclosing include scopes (`shop.admin.x`, then `shop.x`). It falls back to a bare root-level name only for root-scoped routes (top-level routes and routes under `{ name: "" }` mounts); inside a named mount, a dot-local name never leaks into unrelated global names.
67
79
 
68
80
  ```typescript
69
81
  // urls/magazine.tsx — mounted at include("/magazine", magazinePatterns, { name: "magazine" })
@@ -97,7 +109,7 @@ export const tenantPatterns = urls(({ path }) => [
97
109
  ]);
98
110
  ```
99
111
 
100
- Auto-fill uses `{ ...ctx.params, ...hrefParams }` — current request params are defaults, explicit params win. Params not needed by the target route are silently ignored.
112
+ Auto-fill uses `{ ...ctx.params, ...hrefParams }` — current request params are defaults, explicit params win. Params not needed by the target route are silently ignored. Because every current param is a default, drop an optional one explicitly with `undefined` or `""` — under `include("/:locale?", ...)` on `/en/...`, a bare `ctx.reverse("menu.index")` keeps `/en`, while `ctx.reverse("menu.index", { locale: undefined })` returns `/` (see `/i18n`). A missing required param throws `Missing param "slug" for route "…"`; an unknown name throws `Unknown route: "…"`.
101
113
 
102
114
  ### Global names (unprefixed)
103
115
 
@@ -112,7 +124,7 @@ Unprefixed names resolve against the full named-routes map (the generated `route
112
124
 
113
125
  ### reverse with search params
114
126
 
115
- When a route has a `search` schema, pass a typed search object as the third argument:
127
+ When a route has a `search` schema, pass a typed search object as the third argument. `undefined`/`null` values are skipped; keys keep insertion order:
116
128
 
117
129
  ```typescript
118
130
  path("/search", (ctx) => {
@@ -124,7 +136,7 @@ path("/search", (ctx) => {
124
136
 
125
137
  ### scopedReverse() - type-safe ctx.reverse
126
138
 
127
- Wraps `ctx.reverse` with local route type information for autocomplete and validation. Runtime behavior is identical to `ctx.reverse` — `scopedReverse` is a type-only cast. The same dot-prefix rule applies: local names use `.name`, global names use `name.sub`.
139
+ Wraps `ctx.reverse` with the module's local route types for autocomplete and validation. Runtime behavior is identical to `ctx.reverse` — `scopedReverse` is a type-only cast, and it also works on loader contexts. Don't use it on middleware or response-route contexts: their `ctx.reverse` has no `include()` scope, so the local names it types would throw `Unknown route`. Always call local names with the dot: the returned function is typed only against the module's local map, so an unprefixed local name (`reverse("cart")`) type-checks but is resolved as a global name at runtime — it throws `Unknown route` unless a global route of that exact name happens to exist.
128
140
 
129
141
  ```typescript
130
142
  import { scopedReverse } from "@rangojs/router";
@@ -134,12 +146,15 @@ path("/product/:slug", (ctx) => {
134
146
 
135
147
  reverse(".cart"); // Local name (dot-prefixed) — resolves in include scope
136
148
  reverse(".product", { slug: "widget" }); // Local name with params
137
- reverse("blog.post", { slug: "hi" }); // Global name (dotted) — full route map
149
+
150
+ ctx.reverse("blog.post", { slug: "hi" }); // Global names: call ctx.reverse directly
138
151
 
139
152
  return <ProductPage slug={ctx.params.slug} />;
140
153
  }, { name: "product" })
141
154
  ```
142
155
 
156
+ To type local and global names on one function, type the handler with the module's generated map instead: `Handler<"shop.product", routes>` (see `/typesafety`).
157
+
143
158
  `reverse()` does not accept raw path strings (`"/about"`). For static paths in client components, use `href("/about")`; on the server, look up the route by name.
144
159
 
145
160
  ## Client components: receive URLs as props
@@ -150,9 +165,12 @@ path("/product/:slug", (ctx) => {
150
165
 
151
166
  ```tsx
152
167
  // server
153
- function BlogPostPage(ctx: HandlerContext) {
168
+ import type { Handler } from "@rangojs/router";
169
+ import { ShareButton } from "./share-button";
170
+
171
+ export const BlogPostPage: Handler<{ slug: string }> = (ctx) => {
154
172
  return <ShareButton url={ctx.reverse(".post", { slug: ctx.params.slug })} />;
155
- }
173
+ };
156
174
  ```
157
175
 
158
176
  ```tsx
@@ -169,6 +187,8 @@ export function ShareButton({ url }: { url: string }) {
169
187
 
170
188
  ```tsx
171
189
  // server — loaders/nav.ts
190
+ import { createLoader } from "@rangojs/router";
191
+
172
192
  export const NavLoader = createLoader((ctx) => ({
173
193
  home: ctx.reverse("home"),
174
194
  blog: ctx.reverse("blog.index"),
@@ -182,6 +202,8 @@ const urlpatterns = urls(({ path, loader }) => [
182
202
 
183
203
  ```tsx
184
204
  "use client";
205
+ import { Link, useLoader } from "@rangojs/router/client";
206
+ import { NavLoader } from "../loaders/nav";
185
207
 
186
208
  function Nav() {
187
209
  const { data } = useLoader(NavLoader);
@@ -195,6 +217,7 @@ function Nav() {
195
217
 
196
218
  ```tsx
197
219
  "use server";
220
+ import { getRequestContext } from "@rangojs/router";
198
221
 
199
222
  export async function getProductUrl(slug: string) {
200
223
  const ctx = getRequestContext();
@@ -237,7 +260,9 @@ import { href } from "@rangojs/router/client";
237
260
  export const appHref = (path: Rango.Path): string => href(path);
238
261
  ```
239
262
 
240
- `href()` is a raw path helper — it is **not** basename-aware. It returns the path as-is (or with the include mount prefix via `useHref()`). For basename-aware navigation, use `Link`, `useRouter().push()`, or `reverse()`, which auto-prefix root-relative paths with the router's basename.
263
+ `href()` is a raw path helper — it is **not** basename-aware. It returns the path as-is (or with the include mount prefix via `useHref()`). `<Link to={href("/about")}>` still lands on the basename-prefixed URL because `Link` and `useRouter().push()` prefix root-relative paths with the router's basename; a plain `<a href>`, `fetch()`, or `window.location` gets the raw path. On the server, `ctx.reverse()` output is already basename-prefixed.
264
+
265
+ `href.json()`, `href.text()`, `href.html()`, `href.xml()`, `href.md()`, `href.image()`, `href.stream()` and `href.any()` return `{ to, "data-external": "" }` props for linking to response routes with a hard navigation — see `/response-routes`.
241
266
 
242
267
  ## Client: useHref()
243
268
 
@@ -263,6 +288,8 @@ function ShopNav() {
263
288
 
264
289
  Use `useHref()` for local navigation within a mounted module. Use the bare `href()` function for absolute paths outside the current mount.
265
290
 
291
+ `useHref()` concatenates the mount **pattern** as-is and does not substitute params, and its argument is not checked against your routes. Under a parameterized mount (`include("/tenant/:tenantId", ...)`) it would produce `/tenant/:tenantId/cart` — use `useReverse(routes)` there, which substitutes mount params from `useParams()`.
292
+
266
293
  ## Client: useMount()
267
294
 
268
295
  Returns the current `include()` mount path. Useful for building custom logic based on mount location.
@@ -279,7 +306,7 @@ function MountInfo() {
279
306
  }
280
307
  ```
281
308
 
282
- `useMount()` reads from `MountContext`, which is automatically set by `include()` in the segment tree.
309
+ `useMount()` reads from `MountContext`, which is automatically set by `include()` in the segment tree. Nested includes return the nearest mount's full URL prefix (e.g. `/shop/admin`), and a parameterized include returns its pattern (`/tenant/:tenantId`), not the matched URL. `href(path, mount)` is the non-hook equivalent of `useHref()`.
283
310
 
284
311
  ## Client: useReverse(routes)
285
312
 
@@ -309,7 +336,9 @@ export function BlogNav() {
309
336
  1. Strips an optional leading `.` and looks up the name in the imported `routes` map.
310
337
  2. Joins the local pattern with the surrounding `useMount()` value — the include's URL pattern.
311
338
  3. Substitutes params: explicit params from the call, then auto-filled from `useParams()` for anything still unresolved (mount params like `:tenantId` flow in this way).
312
- 4. Appends a query string if a search object is passed and the route has a `search` schema.
339
+ 4. Appends a query string when a search object is passed (typed from the route's `search` schema; `undefined`/`null` values are skipped).
340
+
341
+ The returned function is memoized on the routes map, mount, and current params, so it is safe to pass to memoized children.
313
342
 
314
343
  ### Mount-relativity
315
344
 
@@ -337,13 +366,16 @@ The `/` pattern under a non-root mount collapses cleanly: under `/news`, `revers
337
366
  When the include itself carries `:params`, those are auto-filled from `useParams()` so the caller doesn't have to thread them through:
338
367
 
339
368
  ```typescript
369
+ // urls/tenant.tsx: index "/" and post "/posts/:postId"
340
370
  // urls.tsx
341
- include("/tenant/:tenantId", clientReversePatterns, { name: "tenant" });
371
+ include("/tenant/:tenantId", tenantPatterns, { name: "tenant" });
342
372
  ```
343
373
 
344
374
  ```tsx
375
+ import { routes as tenantRoutes } from "../urls/tenant.gen.js";
376
+
345
377
  // At /tenant/acme/posts/p1, useParams() = { tenantId: "acme", postId: "p1" }
346
- const reverse = useReverse(clientReverseRoutes);
378
+ const reverse = useReverse(tenantRoutes);
347
379
 
348
380
  reverse(".index"); // "/tenant/acme"
349
381
  reverse(".post", { postId: "p2" }); // "/tenant/acme/posts/p2" (tenantId auto-filled)
@@ -411,7 +443,7 @@ pnpm exec rango generate src/urls --static
411
443
 
412
444
  Don't edit the file by hand — re-run codegen when patterns change.
413
445
 
414
- **Today the Vite plugin only regenerates the router-level `*.named-routes.gen.ts`.** Per-module `urls/*.gen.ts` files are emitted only by the CLI (or `writePerModuleRouteTypesForFile` programmatically). Commit the generated files and re-run `rango generate` whenever a `urls()` module's `path()`/`include()` shape changes. A common workflow is to wire it into a `predev` script:
446
+ **The Vite plugin only regenerates the router-level `*.named-routes.gen.ts`.** Per-module `urls/*.gen.ts` files are emitted only by the `rango generate` CLI. Commit the generated files and re-run `rango generate` whenever a `urls()` module's `path()`/`include()` shape changes. `clientUrls()` modules get a `.gen.ts` the same way. A common workflow is to wire it into a `predev` script:
415
447
 
416
448
  ```jsonc
417
449
  // package.json
@@ -452,7 +484,7 @@ queued or in flight; removing it re-arms both Link types.
452
484
  | Context | API | Resolves | Use for |
453
485
  | ---------------- | -------------------------------------------------- | ----------------------------------------- | ---------------------------------------------------------------- |
454
486
  | Server handler | `ctx.reverse("name")` | Named routes (local + absolute) | **Default** server-side URL generation |
455
- | Server handler | `scopedReverse<T>(ctx.reverse)` | Same, with type safety | Type-safe server URLs |
487
+ | Server handler | `scopedReverse<T>(ctx.reverse)` | Local `.name` routes, typed from `T` | Type-safe in-module server URLs |
456
488
  | Client component | `useReverse(routes)` | Local names from an imported `routes` map | Typed in-module URL generation without round-tripping the server |
457
489
  | Client component | (URL passed as prop / loader data / action return) | Named routes | Cross-module URLs or one-off names you don't want to import |
458
490
  | Client component | `href("/path")` | Absolute paths (static strings) | Static navigation where no named-route lookup is needed |
@@ -470,7 +502,7 @@ import { urls, scopedReverse } from "@rangojs/router";
470
502
  export const shopPatterns = urls(({ path, layout }) => [
471
503
  layout((ctx) => {
472
504
  const reverse = scopedReverse<typeof shopPatterns>(ctx.reverse);
473
- return <ShopLayout cartUrl={reverse("cart")} />;
505
+ return <ShopLayout cartUrl={reverse(".cart")} />;
474
506
  }, () => [
475
507
  path("/", ShopIndex, { name: "index" }),
476
508
  path("/cart", CartPage, { name: "cart" }),