@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
package/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # @rangojs/router
2
2
 
3
- Django-inspired RSC router with type-safe partial rendering for Vite.
3
+ Named-route RSC router with structural composability and type-safe partial rendering for Vite.
4
4
 
5
5
  > **Experimental:** This package is under active development. APIs may change between releases. Install with `@experimental` tag.
6
6
 
7
7
  ## Features
8
8
 
9
- - **Composable URL patterns** — Django-style `urls()` DSL with `path`, `layout`, `include`
10
9
  - **Named routes** — `reverse("blogPost", { slug })` for type-safe URL generation (Django-style)
10
+ - **Structural composability** — Attach routes, loaders, middleware, handles, caching, prerendering, and static generation without hiding the route tree
11
+ - **Composable URL patterns** — Django-style `urls()` DSL with `path`, `layout`, `include`
11
12
  - **Data loaders** — `createLoader()` with automatic streaming and Suspense integration
13
+ - **Live data layer** — Pre-render or cache the UI shell while loaders stay live by default at request time
12
14
  - **Layouts & nesting** — Nested layouts with `<Outlet />` and parallel routes
13
15
  - **Segment-level caching** — `cache()` DSL with TTL/SWR and pluggable cache stores
14
16
  - **Middleware** — Route-level middleware with cookie and header access
@@ -16,8 +18,15 @@ Django-inspired RSC router with type-safe partial rendering for Vite.
16
18
  - **Theme support** — Light/dark mode with FOUC prevention and system detection
17
19
  - **Host routing** — Multi-app routing by domain/subdomain via `@rangojs/router/host`
18
20
  - **Response routes** — `path.json()`, `path.text()`, `path.xml()` for API endpoints
21
+ - **Trailing slash control** — Per-route canonical URLs with `"never"`, `"always"`, or `"ignore"`
19
22
  - **CLI codegen** — `rango generate` for route type generation
20
23
 
24
+ ## Design Docs
25
+
26
+ - [Execution model](./docs/internal/execution-model.md)
27
+ - [Semantic change checklist](./docs/internal/semantic-change-checklist.md)
28
+ - [Stability roadmap](./docs/internal/stability-roadmap.md)
29
+
21
30
  ## Installation
22
31
 
23
32
  ```bash
@@ -36,6 +45,30 @@ For Cloudflare Workers:
36
45
  npm install @cloudflare/vite-plugin
37
46
  ```
38
47
 
48
+ ## Import Paths
49
+
50
+ Use these import paths consistently:
51
+
52
+ - `@rangojs/router` — server/RSC router APIs, route DSL, `createRouter`, `urls`, `redirect`, `Prerender`, `Static`, shared types
53
+ - `@rangojs/router/client` — hooks and components such as `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `useAction`, `useLocationState`
54
+ - `@rangojs/router/cache` — public cache APIs such as `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware`
55
+ - `@rangojs/router/host`, `@rangojs/router/theme`, `@rangojs/router/vite` — specialized public subpaths
56
+ - `@rangojs/router/rsc`, `@rangojs/router/ssr` — advanced server-only integration subpaths for custom request/HTML pipelines
57
+
58
+ Use only subpaths that are explicitly exported from the package. Avoid deep imports such as `@rangojs/router/cache/cf`.
59
+
60
+ `@rangojs/router` is conditionally resolved. Server-only root APIs such as
61
+ `createRouter()`, `urls()`, `redirect()`, `Prerender()`, and `cookies()` rely on
62
+ the `react-server` export condition and are meant to run in router definitions,
63
+ handlers, and other RSC/server modules. Outside that environment the root entry
64
+ falls back to stub implementations that throw guidance errors.
65
+
66
+ If you hit a root-entrypoint stub error:
67
+
68
+ - hooks and components like `Link`, `Outlet`, `useLoader`, `useNavigation`, and `MetaTags` belong in `@rangojs/router/client`
69
+ - cache APIs like `CFCacheStore` and `createDocumentCacheMiddleware` belong in `@rangojs/router/cache`
70
+ - host-router APIs belong in `@rangojs/router/host`
71
+
39
72
  ## Quick Start
40
73
 
41
74
  ### Vite Config
@@ -53,23 +86,30 @@ export default defineConfig({
53
86
 
54
87
  ### Router
55
88
 
89
+ This file is a server/RSC module and should import router construction APIs from
90
+ `@rangojs/router`.
91
+
56
92
  ```tsx
57
93
  // src/router.tsx
58
94
  import { createRouter, urls } from "@rangojs/router";
59
95
  import { Document } from "./document";
60
96
 
61
- const urlpatterns = urls(({ path, layout }) => [
62
- layout(<MainLayout />, () => [
63
- path("/", HomePage, { name: "home" }),
64
- path("/about", AboutPage, { name: "about" }),
65
- path("/blog/:slug", BlogPostPage, { name: "blogPost" }),
66
- ]),
97
+ const blogPatterns = urls(({ path }) => [
98
+ path("/", BlogIndexPage, { name: "index" }),
99
+ path("/:slug", BlogPostPage, { name: "post" }),
100
+ ]);
101
+
102
+ const urlpatterns = urls(({ path, include }) => [
103
+ path("/", HomePage, { name: "home" }),
104
+ include("/blog", blogPatterns, { name: "blog" }),
67
105
  ]);
68
106
 
69
107
  export const router = createRouter({ document: Document }).routes(urlpatterns);
70
108
 
71
109
  // Export typed reverse function for URL generation by route name
72
110
  export const reverse = router.reverse;
111
+
112
+ // reverse("blog.post", { slug: "hello-world" }) -> "/blog/hello-world"
73
113
  ```
74
114
 
75
115
  ### Document
@@ -95,7 +135,15 @@ export function Document({ children }: { children: ReactNode }) {
95
135
 
96
136
  ## Defining Routes
97
137
 
98
- ### Path Patterns
138
+ Rango is a named-route router first.
139
+
140
+ Paths define where a route lives. Names define how the app refers to it.
141
+
142
+ It is also structurally composable.
143
+
144
+ As an app grows, routes can pull in external handlers, loaders, middleware, handles, cache policy, intercepts, prerendering, and static generation while keeping the route tree visible at the composition site.
145
+
146
+ ### Named Routes
99
147
 
100
148
  ```tsx
101
149
  import { urls } from "@rangojs/router";
@@ -108,6 +156,99 @@ const urlpatterns = urls(({ path }) => [
108
156
  ]);
109
157
  ```
110
158
 
159
+ Use `reverse()` as the default way to link to routes:
160
+
161
+ ```tsx
162
+ router.reverse("product", { slug: "widget" }); // "/product/widget"
163
+ router.reverse("search", undefined, { q: "rsc" }); // "/search?q=rsc"
164
+ ```
165
+
166
+ ### Composable URL Modules
167
+
168
+ Local route names compose cleanly with `include(..., { name })`:
169
+
170
+ ```tsx
171
+ import { urls } from "@rangojs/router";
172
+
173
+ export const blogPatterns = urls(({ path }) => [
174
+ path("/", BlogIndexPage, { name: "index" }),
175
+ path("/:slug", BlogPostPage, { name: "post" }),
176
+ ]);
177
+
178
+ export const urlpatterns = urls(({ path, include }) => [
179
+ path("/", HomePage, { name: "home" }),
180
+ include("/blog", blogPatterns, { name: "blog" }),
181
+ ]);
182
+
183
+ router.reverse("blog.index"); // "/blog"
184
+ router.reverse("blog.post", { slug: "hello-world" }); // "/blog/hello-world"
185
+ ```
186
+
187
+ This is the core composition model:
188
+
189
+ - Paths stay local to the module that defines them
190
+ - Names become stable references across the app
191
+ - `include()` scales those names without forcing raw path-string coupling
192
+
193
+ ### Structural Composability
194
+
195
+ Rango avoids the usual tradeoff between modularity and visibility.
196
+
197
+ You can extract route behavior into separate files or packages and still keep one readable route definition that shows the structure of the app.
198
+
199
+ ```tsx
200
+ import { urls } from "@rangojs/router";
201
+ import { ProductPage } from "./routes/product";
202
+ import { ProductLoader } from "./loaders/product";
203
+ import { productMiddleware } from "./middleware/product";
204
+ import { productRevalidate } from "./revalidation/product";
205
+
206
+ const shopPatterns = urls(({ path, loader, middleware, revalidate, cache }) => [
207
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
208
+ middleware(productMiddleware),
209
+ loader(ProductLoader),
210
+ revalidate(productRevalidate),
211
+ cache({ ttl: 300 }),
212
+ ]),
213
+ ]);
214
+ ```
215
+
216
+ The route tree stays explicit even when behavior is modular.
217
+
218
+ This applies to:
219
+
220
+ - external route modules mounted with `include()`
221
+ - imported loaders, middleware, and handles attached at the route site
222
+ - prerendering and static generation attached without turning the route tree opaque
223
+
224
+ ### Loaders As the Live Data Layer
225
+
226
+ Rango separates app structure from app data.
227
+
228
+ Routes, layouts, and pre-rendered segments can be static or cached, while
229
+ loaders stay live by default and re-resolve at request time.
230
+
231
+ This means you can pre-render or cache the shell of a page without freezing its
232
+ data.
233
+
234
+ - `cache()` caches route structure and rendered UI segments
235
+ - `Prerender()` skips loaders at build time
236
+ - `loader()` provides fresh request-time data
237
+ - individual loaders can opt into caching explicitly when needed
238
+
239
+ ```tsx
240
+ import { urls, Prerender } from "@rangojs/router";
241
+ import { ArticleLoader } from "./loaders/article";
242
+
243
+ const docsPatterns = urls(({ path, loader }) => [
244
+ path("/docs/:slug", Prerender(DocsArticle), { name: "docs.article" }, () => [
245
+ loader(ArticleLoader), // fresh by default
246
+ ]),
247
+ ]);
248
+ ```
249
+
250
+ Pre-render the page, keep the data live.
251
+
111
252
  ### Typed Handlers
112
253
 
113
254
  Route handlers receive a typed context with params, search params, and `reverse()`:
@@ -122,6 +263,29 @@ export const ProductPage: Handler<"product"> = (ctx) => {
122
263
  };
123
264
  ```
124
265
 
266
+ ### Choosing a Handler Style
267
+
268
+ All handler typing styles are supported, but they solve different problems:
269
+
270
+ - `Handler<"product">` — default for named app routes
271
+ - `Handler<".post", ScopedRouteMap<"blog">>` — best for reusable included modules
272
+ - `Handler<"/blog/:slug">` — good for unnamed or local-only extracted handlers
273
+ - `Handler<{ slug: string }>` — escape hatch for advanced or decoupled cases
274
+
275
+ Example of a scoped local name inside a mounted module:
276
+
277
+ ```tsx
278
+ import type { Handler, ScopedRouteMap } from "@rangojs/router";
279
+
280
+ type BlogRoutes = ScopedRouteMap<"blog">;
281
+
282
+ export const BlogPostPage: Handler<".post", BlogRoutes> = (ctx) => {
283
+ return <a href={ctx.reverse(".index")}>Back to blog</a>;
284
+ };
285
+ ```
286
+
287
+ See [`../../docs/named-routes.md`](../../docs/named-routes.md) for the recommended mental model.
288
+
125
289
  ### Search Params
126
290
 
127
291
  Define a search schema on the route for type-safe search parameters:
@@ -141,6 +305,44 @@ const SearchPage: Handler<"search"> = (ctx) => {
141
305
  };
142
306
  ```
143
307
 
308
+ ### Trailing Slash Handling
309
+
310
+ Trailing slash behavior is a current `path()` feature.
311
+
312
+ Set it per route with `trailingSlash`:
313
+
314
+ ```tsx
315
+ const urlpatterns = urls(({ path }) => [
316
+ path("/about", AboutPage, {
317
+ name: "about",
318
+ trailingSlash: "never",
319
+ }),
320
+ path("/docs/", DocsPage, {
321
+ name: "docs",
322
+ trailingSlash: "always",
323
+ }),
324
+ path("/webhook", WebhookHandler, {
325
+ name: "webhook",
326
+ trailingSlash: "ignore",
327
+ }),
328
+ ]);
329
+ ```
330
+
331
+ Modes:
332
+
333
+ - `"never"` — canonical URL has no trailing slash, redirects `/about/` to `/about`
334
+ - `"always"` — canonical URL has a trailing slash, redirects `/docs` to `/docs/`
335
+ - `"ignore"` — matches both forms without redirect
336
+
337
+ Default behavior when `trailingSlash` is omitted:
338
+
339
+ - There is no separate global default mode
340
+ - If the pattern is defined without a trailing slash, the canonical URL is the no-slash form
341
+ - If the pattern is defined with a trailing slash, the canonical URL is the slash form
342
+ - The router redirects to the canonical form based on the pattern you defined
343
+
344
+ The recommended public API is the per-route `path(..., { trailingSlash })` option. Use `"ignore"` sparingly, especially on content pages, because `/x` and `/x/` are distinct URLs.
345
+
144
346
  ### Response Routes
145
347
 
146
348
  Define API endpoints that bypass the RSC pipeline:
@@ -248,10 +450,10 @@ import { useLoader } from "@rangojs/router/client";
248
450
  import { BlogSidebarLoader } from "./loaders/blog";
249
451
 
250
452
  function BlogSidebar() {
251
- const { posts } = useLoader(BlogSidebarLoader);
453
+ const { data } = useLoader(BlogSidebarLoader);
252
454
  return (
253
455
  <ul>
254
- {posts.map((p) => (
456
+ {data.posts.map((p) => (
255
457
  <li key={p.slug}>{p.title}</li>
256
458
  ))}
257
459
  </ul>
@@ -313,7 +515,7 @@ function Nav() {
313
515
  return (
314
516
  <nav>
315
517
  <Link to={href("/")}>Home</Link>
316
- <Link to={href("/blog")} prefetch="intent">
518
+ <Link to={href("/blog")} prefetch="adaptive">
317
519
  Blog
318
520
  </Link>
319
521
  <Link to={href("/about")}>About</Link>
@@ -324,20 +526,21 @@ function Nav() {
324
526
 
325
527
  `href()` validates that the path matches a registered route pattern at compile time (e.g. `/blog/my-post` matches `/blog/:slug`).
326
528
 
327
- ### Navigation Hook
529
+ ### Navigation Hooks
328
530
 
329
531
  ```tsx
330
532
  "use client";
331
- import { useNavigation } from "@rangojs/router/client";
533
+ import { useNavigation, useRouter } from "@rangojs/router/client";
332
534
 
333
535
  function SearchForm() {
334
- const { navigate, isPending } = useNavigation();
536
+ const router = useRouter();
537
+ const nav = useNavigation();
335
538
 
336
539
  function handleSubmit(query: string) {
337
- navigate(`/search?q=${encodeURIComponent(query)}`);
540
+ router.push(`/search?q=${encodeURIComponent(query)}`);
338
541
  }
339
542
 
340
- return <form onSubmit={...}>{isPending && <Spinner />}</form>;
543
+ return <form onSubmit={...}>{nav.state !== "idle" && <Spinner />}</form>;
341
544
  }
342
545
  ```
343
546
 
@@ -385,6 +588,20 @@ export const urlpatterns = urls(({ path, include }) => [
385
588
 
386
589
  Included route names are prefixed with the include name: `reverse("api.health")`, `reverse("api.products")`.
387
590
 
591
+ ### Include name scoping
592
+
593
+ The `name` option controls how child route names appear globally:
594
+
595
+ | Form | Child names | Generated types | Reverse resolution |
596
+ | ---------------------------------- | ------------------- | ---------------------- | -------------------------------------------------------------------- |
597
+ | `include("/x", p, { name: "ns" })` | `ns.child` | Exported as `ns.child` | `reverse("ns.child")` globally, `reverse(".child")` inside |
598
+ | `include("/x", p, { name: "" })` | `child` (flattened) | Exported as-is | `reverse("child")` globally, `reverse(".child")` inside (root-scope) |
599
+ | `include("/x", p)` | Private scope | Not exported | `reverse(".child")` inside only |
600
+
601
+ Without a `name`, included routes are local to the mounted module. They still match requests and render normally, but their names are hidden from the generated route map and cannot be reversed globally. Use `{ name: "" }` to merge children into the parent namespace without adding a prefix.
602
+
603
+ **`{ name: "" }` is flattening, not isolation.** Flattened routes behave as if defined inline at the include site — dot-local reverse (`.name`) can reach any sibling route at root scope, including routes from other `{ name: "" }` mounts. If you need module-level isolation, omit the `name` option or use a namespace.
604
+
388
605
  ## Middleware
389
606
 
390
607
  ```tsx
@@ -511,6 +728,23 @@ export const ProductPage = Prerender(
511
728
 
512
729
  With `passthrough: true`, known params are served from the build-time cache and unknown params fall through to live rendering.
513
730
 
731
+ Handlers can also skip individual param sets with `ctx.passthrough()`, deferring them to the live handler at runtime:
732
+
733
+ ```tsx
734
+ export const ProductPage = Prerender(
735
+ async () => {
736
+ const all = await db.getAllProducts();
737
+ return all.map((p) => ({ id: p.id }));
738
+ },
739
+ async (ctx) => {
740
+ const product = await db.getProduct(ctx.params.id);
741
+ if (!product.published) return ctx.passthrough();
742
+ return <Product data={product} />;
743
+ },
744
+ { passthrough: true },
745
+ );
746
+ ```
747
+
514
748
  ## Theme
515
749
 
516
750
  ### Router Configuration
@@ -605,20 +839,52 @@ Auto-detects file type:
605
839
 
606
840
  ## Type Safety
607
841
 
608
- The Vite plugin automatically generates a `router.named-routes.gen.ts` file that globally registers all route names, patterns, and search schemas. Type-safe `reverse()`, `Handler<"name">`, `href()`, and `RouteParams<"name">` work out of the box — no manual type registration needed. The gen file is updated on dev server startup, HMR, and production builds.
842
+ The Vite plugin automatically generates a `router.named-routes.gen.ts` file that globally registers route names, patterns, and search schemas via `RSCRouter.GeneratedRouteMap`. This powers server-side named-route typing such as `Handler<"name">`, `ctx.reverse()`, `getRequestContext().reverse()`, and `RouteParams<"name">` without any manual route registration. The gen file is updated on dev server startup, HMR, and production builds.
843
+
844
+ Use the generated map by default. Augment `RSCRouter.RegisteredRoutes` only when you need the richer `typeof router.routeMap` shape globally, especially for response-aware and path-based utilities.
845
+
846
+ ```typescript
847
+ // router.tsx
848
+ const router = createRouter<AppBindings>({}).routes(urlpatterns);
849
+
850
+ declare global {
851
+ namespace RSCRouter {
852
+ interface Env extends AppEnv {}
853
+ interface Vars extends AppVars {}
854
+ interface RegisteredRoutes extends typeof router.routeMap {}
855
+ }
856
+ }
857
+ ```
858
+
859
+ Quick rule of thumb:
860
+
861
+ - `GeneratedRouteMap` (auto-generated) — use for server-side named-route typing: `Handler<"name">`, `ctx.reverse()`, `Prerender<"name">`
862
+ - `typeof router.routeMap` — use when you need route entries with response metadata
863
+ - `RegisteredRoutes` (manual augmentation) — use to expose `typeof router.routeMap` globally for `href()`, `PathResponse`, `ValidPaths`, and other path/response-aware utilities
864
+
865
+ For extracted reusable loaders or middleware, prefer global dotted names on
866
+ `ctx.reverse()` by default. If you want type-safe local names for a specific
867
+ module, use `scopedReverse<typeof localPatterns>(ctx.reverse)` or
868
+ `scopedReverse<routes>(ctx.reverse)` with a generated local route type.
609
869
 
610
870
  ## Subpath Exports
611
871
 
612
- | Export | Description |
613
- | ------------------------ | --------------------------------------------------------------------------------- |
614
- | `@rangojs/router` | Core: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
615
- | `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags` |
616
- | `@rangojs/router/cache` | Cache: `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
617
- | `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
618
- | `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts` |
619
- | `@rangojs/router/vite` | Vite plugin: `rango()` |
620
- | `@rangojs/router/server` | Server utilities |
621
- | `@rangojs/router/build` | Build utilities |
872
+ | Export | Description |
873
+ | ------------------------ | -------------------------------------------------------------------------------------------------------- |
874
+ | `@rangojs/router` | Server/RSC core and shared types: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
875
+ | `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags` |
876
+ | `@rangojs/router/cache` | Cache: `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
877
+ | `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
878
+ | `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts` |
879
+ | `@rangojs/router/vite` | Vite plugin: `rango()` |
880
+ | `@rangojs/router/rsc` | Advanced server pipeline APIs: `createRSCHandler`, request-context access |
881
+ | `@rangojs/router/ssr` | Advanced SSR bridge APIs: `createSSRHandler` |
882
+ | `@rangojs/router/server` | Internal build/runtime utilities for advanced integrations |
883
+ | `@rangojs/router/build` | Build utilities |
884
+
885
+ The root entrypoint is not a generic client/runtime barrel. If you need hooks
886
+ or components, import from `@rangojs/router/client`; if you need cache or host
887
+ APIs, use their dedicated subpaths.
622
888
 
623
889
  ## Examples
624
890