@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
@@ -32,6 +32,12 @@ import { menuRoutes } from "./menu";
32
32
  export const urlpatterns = urls(({ include }) => [
33
33
  include("/:locale?", menuRoutes, { name: "menu" }),
34
34
  ]);
35
+
36
+ // menu.tsx
37
+ export const menuRoutes = urls(({ path }) => [
38
+ path("/", MenuIndex, { name: "index" }),
39
+ path("/c/:slug", MenuCategory, { name: "category" }),
40
+ ]);
35
41
  ```
36
42
 
37
43
  URLs that match:
@@ -59,7 +65,8 @@ URLs that match:
59
65
  Absent optionals are `undefined` (not `""`), so `??` coalesces correctly:
60
66
 
61
67
  ```typescript
62
- import { Handler } from "@rangojs/router";
68
+ import type { Handler } from "@rangojs/router";
69
+ import { resolveLocale } from "../lib/locale";
63
70
 
64
71
  export const MenuIndex: Handler<"menu.index"> = (ctx) => {
65
72
  // ctx.params.locale is `string | undefined`
@@ -87,9 +94,9 @@ import { cookies, headers } from "@rangojs/router";
87
94
 
88
95
  export const SUPPORTED_LOCALES = ["en", "gb", "fr"] as const;
89
96
  export type Locale = (typeof SUPPORTED_LOCALES)[number];
90
- const DEFAULT_LOCALE: Locale = "en";
97
+ export const DEFAULT_LOCALE: Locale = "en";
91
98
 
92
- const isSupported = (v: string): v is Locale =>
99
+ export const isSupported = (v: string): v is Locale =>
93
100
  (SUPPORTED_LOCALES as readonly string[]).includes(v);
94
101
 
95
102
  export function resolveLocale(ctx: {
@@ -104,24 +111,35 @@ export function resolveLocale(ctx: {
104
111
  const accept = headers().get("accept-language") ?? "";
105
112
  for (const tag of accept.split(",")) {
106
113
  const code = tag.split(";")[0].trim().split("-")[0];
107
- if (isSupported(code)) return code as Locale;
114
+ if (isSupported(code)) return code;
108
115
  }
109
116
  return DEFAULT_LOCALE;
110
117
  }
111
118
  ```
112
119
 
120
+ `cookies()` and `headers()` read the current request, so the helper works in
121
+ handlers, layouts, loaders, and middleware without passing the request in.
122
+
113
123
  If you want to redirect to the canonical URL when the resolved locale
114
124
  doesn't match the URL (e.g., user has `gb` cookie but visits `/`), do
115
- that in a global middleware so it covers actions too:
125
+ that in global middleware (`router.use()`). Global middleware runs before
126
+ route matching, and its `ctx.params` come from its own pattern — not from
127
+ `include("/:locale?")` — so read the locale segment from the pathname:
116
128
 
117
129
  ```typescript
130
+ // router.tsx
118
131
  import { redirect } from "@rangojs/router";
119
-
120
- router.use("/*", async (ctx, next) => {
121
- const fromUrl = ctx.params.locale;
122
- const resolved = resolveLocale(ctx);
123
- if (resolved !== DEFAULT_LOCALE && !fromUrl) {
124
- return redirect(`/${resolved}${ctx.url.pathname}`);
132
+ import { DEFAULT_LOCALE, isSupported, resolveLocale } from "./lib/locale";
133
+
134
+ router.use(async (ctx, next) => {
135
+ if (ctx.request.method !== "GET") return next(); // leave actions alone
136
+ const first = ctx.url.pathname.split("/")[1] ?? "";
137
+ if (!isSupported(first)) {
138
+ const resolved = resolveLocale({ params: {} }); // cookie → Accept-Language → default
139
+ if (resolved !== DEFAULT_LOCALE) {
140
+ const rest = ctx.url.pathname === "/" ? "" : ctx.url.pathname;
141
+ return redirect(`/${resolved}${rest}${ctx.url.search}`);
142
+ }
125
143
  }
126
144
  await next();
127
145
  });
@@ -138,9 +156,16 @@ ctx.reverse("menu.index", { locale: "" }); // → "/"
138
156
  ctx.reverse("menu.index", { locale: undefined }); // → "/"
139
157
  ctx.reverse("menu.index", { locale: "en" }); // → "/en"
140
158
  ctx.reverse("menu.category", { locale: "en", slug: "breads" }); // → "/en/c/breads"
141
- ctx.reverse("menu.category", { slug: "breads" }); // → "/c/breads"
159
+ ctx.reverse("menu.category", { slug: "breads" }); // → "/c/breads" on an unprefixed request
142
160
  ```
143
161
 
162
+ `ctx.reverse()` auto-fills missing params from the current request, so the
163
+ last call returns `/en/c/breads` when the current URL is `/en/...`. That is
164
+ what you want for in-locale links; to leave the locale, pass it explicitly
165
+ (`{ locale: "fr" }`, or `undefined`/`""` for the bare default). Middleware and
166
+ response routes are the exception: their `ctx.reverse` fills nothing from the
167
+ request, so pass `locale` there explicitly.
168
+
144
169
  If the active locale is the app default and your URL strategy hides it
145
170
  (`"en"` → `/`, others → `/<locale>`), normalize before calling reverse:
146
171
 
@@ -177,28 +202,27 @@ export async function loadMessages(locale: Locale) {
177
202
 
178
203
  ### Server layout: hand off to the client provider
179
204
 
205
+ A layout handler receives the handler context (not props) and renders its
206
+ children with `<Outlet />`. It sees the matched route's params, including
207
+ `locale` from the include below it:
208
+
180
209
  ```tsx
181
- // layouts/intl-layout.tsx (server component)
182
- import type { ReactNode } from "react";
210
+ // layouts/intl-layout.tsx (server)
211
+ import type { Handler } from "@rangojs/router";
212
+ import { Outlet } from "@rangojs/router/client";
183
213
  import { resolveLocale } from "../lib/locale";
184
214
  import { loadMessages } from "../lib/messages";
185
215
  import { IntlClientProvider } from "../components/intl-client-provider";
186
216
 
187
- export async function IntlLayout({
188
- ctx,
189
- children,
190
- }: {
191
- ctx: any;
192
- children: ReactNode;
193
- }) {
217
+ export const IntlLayout: Handler = async (ctx) => {
194
218
  const locale = resolveLocale(ctx);
195
219
  const messages = await loadMessages(locale);
196
220
  return (
197
221
  <IntlClientProvider locale={locale} messages={messages}>
198
- {children}
222
+ <Outlet />
199
223
  </IntlClientProvider>
200
224
  );
201
- }
225
+ };
202
226
  ```
203
227
 
204
228
  ### Client provider
@@ -252,20 +276,22 @@ export const urlpatterns = urls(({ layout, include }) => [
252
276
  ```
253
277
 
254
278
  `<FormattedMessage>`, `useIntl()`, etc. work in any client component
255
- under the layout. Server components can use `formatjs`'s `createIntl()`
256
- directly with the same `messages` map for static text.
279
+ under the layout. Server components and handlers can't use React context
280
+ providers from the client; call `createIntl({ locale, messages })` (from
281
+ `@formatjs/intl` or `react-intl`) directly with the same `messages` map for
282
+ server-rendered text.
257
283
 
258
284
  ## Common Pitfalls
259
285
 
260
- | Pitfall | Fix |
261
- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
262
- | `ctx.params.locale === ""` returns `false` | Absent optionals are `undefined`, not `""`. Use `=== undefined` or `??`. |
263
- | `ctx.params.locale ?? "en"` returns `""` | Pre-fix behavior. After the include-prefix fix this works correctly. |
264
- | Bare `/` 404s when mounted via `include("/:locale?", routes)` | Requires the all-optional pattern fix in `compilePattern` (shipped). |
265
- | Unknown locale (e.g. `/de`) matches as `locale: "de"` | Add a constraint: `:locale(en\|gb\|fr)?`. Unknown values now 404. |
266
- | Reverse produces `//c/breads` for absent locale | `reverse()` collapses `undefined`/`""` segments — should not happen. File a bug. |
267
- | Locale switcher loses search params | Read `ctx.url.search` and pass to `reverse(..., undefined, parsedSearch)`. |
268
- | Action middleware can't read `ctx.params.locale` | Route middleware doesn't wrap action execution. Use global `router.use()` for actions. |
286
+ | Pitfall | Fix |
287
+ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
288
+ | `ctx.params.locale === ""` returns `false` | Absent optionals are `undefined`, not `""`. Use `=== undefined` or `??`. |
289
+ | Unknown locale (e.g. `/de`) matches as `locale: "de"` | Add a constraint: `:locale(en\|gb\|fr)?`. Unknown values now 404. |
290
+ | Link meant for the default locale keeps `/en` | `ctx.reverse()` auto-fills the current `locale`. Pass `{ locale: undefined }` (or `""`) to drop it. |
291
+ | Reverse produces `//c/breads` for absent locale | `reverse()` collapses `undefined`/`""` segments — should not happen. File a bug. |
292
+ | Locale switcher loses search params | Append `ctx.url.search` to the reversed URL, or pass `Object.fromEntries(ctx.searchParams)` as the third argument. |
293
+ | Global middleware's `ctx.params.locale` is always `undefined` | Global middleware params come from its own pattern. Parse the first path segment (see "Locale Resolution"). |
294
+ | Route middleware doesn't see actions | Route middleware doesn't wrap action execution. Use global `router.use()` for action-time locale logic. |
269
295
 
270
296
  ## Cross-references
271
297
 
@@ -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);