@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2

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 (222) hide show
  1. package/AGENTS.md +6 -10
  2. package/README.md +289 -938
  3. package/dist/bin/rango.js +271 -46
  4. package/dist/vite/index.js +673 -193
  5. package/package.json +10 -8
  6. package/skills/api-client/SKILL.md +1 -1
  7. package/skills/breadcrumbs/SKILL.md +31 -14
  8. package/skills/cache-guide/SKILL.md +5 -2
  9. package/skills/caching/SKILL.md +59 -4
  10. package/skills/catalog.json +271 -0
  11. package/skills/comparison/SKILL.md +50 -0
  12. package/skills/comparison/agents/openai.yaml +4 -0
  13. package/skills/comparison/references/framework-comparison.md +837 -0
  14. package/skills/composability/SKILL.md +83 -2
  15. package/skills/debug-manifest/SKILL.md +1 -1
  16. package/skills/defer-hydration/SKILL.md +235 -0
  17. package/skills/document-cache/SKILL.md +9 -1
  18. package/skills/fonts/SKILL.md +1 -1
  19. package/skills/handler-use/SKILL.md +8 -8
  20. package/skills/hooks/SKILL.md +54 -892
  21. package/skills/hooks/data.md +273 -0
  22. package/skills/hooks/handle-and-actions.md +103 -0
  23. package/skills/hooks/navigation.md +110 -0
  24. package/skills/hooks/outlets.md +41 -0
  25. package/skills/hooks/state.md +228 -0
  26. package/skills/hooks/urls.md +135 -0
  27. package/skills/host-router/SKILL.md +4 -4
  28. package/skills/i18n/SKILL.md +1 -1
  29. package/skills/intercept/SKILL.md +46 -14
  30. package/skills/layout/SKILL.md +27 -10
  31. package/skills/links/SKILL.md +1 -1
  32. package/skills/loader/SKILL.md +23 -1
  33. package/skills/middleware/SKILL.md +7 -3
  34. package/skills/migrate-nextjs/SKILL.md +167 -6
  35. package/skills/migrate-react-router/SKILL.md +59 -677
  36. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  37. package/skills/migrate-react-router/component-migration.md +196 -0
  38. package/skills/migrate-react-router/data-and-actions.md +225 -0
  39. package/skills/migrate-react-router/route-mapping.md +271 -0
  40. package/skills/mime-routes/SKILL.md +1 -1
  41. package/skills/observability/SKILL.md +9 -1
  42. package/skills/parallel/SKILL.md +23 -4
  43. package/skills/ppr/SKILL.md +622 -0
  44. package/skills/prerender/SKILL.md +28 -18
  45. package/skills/rango/SKILL.md +84 -25
  46. package/skills/response-routes/SKILL.md +15 -1
  47. package/skills/route/SKILL.md +71 -4
  48. package/skills/router-setup/SKILL.md +14 -3
  49. package/skills/scripts/SKILL.md +1 -1
  50. package/skills/server-actions/SKILL.md +3 -2
  51. package/skills/shell-manifest/SKILL.md +185 -0
  52. package/skills/streams-and-websockets/SKILL.md +1 -1
  53. package/skills/tailwind/SKILL.md +1 -1
  54. package/skills/testing/SKILL.md +2 -1
  55. package/skills/testing/handles.md +4 -2
  56. package/skills/testing/render-handler.md +15 -14
  57. package/skills/testing/reverse-and-types.md +8 -7
  58. package/skills/theme/SKILL.md +1 -1
  59. package/skills/typesafety/SKILL.md +45 -919
  60. package/skills/typesafety/env-and-bindings.md +254 -0
  61. package/skills/typesafety/generated-files-and-cli.md +335 -0
  62. package/skills/typesafety/params-and-search.md +153 -0
  63. package/skills/typesafety/route-types.md +209 -0
  64. package/skills/use-cache/SKILL.md +30 -3
  65. package/skills/vercel/SKILL.md +1 -1
  66. package/skills/view-transitions/SKILL.md +44 -1
  67. package/src/browser/event-controller.ts +62 -10
  68. package/src/browser/logging.ts +28 -0
  69. package/src/browser/merge-segment-loaders.ts +6 -4
  70. package/src/browser/navigation-bridge.ts +65 -16
  71. package/src/browser/navigation-client.ts +32 -2
  72. package/src/browser/navigation-store.ts +128 -14
  73. package/src/browser/network-error-handler.ts +34 -7
  74. package/src/browser/partial-update.ts +76 -17
  75. package/src/browser/prefetch/cache.ts +51 -11
  76. package/src/browser/prefetch/fetch.ts +59 -21
  77. package/src/browser/prefetch/queue.ts +19 -4
  78. package/src/browser/react/Link.tsx +13 -3
  79. package/src/browser/react/NavigationProvider.tsx +108 -4
  80. package/src/browser/response-adapter.ts +38 -9
  81. package/src/browser/rsc-router.tsx +54 -4
  82. package/src/browser/scroll-restoration.ts +7 -5
  83. package/src/browser/segment-reconciler.ts +31 -21
  84. package/src/browser/server-action-bridge.ts +22 -10
  85. package/src/browser/types.ts +54 -1
  86. package/src/build/generate-manifest.ts +155 -131
  87. package/src/build/index.ts +3 -1
  88. package/src/build/route-trie.ts +35 -7
  89. package/src/build/route-types/include-resolution.ts +347 -47
  90. package/src/build/runtime-discovery.ts +4 -1
  91. package/src/cache/cache-key-utils.ts +29 -0
  92. package/src/cache/cache-runtime.ts +262 -71
  93. package/src/cache/cache-scope.ts +2 -17
  94. package/src/cache/cache-tag.ts +60 -14
  95. package/src/cache/cf/cf-cache-store.ts +243 -20
  96. package/src/cache/document-cache.ts +54 -21
  97. package/src/cache/index.ts +1 -0
  98. package/src/cache/memory-segment-store.ts +110 -3
  99. package/src/cache/profile-registry.ts +15 -0
  100. package/src/cache/read-through-swr.ts +15 -1
  101. package/src/cache/segment-codec.ts +4 -4
  102. package/src/cache/shell-snapshot.ts +417 -0
  103. package/src/cache/types.ts +158 -0
  104. package/src/cache/vercel/vercel-cache-store.ts +401 -124
  105. package/src/client.rsc.tsx +0 -3
  106. package/src/client.tsx +0 -3
  107. package/src/cloudflare/tracing.ts +7 -8
  108. package/src/defer.ts +11 -22
  109. package/src/handle.ts +37 -15
  110. package/src/handles/MetaTags.tsx +16 -82
  111. package/src/handles/breadcrumbs.ts +12 -14
  112. package/src/handles/deferred-resolution.ts +127 -0
  113. package/src/handles/is-thenable.ts +7 -8
  114. package/src/handles/meta.ts +7 -44
  115. package/src/host/errors.ts +15 -0
  116. package/src/host/index.ts +1 -0
  117. package/src/index.rsc.ts +8 -2
  118. package/src/index.ts +19 -13
  119. package/src/internal-debug.ts +11 -8
  120. package/src/prerender.ts +17 -4
  121. package/src/redirect-origin.ts +14 -0
  122. package/src/render-error-thrower.tsx +20 -0
  123. package/src/route-content-wrapper.tsx +12 -5
  124. package/src/route-definition/dsl-helpers.ts +21 -32
  125. package/src/route-definition/helper-factories.ts +0 -2
  126. package/src/route-definition/helpers-types.ts +43 -43
  127. package/src/route-definition/index.ts +1 -2
  128. package/src/route-definition/resolve-handler-use.ts +0 -1
  129. package/src/route-definition/use-item-types.ts +3 -6
  130. package/src/route-map-builder.ts +41 -4
  131. package/src/route-types.ts +0 -5
  132. package/src/router/find-match.ts +86 -8
  133. package/src/router/instrument.ts +9 -4
  134. package/src/router/lazy-includes.ts +72 -12
  135. package/src/router/loader-resolution.ts +14 -2
  136. package/src/router/manifest.ts +56 -11
  137. package/src/router/match-api.ts +76 -32
  138. package/src/router/match-handlers.ts +181 -135
  139. package/src/router/match-middleware/background-revalidation.ts +40 -23
  140. package/src/router/match-middleware/cache-store.ts +39 -24
  141. package/src/router/match-result.ts +35 -15
  142. package/src/router/middleware.ts +64 -38
  143. package/src/router/navigation-snapshot.ts +7 -5
  144. package/src/router/parse-pattern.ts +115 -0
  145. package/src/router/pattern-matching.ts +53 -64
  146. package/src/router/prefetch-limits.ts +37 -0
  147. package/src/router/prerender-match.ts +11 -5
  148. package/src/router/preview-match.ts +3 -1
  149. package/src/router/request-classification.ts +23 -8
  150. package/src/router/route-snapshot.ts +14 -2
  151. package/src/router/router-context.ts +3 -1
  152. package/src/router/router-interfaces.ts +32 -1
  153. package/src/router/router-options.ts +30 -0
  154. package/src/router/segment-resolution/fresh.ts +39 -3
  155. package/src/router/segment-resolution/loader-cache.ts +93 -2
  156. package/src/router/segment-resolution/loader-mask.ts +60 -0
  157. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  158. package/src/router/segment-resolution/mask-nested.ts +83 -0
  159. package/src/router/segment-resolution/revalidation.ts +3 -0
  160. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  161. package/src/router/substitute-pattern-params.ts +54 -35
  162. package/src/router/telemetry-otel.ts +6 -8
  163. package/src/router/telemetry.ts +9 -1
  164. package/src/router/tracing.ts +14 -5
  165. package/src/router/trie-matching.ts +19 -11
  166. package/src/router/url-params.ts +13 -0
  167. package/src/router.ts +47 -16
  168. package/src/rsc/full-payload.ts +70 -0
  169. package/src/rsc/handler.ts +60 -33
  170. package/src/rsc/manifest-init.ts +1 -1
  171. package/src/rsc/nonce.ts +10 -1
  172. package/src/rsc/progressive-enhancement.ts +61 -4
  173. package/src/rsc/redirect-guard.ts +2 -1
  174. package/src/rsc/rsc-rendering.ts +429 -37
  175. package/src/rsc/server-action.ts +25 -2
  176. package/src/rsc/shell-capture.ts +1190 -0
  177. package/src/rsc/shell-serve.ts +181 -0
  178. package/src/rsc/transition-gate.ts +89 -0
  179. package/src/rsc/types.ts +30 -0
  180. package/src/segment-loader-promise.ts +18 -0
  181. package/src/segment-system.tsx +149 -14
  182. package/src/server/context.ts +67 -9
  183. package/src/server/cookie-store.ts +73 -1
  184. package/src/server/loader-registry.ts +13 -1
  185. package/src/server/request-context.ts +169 -10
  186. package/src/ssr/index.tsx +462 -178
  187. package/src/ssr/inject-rsc-eager.ts +167 -0
  188. package/src/ssr/ssr-root.tsx +228 -0
  189. package/src/testing/collect-handle.ts +14 -8
  190. package/src/testing/dispatch.ts +152 -40
  191. package/src/testing/generated-routes.ts +27 -11
  192. package/src/testing/index.ts +6 -0
  193. package/src/testing/render-handler.ts +14 -0
  194. package/src/testing/render-route.tsx +13 -10
  195. package/src/testing/run-transition-when.ts +164 -0
  196. package/src/theme/ThemeProvider.tsx +36 -26
  197. package/src/types/handler-context.ts +1 -1
  198. package/src/types/index.ts +2 -0
  199. package/src/types/route-config.ts +19 -7
  200. package/src/types/segments.ts +100 -0
  201. package/src/urls/include-helper.ts +10 -8
  202. package/src/urls/include-provider.ts +71 -0
  203. package/src/urls/index.ts +1 -0
  204. package/src/urls/path-helper-types.ts +44 -12
  205. package/src/urls/path-helper.ts +5 -0
  206. package/src/urls/pattern-types.ts +36 -0
  207. package/src/urls/type-extraction.ts +43 -18
  208. package/src/urls/urls-function.ts +0 -1
  209. package/src/vercel/tracing.ts +7 -7
  210. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  211. package/src/vite/discovery/discover-routers.ts +1 -1
  212. package/src/vite/discovery/discovery-errors.ts +61 -0
  213. package/src/vite/index.ts +7 -0
  214. package/src/vite/inject-client-debug.ts +88 -0
  215. package/src/vite/plugins/vercel-output.ts +114 -25
  216. package/src/vite/plugins/version-injector.ts +22 -7
  217. package/src/vite/plugins/virtual-entries.ts +80 -22
  218. package/src/vite/rango.ts +29 -19
  219. package/src/vite/router-discovery.ts +171 -43
  220. package/src/vite/utils/prerender-utils.ts +17 -4
  221. package/src/vite/utils/shared-utils.ts +47 -0
  222. package/src/network-error-thrower.tsx +0 -18
@@ -0,0 +1,228 @@
1
+ # State and Cache Control Hooks
2
+
3
+ ## State Hooks
4
+
5
+ ### useLocationState()
6
+
7
+ Read type-safe state from history:
8
+
9
+ ```tsx
10
+ "use client";
11
+ import { useLocationState, createLocationState } from "@rangojs/router/client";
12
+
13
+ // Define typed state (all export patterns supported)
14
+ // Keys are auto-injected by the Vite plugin -- no manual key needed.
15
+ export const ProductState = createLocationState<{
16
+ name: string;
17
+ price: number;
18
+ }>();
19
+
20
+ // Also valid: const ProductState = createLocationState<...>();
21
+ // export { ProductState };
22
+ // Also valid: export { ProductState as MyState };
23
+
24
+ function ProductHeader() {
25
+ const state = useLocationState(ProductState);
26
+ // { name: string; price: number } | undefined
27
+
28
+ if (state) {
29
+ return (
30
+ <h1>
31
+ {state.name} - ${state.price}
32
+ </h1>
33
+ );
34
+ }
35
+ return <h1>Loading...</h1>;
36
+ }
37
+ ```
38
+
39
+ Pass state through Link:
40
+
41
+ ```tsx
42
+ import { Link } from "@rangojs/router/client";
43
+ import { ProductState } from "./state";
44
+
45
+ <Link to="/product/123" state={[ProductState({ name: "Widget", price: 99 })]}>
46
+ View Product
47
+ </Link>;
48
+ ```
49
+
50
+ Pass typed state just in time (getter evaluated at click time, not render time):
51
+
52
+ ```tsx
53
+ "use client"; // JIT state requires a client component (getter can't cross RSC boundary)
54
+
55
+ import { Link } from "@rangojs/router/client";
56
+ import { ProductState } from "./state";
57
+
58
+ // The getter is stored lazily and only called when the user clicks the link.
59
+ // This is useful for capturing values that change after render (e.g., scroll
60
+ // position, form state, ref values).
61
+ <Link
62
+ to="/product/123"
63
+ state={[ProductState(() => ({ name: product.name, price: product.price }))]}
64
+ >
65
+ View Product
66
+ </Link>;
67
+ ```
68
+
69
+ Plain state can also be evaluated just in time (also requires a client component):
70
+
71
+ ```tsx
72
+ <Link to="/product/123" state={() => ({ from: window.location.pathname })}>
73
+ View Product
74
+ </Link>
75
+ ```
76
+
77
+ ### Flash State (read-once)
78
+
79
+ Create a location state with `{ flash: true }` for read-once state that
80
+ auto-clears after first render. Ideal for flash messages (success/error
81
+ notifications after redirect):
82
+
83
+ ```tsx
84
+ // location-states.ts
85
+ import { createLocationState } from "@rangojs/router";
86
+
87
+ export const FlashMessage = createLocationState<{ text: string }>({
88
+ flash: true,
89
+ });
90
+ ```
91
+
92
+ Read flash state with `useLocationState` (same hook as persistent state):
93
+
94
+ ```tsx
95
+ "use client";
96
+ import { useLocationState } from "@rangojs/router/client";
97
+ import { FlashMessage } from "../location-states";
98
+
99
+ function FlashBanner() {
100
+ const flash = useLocationState(FlashMessage);
101
+ // { text: string } | undefined
102
+
103
+ if (!flash) return null;
104
+ return <div className="flash">{flash.text}</div>;
105
+ }
106
+ ```
107
+
108
+ Flash behavior is determined by the definition (`{ flash: true }`), not by which
109
+ hook reads it. `useLocationState` reads the value synchronously during render,
110
+ then clears it from `history.state` via `replaceState` in a `useEffect`.
111
+ Multiple components reading the same flash definition all see the value.
112
+ Pressing back/forward will not re-show the flash since it was cleared.
113
+
114
+ Set flash state from the server via `redirect()` with state:
115
+
116
+ ```tsx
117
+ // In a route handler
118
+ import { redirect, createLocationState } from "@rangojs/router";
119
+
120
+ export const FlashMessage = createLocationState<{ text: string }>({
121
+ flash: true,
122
+ });
123
+
124
+ // Handler
125
+ (ctx) => {
126
+ return redirect("/dashboard", {
127
+ state: [FlashMessage({ text: "Item saved!" })],
128
+ });
129
+ };
130
+ ```
131
+
132
+ Or via `ctx.setLocationState()` on any response:
133
+
134
+ ```tsx
135
+ (ctx) => {
136
+ ctx.setLocationState(FlashMessage({ text: "Welcome back!" }));
137
+ return <Dashboard />;
138
+ };
139
+ ```
140
+
141
+ ### .read() (non-hook access)
142
+
143
+ Read current location state outside React components (client-side only):
144
+
145
+ ```tsx
146
+ import { FlashMessage, ProductState } from "../location-states";
147
+
148
+ // Returns TState | undefined. Returns undefined during SSR.
149
+ const flash = FlashMessage.read();
150
+ const product = ProductState.read();
151
+ ```
152
+
153
+ > **Hydration:** `.read()` returns `undefined` on the server but may return
154
+ > a real value on the first client render (history state survives reload).
155
+ > Do not call `.read()` directly during the initial render of a component;
156
+ > call it from an event handler or inside a `useEffect` post-mount. For
157
+ > reactive hydration-safe access, use `useLocationState()` instead.
158
+
159
+ ### .write() / .delete() (static, non-reactive)
160
+
161
+ Static counterparts to `.read()`. Both mutate the current history entry's
162
+ `history.state` via `replaceState`, preserving any other keys (router
163
+ bookkeeping, other location state slots). Both are client-only; they throw
164
+ when called on the server.
165
+
166
+ Neither dispatches an event, so components reading via `useLocationState`
167
+ will NOT re-render until the next navigation/popstate. Pair with `.read()`
168
+ (or a fresh mount via back/forward/reload) instead.
169
+
170
+ ```tsx
171
+ "use client";
172
+ import { ProductState } from "./state";
173
+
174
+ // Persisted across hard refresh and back/forward of this entry.
175
+ ProductState.write({ name: "Widget", price: 9.99 });
176
+
177
+ // Read later (or on next mount).
178
+ const current = ProductState.read();
179
+
180
+ // Manually clear the slot. Idempotent if it isn't set.
181
+ ProductState.delete();
182
+ ```
183
+
184
+ | Method | Updates `history.state` | Fires `useLocationState` rerender | SSR behavior |
185
+ | ----------- | ----------------------- | --------------------------------- | ------------------- |
186
+ | `.read()` | no | n/a (returns snapshot) | returns `undefined` |
187
+ | `.write()` | yes (replace this slot) | no | throws |
188
+ | `.delete()` | yes (remove this slot) | no | throws |
189
+
190
+ ## Cache Control
191
+
192
+ ### invalidateClientCache()
193
+
194
+ Force the client's caches to miss after a mutation the router can't see (a REST
195
+ call, a WebSocket push, a login). It is a plain function, not a hook, so it works
196
+ from module-level callbacks too. Imported from the root entry `@rangojs/router`,
197
+ it is selected by export conditions: in a client component it marks the caches
198
+ stale immediately; from a handler/server component it writes a rotated
199
+ `Set-Cookie` for the responding client.
200
+
201
+ ```tsx
202
+ "use client";
203
+ import { invalidateClientCache } from "@rangojs/router";
204
+
205
+ function SaveButton() {
206
+ const handleSave = async () => {
207
+ await fetch("/api/data", {
208
+ method: "POST",
209
+ body: JSON.stringify(data),
210
+ });
211
+
212
+ // Invalidate the client's caches after the mutation
213
+ invalidateClientCache();
214
+ };
215
+
216
+ return <button onClick={handleSave}>Save</button>;
217
+ }
218
+ ```
219
+
220
+ A module-level subscription works the same way (no component needed):
221
+
222
+ ```ts
223
+ import { invalidateClientCache } from "@rangojs/router";
224
+
225
+ socket.on("catalog-updated", () => invalidateClientCache());
226
+ ```
227
+
228
+ **Use cases**: REST API mutations, WebSocket updates, non-RSC data changes.
@@ -0,0 +1,135 @@
1
+ # URL Hooks
2
+
3
+ ### useParams()
4
+
5
+ Access route params from the current URL:
6
+
7
+ ```tsx
8
+ "use client";
9
+ import { useParams } from "@rangojs/router/client";
10
+
11
+ // Route: /product/:productId
12
+ function ProductPage() {
13
+ const params = useParams();
14
+ // { productId: "123" }
15
+
16
+ return <h1>Product {params.productId}</h1>;
17
+ }
18
+
19
+ // Annotate the expected shape via a generic
20
+ function ProductPageTyped() {
21
+ const { productId } = useParams<{ productId: string }>();
22
+ return <h1>Product {productId}</h1>;
23
+ }
24
+
25
+ // With selector for performance (re-renders only when selected value changes)
26
+ function ProductId() {
27
+ const productId = useParams((p) => p.productId);
28
+ return <span>ID: {productId}</span>;
29
+ }
30
+ ```
31
+
32
+ Returns merged params from all matched route segments as a `Readonly<T>` map. Updates on navigation commit (not during pending navigation).
33
+
34
+ ### usePathname()
35
+
36
+ Access the current URL pathname:
37
+
38
+ ```tsx
39
+ "use client";
40
+ import { usePathname } from "@rangojs/router/client";
41
+
42
+ function CurrentPage() {
43
+ const pathname = usePathname();
44
+ // "/product/123" (no search params)
45
+
46
+ return <span>Current path: {pathname}</span>;
47
+ }
48
+ ```
49
+
50
+ Returns the pathname string without search params or hash. Updates on navigation commit.
51
+
52
+ ### useSearchParams()
53
+
54
+ Access the current URL search params:
55
+
56
+ ```tsx
57
+ "use client";
58
+ import { useSearchParams } from "@rangojs/router/client";
59
+
60
+ function SearchResults() {
61
+ const searchParams = useSearchParams();
62
+ const query = searchParams.get("q"); // "react"
63
+ const page = searchParams.get("page"); // "2"
64
+
65
+ return (
66
+ <div>
67
+ Searching for: {query}, page {page}
68
+ </div>
69
+ );
70
+ }
71
+ ```
72
+
73
+ Returns a `ReadonlyURLSearchParams` (URLSearchParams without mutation methods). During SSR, returns empty params and syncs from the browser URL on mount.
74
+
75
+ ### useHref()
76
+
77
+ Mount-aware href for client components inside `include()` scopes:
78
+
79
+ ```tsx
80
+ "use client";
81
+ import { useHref, href, Link } from "@rangojs/router/client";
82
+
83
+ // Inside include("/shop", shopPatterns)
84
+ function ShopNav() {
85
+ const href = useHref();
86
+
87
+ return (
88
+ <>
89
+ {/* Local paths - auto-prefixed with /shop */}
90
+ <Link to={href("/cart")}>Cart</Link>
91
+ <Link to={href("/product/widget")}>Widget</Link>
92
+ </>
93
+ );
94
+ }
95
+ ```
96
+
97
+ Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths.
98
+
99
+ ### useMount()
100
+
101
+ Returns the current `include()` mount path:
102
+
103
+ ```tsx
104
+ "use client";
105
+ import { useMount } from "@rangojs/router/client";
106
+
107
+ function MountInfo() {
108
+ const mount = useMount(); // "/shop" inside include("/shop", ...)
109
+ return <span>Mounted at: {mount}</span>;
110
+ }
111
+ ```
112
+
113
+ ### useReverse(routes)
114
+
115
+ Mount-aware local reverse for client components. Import the generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse("name", params?)` — the leading dot is optional. Auto-fills params from `useParams()`; explicit params override.
116
+
117
+ > Per-module `*.gen.ts` files are **CLI opt-in and not Vite-watched** — run `rango generate <urls-file>` (or wire it into `predev`) and re-run it whenever the module's routes change. See `/links` for the full generated-file setup and exposure-boundary rules.
118
+
119
+ ```tsx
120
+ "use client";
121
+ import { Link, useReverse } from "@rangojs/router/client";
122
+ import { routes as blogRoutes } from "../urls/blog.gen.js";
123
+
124
+ function BlogNav() {
125
+ const reverse = useReverse(blogRoutes);
126
+ return (
127
+ <nav>
128
+ <Link to={reverse("index")}>Blog</Link>
129
+ <Link to={reverse("post", { postId: "hello" })}>Post</Link>
130
+ </nav>
131
+ );
132
+ }
133
+ ```
134
+
135
+ See `/links` for the full URL generation guide. `ctx.reverse()` is server-only; on the client, prefer `useReverse(routes)` for in-module names and pass URLs as props for cross-module ones.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: host-router
3
- description: Multi-app host routing with domain/subdomain patterns
3
+ description: Multi-app host routing with domain/subdomain patterns. Use when running multiple apps behind one domain or across subdomains, or routing requests to different apps based on hostname.
4
4
  argument-hint:
5
5
  ---
6
6
 
@@ -176,17 +176,17 @@ router.fallback().map((request) => {
176
176
  });
177
177
  ```
178
178
 
179
- For unmatched hosts without `hostOverride`, catch `NoRouteMatchError` in your worker fetch:
179
+ For unmatched hosts without `hostOverride`, catch `NoRouteMatchError` in your worker fetch. Use the `isNoRouteMatchError()` guard rather than a bare `instanceof`: a workspace with a duplicated `@rangojs/router` copy can throw the error with a different class identity, and `instanceof` would then turn the 404 into an opaque 500.
180
180
 
181
181
  ```typescript
182
- import { NoRouteMatchError } from "@rangojs/router/host";
182
+ import { isNoRouteMatchError } from "@rangojs/router/host";
183
183
 
184
184
  export default {
185
185
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
186
186
  try {
187
187
  return await router.match(request, { env, ctx });
188
188
  } catch (err) {
189
- if (err instanceof NoRouteMatchError) {
189
+ if (isNoRouteMatchError(err)) {
190
190
  return new Response("Not Found", { status: 404 });
191
191
  }
192
192
  throw err;
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: i18n
3
- description: Locale-aware routing with `include("/:locale?", ...)`, locale resolution chains, and react-intl integration
3
+ description: Locale-aware routing with `include("/:locale?", ...)`, locale resolution chains, and react-intl integration. Use when building a multi-language app, routes need a locale segment, or wiring up react-intl translations.
4
4
  argument-hint: "[topic]"
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: intercept
3
- description: Define intercept routes for modals, slide-overs, and soft navigation patterns in @rangojs/router
3
+ description: Define intercept routes for modals, slide-overs, and soft navigation patterns in @rangojs/router. Use when opening a route as a modal/overlay on top of the current page while keeping the URL shareable, or asking "how do I show this page in a modal".
4
4
  argument-hint: [@slot-name] [route-to-intercept]
5
5
  ---
6
6
 
@@ -8,6 +8,13 @@ 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
+ ## Not this skill if…
12
+
13
+ - You want a slot that ALWAYS renders alongside the page (sidebar, multi-column
14
+ layout) — that is a permanent `parallel()` slot: see `/parallel`.
15
+ - You want the same component regardless of soft vs hard navigation —
16
+ intercepts only swap on soft navigation; see `/parallel`.
17
+
11
18
  ## Basic Intercept
12
19
 
13
20
  ```typescript
@@ -142,18 +149,41 @@ layout(ProductLayout, () => [
142
149
  ]);
143
150
  ```
144
151
 
145
- ## Conditional Intercept with when()
152
+ ## Conditional Intercept with the `when` config
153
+
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.
157
+
158
+ ```typescript
159
+ intercept(
160
+ "@modal",
161
+ "product",
162
+ <ProductModal />,
163
+ // Only intercept when coming from a different section
164
+ { when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
165
+ () => [
166
+ loader(ProductLoader),
167
+ ]
168
+ )
169
+ ```
146
170
 
147
- Only intercept based on navigation context:
171
+ `when` is a match-time selector receiving `{ from, to, params, segments, ... }`.
172
+ Pass an array of predicates for AND logic (all must return true). Omit `when`
173
+ entirely and the intercept always activates.
148
174
 
149
175
  ```typescript
150
176
  intercept(
151
177
  "@modal",
152
178
  "product",
153
179
  <ProductModal />,
180
+ {
181
+ when: [
182
+ ({ from }) => from.pathname.startsWith("/shop"),
183
+ ({ params }) => params.slug !== "featured",
184
+ ],
185
+ },
154
186
  () => [
155
- // Only intercept when coming from a different section
156
- when(({ from }) => !from.pathname.startsWith("/shop/product/")),
157
187
  loader(ProductLoader),
158
188
  ]
159
189
  )
@@ -242,10 +272,13 @@ layout(ShopLayout, () => [
242
272
  ]),
243
273
 
244
274
  // This intercept is also pre-rendered at build time
245
- intercept("@modal", ".detail", <ProductModal />, () => [
246
- when(({ from }) => from.pathname.startsWith("/shop")),
247
- loader(ProductLoader),
248
- ]),
275
+ intercept(
276
+ "@modal",
277
+ ".detail",
278
+ <ProductModal />,
279
+ { when: ({ from }) => from.pathname.startsWith("/shop") },
280
+ () => [loader(ProductLoader)],
281
+ ),
249
282
  ])
250
283
  ```
251
284
 
@@ -253,8 +286,8 @@ Build-time behavior:
253
286
 
254
287
  - The intercept handler (`<ProductModal />`) is resolved with BuildContext
255
288
  - Result is stored under the key `"detail/paramHash/i"` (intercept variant)
256
- - `when()` conditions are skipped at build time (all intercepts pre-rendered unconditionally)
257
- - `when()` is still evaluated at runtime by the intercept-resolution middleware
289
+ - `when` config conditions are skipped at build time (all intercepts pre-rendered unconditionally)
290
+ - `when` is still evaluated at runtime by the intercept-resolution middleware
258
291
 
259
292
  Runtime behavior:
260
293
 
@@ -305,7 +338,6 @@ export const shopPatterns = urls(({
305
338
  intercept,
306
339
  loader,
307
340
  loading,
308
- when,
309
341
  }) => [
310
342
  layout(<ShopLayout />, () => [
311
343
  parallel({
@@ -317,8 +349,8 @@ export const shopPatterns = urls(({
317
349
  "@modal",
318
350
  "product", // Route name (without prefix)
319
351
  <ProductModalContent />,
352
+ { when: ({ from }) => !from.pathname.startsWith("/shop/product/") },
320
353
  () => [
321
- when(({ from }) => !from.pathname.startsWith("/shop/product/")),
322
354
  layout(<ModalWrapper />),
323
355
  loading(<ProductModalSkeleton />),
324
356
  loader(ProductLoader, () => [cache()]),
@@ -338,7 +370,7 @@ export const shopPatterns = urls(({
338
370
 
339
371
  ## Handler-attached `.use`
340
372
 
341
- Intercept handlers can carry their own middleware, loaders, loading state, error/notFound boundaries, and even nested `layout`/`route`/`when` defaults via `.use` — useful for self-contained modal components that travel with their own data and chrome.
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`.)
342
374
 
343
375
  ```typescript
344
376
  const QuickViewModal: Handler = async (ctx) => {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: layout
3
- description: Define layout routes that wrap child routes in @rangojs/router
3
+ description: Define layout routes that wrap child routes in @rangojs/router. Use when sharing a persistent UI shell (nav, sidebar) across nested routes, or asking how to wrap child pages with a common layout.
4
4
  argument-hint: [component]
5
5
  ---
6
6
 
@@ -147,12 +147,21 @@ A layout as a child of `path()` wraps the route content and can read
147
147
  data set by the route handler via `ctx.get()`. The handler always
148
148
  executes before its children.
149
149
 
150
- This handler-first guarantee applies to a single full render pass
151
- (initial render, prerender, or full HTML re-render). During partial
152
- action revalidation, only the segments that revalidate are recomputed.
153
- If an orphan layout depends on data established by an outer handler or
154
- layout, that outer segment must also revalidate, or the orphan must
155
- guard/reload the data independently.
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` →
164
+ "Passing data down the tree" for the full safest-first ladder.
156
165
 
157
166
  ```typescript
158
167
  import { Outlet, ParallelOutlet } from "@rangojs/router/client";
@@ -191,7 +200,10 @@ orphan layouts to read them.
191
200
 
192
201
  ## Layout Revalidation
193
202
 
194
- Layouts don't revalidate by default. Control with `revalidate()`:
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()`:
195
207
 
196
208
  ```typescript
197
209
  layout(<ShopLayout />, () => [
@@ -218,8 +230,13 @@ their `ctx.set()` state.
218
230
 
219
231
  ### Revalidation Contracts
220
232
 
221
- For shared upstream data, define named revalidation functions and reuse
222
- them on both producer and consumer segments:
233
+ Contracts are the tool for cross-entry sharing the bottom rung of the
234
+ data-passing ladder (`/rango` "Passing data down the tree"). Before
235
+ writing one, check whether the producer can move down a rung: into the
236
+ consumer's own entry as an orphan layout, into middleware, or into a
237
+ loader. When the data genuinely must flow from an outer entry, define
238
+ named revalidation functions and reuse them on both producer and
239
+ consumer segments:
223
240
 
224
241
  ```typescript
225
242
  // revalidation-contracts.ts
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: links
3
- description: URL generation with ctx.reverse (server default), href (client), useHref (mounted), useMount, useReverse, and scopedReverse
3
+ description: URL generation with ctx.reverse (server default), href (client), useHref (mounted), useMount, useReverse, and scopedReverse. Use when generating a link to a route by name instead of hardcoding a path, or a link breaks after routes move or get mounted elsewhere.
4
4
  argument-hint: [ctx.reverse|href|useHref|useMount|useReverse|scopedReverse]
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: loader
3
- description: Define data loaders for fetching data in routes with createLoader
3
+ description: Define data loaders for fetching data in routes with createLoader. Use when pages need per-request data that stays fresh, data should stream while the page renders, or client components need reactive server data.
4
4
  argument-hint: [loader]
5
5
  ---
6
6
 
@@ -11,6 +11,13 @@ Loaders fetch data on the server and stream it to the client. For mutations
11
11
  `/server-actions`. Loaders re-resolve after an action runs, so the typical
12
12
  flow is _action mutates → loader re-reads → UI updates_.
13
13
 
14
+ ## Not this skill if…
15
+
16
+ - You want to mutate state — mutations are `"use server"` actions: see
17
+ `/server-actions`. Loaders read per-request live data.
18
+ - You want to cache a function's return value — loaders are fresh every request
19
+ by default; caching one function is `"use cache"`: see `/use-cache`.
20
+
14
21
  ## Creating a Loader
15
22
 
16
23
  ```typescript
@@ -140,6 +147,11 @@ same memoized result — loaders never run twice per request.
140
147
  - The handler output depends on the loader data. If the route is inside
141
148
  `cache()`, the handler is cached with the loader result baked in —
142
149
  defeating the live data guarantee.
150
+ - The same holds under a PPR shell capture (`/ppr`): handler consumption is
151
+ the BAKED lane — the loader executes at capture (identity reads permitted)
152
+ and the rendered value is a capture-time copy; `useLoader` client-side is
153
+ the live lane. One rule across `cache()`, `"use cache"`, and PPR: the
154
+ consumption-lane rule (`/rango` → Invariants).
143
155
  - Non-cacheable variable reads (`createVar({ cache: false })`) inside the
144
156
  handler still throw, even if the data came from a loader.
145
157
  - Prefer DSL `loader()` + client `useLoader()` for data that depends on
@@ -671,6 +683,16 @@ export const SearchLoader = createLoader(async (ctx) => {
671
683
  }, true); // true = fetchable
672
684
  ```
673
685
 
686
+ > **No registration needed — and no worker-entry import.** A fetchable loader
687
+ > does not have to be registered with `loader()` in the route DSL, and it does
688
+ > not have to be imported by any server module. Importing it into the client
689
+ > component that calls `useFetchLoader()` / `load()` is enough. Rango discovers
690
+ > every `createLoader(fn, true)` at build time and registers it for the
691
+ > `_rsc_loader` endpoint, so a loader reachable only through a client component
692
+ > still resolves in production — on both the generated entry and a hand-written
693
+ > worker entry (e.g. a Cloudflare `worker.rsc.tsx`). You do **not** need to
694
+ > force-import the loader in your worker entry to make it resolve.
695
+
674
696
  ### Fetchable Loader with Middleware
675
697
 
676
698
  Pass an options object instead of `true` to attach per-loader middleware.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: middleware
3
- description: Define middleware for authentication, logging, and request processing in @rangojs/router
3
+ description: Define middleware for authentication, logging, and request processing in @rangojs/router. Use when gating routes behind auth checks, logging requests, or running shared logic before a handler runs.
4
4
  argument-hint: [middleware-name]
5
5
  ---
6
6
 
@@ -60,8 +60,12 @@ data itself.
60
60
  ### Revalidation Contracts with Middleware-Backed Trees
61
61
 
62
62
  Middleware can establish request-level context (`ctx.set`) for segments that
63
- execute in the current render pass. It does not change partial revalidation
64
- boundaries between handler/layout/parallel segments.
63
+ execute in the current render pass. Because route middleware wraps **every**
64
+ render pass normal renders, post-action revalidation, PE re-renders — its
65
+ variables are never stale: middleware is the safest `ctx.set` rung on the
66
+ data-passing ladder (`/rango` → "Passing data down the tree"). But it does
67
+ not change partial revalidation boundaries between handler/layout/parallel
68
+ segments.
65
69
 
66
70
  For shared segment data, use named revalidation contracts on both the producer
67
71
  and consumer segments, even when middleware is present in the chain.