@rangojs/router 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +7 -4
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +1 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/types/cache-types.d.ts +14 -10
  67. package/dist/types/types/handler-context.d.ts +14 -0
  68. package/dist/types/types/loader-types.d.ts +8 -6
  69. package/dist/types/urls/response-types.d.ts +3 -12
  70. package/dist/types/use-loader.d.ts +12 -3
  71. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  72. package/dist/types/vite/rango.d.ts +1 -1
  73. package/dist/vite/index.js +1381 -1346
  74. package/package.json +2 -2
  75. package/skills/api-client/SKILL.md +22 -10
  76. package/skills/breadcrumbs/SKILL.md +65 -22
  77. package/skills/bundle-analysis/SKILL.md +22 -22
  78. package/skills/cache-guide/SKILL.md +207 -97
  79. package/skills/caching/SKILL.md +295 -93
  80. package/skills/catalog.json +16 -16
  81. package/skills/client-urls/SKILL.md +53 -31
  82. package/skills/cloudflare/SKILL.md +72 -20
  83. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  84. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  85. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  86. package/skills/comparison/SKILL.md +5 -0
  87. package/skills/comparison/references/framework-comparison.md +39 -33
  88. package/skills/composability/SKILL.md +38 -29
  89. package/skills/css/SKILL.md +10 -13
  90. package/skills/debug-manifest/SKILL.md +74 -31
  91. package/skills/defer-hydration/SKILL.md +2 -1
  92. package/skills/deployment-caching/SKILL.md +6 -1
  93. package/skills/document-cache/SKILL.md +92 -28
  94. package/skills/fonts/SKILL.md +16 -8
  95. package/skills/handler-use/SKILL.md +53 -17
  96. package/skills/hooks/SKILL.md +28 -20
  97. package/skills/hooks/data.md +23 -3
  98. package/skills/hooks/handle-and-actions.md +31 -16
  99. package/skills/hooks/navigation.md +39 -10
  100. package/skills/hooks/outlets.md +13 -6
  101. package/skills/hooks/state.md +37 -15
  102. package/skills/hooks/urls.md +7 -5
  103. package/skills/host-router/SKILL.md +64 -12
  104. package/skills/i18n/SKILL.md +60 -34
  105. package/skills/intercept/SKILL.md +161 -26
  106. package/skills/layout/SKILL.md +106 -50
  107. package/skills/links/SKILL.md +51 -19
  108. package/skills/loader/SKILL.md +242 -106
  109. package/skills/middleware/SKILL.md +119 -23
  110. package/skills/migrate-nextjs/SKILL.md +100 -45
  111. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  112. package/skills/migrate-react-router/SKILL.md +49 -38
  113. package/skills/migrate-react-router/component-migration.md +25 -6
  114. package/skills/migrate-react-router/data-and-actions.md +18 -5
  115. package/skills/migrate-react-router/route-mapping.md +12 -6
  116. package/skills/mime-routes/SKILL.md +12 -7
  117. package/skills/observability/SKILL.md +28 -14
  118. package/skills/parallel/SKILL.md +75 -39
  119. package/skills/ppr/SKILL.md +345 -150
  120. package/skills/prerender/SKILL.md +189 -100
  121. package/skills/rango/SKILL.md +65 -34
  122. package/skills/react-compiler/SKILL.md +6 -5
  123. package/skills/response-routes/SKILL.md +133 -25
  124. package/skills/route/SKILL.md +165 -60
  125. package/skills/router-setup/SKILL.md +418 -240
  126. package/skills/scripts/SKILL.md +31 -20
  127. package/skills/server-actions/SKILL.md +220 -59
  128. package/skills/shell-manifest/SKILL.md +12 -7
  129. package/skills/streams-and-websockets/SKILL.md +32 -14
  130. package/skills/tailwind/SKILL.md +37 -33
  131. package/skills/testing/SKILL.md +68 -32
  132. package/skills/testing/cache-prerender.md +55 -29
  133. package/skills/testing/client-components.md +59 -6
  134. package/skills/testing/e2e-parity.md +12 -2
  135. package/skills/testing/flight.md +19 -11
  136. package/skills/testing/handles.md +8 -5
  137. package/skills/testing/loader.md +8 -5
  138. package/skills/testing/middleware.md +8 -3
  139. package/skills/testing/predicates.md +120 -0
  140. package/skills/testing/render-handler.md +70 -22
  141. package/skills/testing/response-routes.md +11 -4
  142. package/skills/testing/reverse-and-types.md +20 -14
  143. package/skills/testing/server-actions.md +12 -3
  144. package/skills/testing/server-tree.md +9 -0
  145. package/skills/testing/setup.md +9 -5
  146. package/skills/theme/SKILL.md +106 -29
  147. package/skills/typesafety/SKILL.md +20 -9
  148. package/skills/typesafety/env-and-bindings.md +126 -47
  149. package/skills/typesafety/generated-files-and-cli.md +26 -14
  150. package/skills/typesafety/params-and-search.md +44 -19
  151. package/skills/typesafety/route-types.md +50 -14
  152. package/skills/use-cache/SKILL.md +200 -82
  153. package/skills/vercel/SKILL.md +21 -11
  154. package/skills/view-transitions/SKILL.md +41 -20
  155. package/src/__augment-tests__/augmented.check.ts +26 -1
  156. package/src/browser/index.ts +2 -2
  157. package/src/browser/navigation-bridge.ts +3 -4
  158. package/src/browser/navigation-transaction.ts +22 -2
  159. package/src/browser/partial-update.ts +35 -25
  160. package/src/browser/react/ScrollRestoration.tsx +6 -6
  161. package/src/browser/rsc-router.tsx +1 -1
  162. package/src/browser/scroll-restoration.ts +53 -6
  163. package/src/cache/cache-exec-scope.ts +37 -4
  164. package/src/cache/cache-key-utils.ts +1 -6
  165. package/src/cache/cache-runtime.ts +230 -61
  166. package/src/cache/cache-scope.ts +76 -12
  167. package/src/cache/document-cache.ts +15 -0
  168. package/src/cache/handle-capture.ts +90 -10
  169. package/src/cache/handle-snapshot.ts +132 -28
  170. package/src/cache/segment-codec.ts +26 -7
  171. package/src/cache/tag-invalidation.ts +4 -3
  172. package/src/cache/taint.ts +1 -1
  173. package/src/cache/types.ts +16 -3
  174. package/src/client.rsc.tsx +10 -9
  175. package/src/client.tsx +5 -4
  176. package/src/components/DefaultDocument.tsx +7 -2
  177. package/src/context-var.ts +6 -5
  178. package/src/escape-script.ts +1 -1
  179. package/src/handles/MetaTags.tsx +13 -11
  180. package/src/handles/Scripts.tsx +11 -10
  181. package/src/handles/meta.ts +2 -2
  182. package/src/handles/script.ts +17 -16
  183. package/src/href-client.ts +1 -1
  184. package/src/html.ts +12 -0
  185. package/src/index.rsc.ts +5 -0
  186. package/src/index.ts +1 -0
  187. package/src/loader-store.ts +7 -4
  188. package/src/prerender/build-shell-capture.ts +8 -11
  189. package/src/reverse.ts +44 -4
  190. package/src/route-definition/dsl-helpers.ts +117 -30
  191. package/src/route-definition/helpers-types.ts +33 -9
  192. package/src/route-definition/resolve-handler-use.ts +121 -6
  193. package/src/route-types.ts +0 -3
  194. package/src/router/debug-manifest.ts +10 -2
  195. package/src/router/error-handling.ts +51 -15
  196. package/src/router/handler-context.ts +34 -25
  197. package/src/router/intercept-resolution.ts +125 -39
  198. package/src/router/loader-resolution.ts +56 -11
  199. package/src/router/match-api.ts +12 -17
  200. package/src/router/match-middleware/background-revalidation.ts +161 -96
  201. package/src/router/match-middleware/cache-lookup.ts +93 -10
  202. package/src/router/match-middleware/cache-store.ts +17 -90
  203. package/src/router/middleware-types.ts +3 -5
  204. package/src/router/middleware.ts +52 -9
  205. package/src/router/prerender-match.ts +53 -19
  206. package/src/router/route-snapshot.ts +11 -1
  207. package/src/router/router-context.ts +2 -0
  208. package/src/router/router-interfaces.ts +25 -2
  209. package/src/router/router-options.ts +8 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -12
  211. package/src/router/segment-resolution/loader-cache.ts +205 -45
  212. package/src/router/segment-resolution/loader-mask.ts +12 -22
  213. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  214. package/src/router/segment-resolution/mask-nested.ts +7 -7
  215. package/src/router/segment-resolution/revalidation.ts +34 -6
  216. package/src/router/segment-wrappers.ts +4 -0
  217. package/src/rsc/render-pipeline.ts +3 -0
  218. package/src/rsc/response-route-handler.ts +11 -3
  219. package/src/rsc/rsc-rendering.ts +8 -45
  220. package/src/rsc/shell-capture-constants.ts +13 -0
  221. package/src/rsc/shell-capture.ts +111 -90
  222. package/src/rsc/ssr-setup.ts +14 -8
  223. package/src/rsc/types.ts +11 -0
  224. package/src/server/context.ts +88 -11
  225. package/src/server/cookie-store.ts +5 -4
  226. package/src/server/handle-store.ts +232 -7
  227. package/src/server/request-context.ts +35 -30
  228. package/src/ssr/index.tsx +40 -7
  229. package/src/static-handler.ts +4 -1
  230. package/src/testing/flight-runtime.d.ts +21 -0
  231. package/src/testing/generated-routes.ts +2 -2
  232. package/src/testing/internal/context.ts +1 -1
  233. package/src/testing/internal/flight-client-globals.ts +7 -5
  234. package/src/testing/render-route.tsx +85 -24
  235. package/src/testing/run-loader.ts +5 -1
  236. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  237. package/src/testing/vitest.ts +62 -1
  238. package/src/theme/ThemeScript.tsx +3 -3
  239. package/src/types/cache-types.ts +14 -10
  240. package/src/types/handler-context.ts +14 -0
  241. package/src/types/loader-types.ts +8 -6
  242. package/src/urls/response-types.ts +3 -23
  243. package/src/use-loader.tsx +12 -3
  244. package/src/vite/plugins/use-cache-transform.ts +118 -74
  245. package/src/vite/rango.ts +7 -3
@@ -1,12 +1,39 @@
1
1
  ---
2
2
  name: router-setup
3
- description: Create and configure the RSC router with createRouter. Use when bootstrapping a new Rango app, or configuring top-level router options like base path, cache store, or environment.
3
+ description: Create and configure a Rango app with createRouter() and the rango() Vite plugin. Use when bootstrapping a new app, wiring vite.config, writing the Document or worker entry, or setting top-level router options (basename, cache store, prefetch, timeouts, origin check, telemetry, SSR streaming).
4
4
  argument-hint: [option]
5
5
  ---
6
6
 
7
7
  # Router Setup with createRouter
8
8
 
9
- ## Basic Router Creation
9
+ This skill covers the app skeleton: the Vite plugin, the router module, the
10
+ Document, the request entry, and every top-level `createRouter()` and `rango()`
11
+ option. Route-level features (loaders, caching, middleware, intercepts) have
12
+ their own skills; start at `/rango` for the map.
13
+
14
+ ## Minimal app
15
+
16
+ On the default Node preset an app needs a Vite config, a router module, and a
17
+ `urls()` module (plus your page components). `rango()` finds the router by
18
+ scanning for the single `createRouter()` call and generates the client, SSR,
19
+ and RSC entries, so there is no `index.html` and no entry file to write. The
20
+ `document` option is optional (see [Document component](#document-component)).
21
+
22
+ ```typescript
23
+ // vite.config.ts
24
+ import { defineConfig } from "vite";
25
+ import { rango } from "@rangojs/router/vite";
26
+
27
+ export default defineConfig(({ command }) => ({
28
+ plugins: [rango()],
29
+ // Node preset only: fold NODE_ENV so SSR/RSC ship only React's production
30
+ // build. The cloudflare and vercel presets do this for you (see /bundle-analysis).
31
+ define:
32
+ command === "build"
33
+ ? { "process.env.NODE_ENV": JSON.stringify("production") }
34
+ : undefined,
35
+ }));
36
+ ```
10
37
 
11
38
  ```typescript
12
39
  // src/router.tsx
@@ -14,15 +41,32 @@ import { createRouter } from "@rangojs/router";
14
41
  import { Document } from "./document";
15
42
  import { urlpatterns } from "./urls";
16
43
 
17
- const router = createRouter({
44
+ // Node and Vercel presets import the NAMED `router` export from this module.
45
+ // A default-only export is not picked up.
46
+ export const router = createRouter({
18
47
  document: Document,
19
48
  urls: urlpatterns,
20
49
  });
50
+ ```
21
51
 
22
- export default router;
52
+ ```typescript
53
+ // src/urls.tsx
54
+ import { urls } from "@rangojs/router";
55
+ import { HomePage } from "./pages/home";
56
+ import { AboutPage } from "./pages/about";
57
+
58
+ export const urlpatterns = urls(({ path }) => [
59
+ path("/", HomePage, { name: "home" }),
60
+ path("/about", AboutPage, { name: "about" }),
61
+ ]);
23
62
  ```
24
63
 
25
- ## URL Patterns (Django-style)
64
+ `@vitejs/plugin-react` is optional: `rango()` works on its own. Add
65
+ `react()` before `rango()` when you want its features, such as the React
66
+ Compiler (`/react-compiler`). For Workers use `/cloudflare`; for Vercel
67
+ Functions use `/vercel`.
68
+
69
+ ## URL patterns (Django-style)
26
70
 
27
71
  ```typescript
28
72
  // src/urls.tsx
@@ -30,7 +74,9 @@ import { urls } from "@rangojs/router";
30
74
  import { HomePage } from "./pages/home";
31
75
  import { AboutPage } from "./pages/about";
32
76
  import { ProductPage } from "./pages/product";
77
+ import { ProductSkeleton } from "./pages/product-skeleton";
33
78
  import { RootLayout } from "./layouts/RootLayout";
79
+ import { ProductLoader } from "./loaders/product";
34
80
 
35
81
  export const urlpatterns = urls(({ path, layout, loader, loading }) => [
36
82
  path("/", HomePage, { name: "home" }),
@@ -47,105 +93,239 @@ export const urlpatterns = urls(({ path, layout, loader, loading }) => [
47
93
 
48
94
  ## The urls() DSL
49
95
 
50
- The `urls()` function provides a callback with all available DSL functions:
96
+ `urls()` passes every DSL helper to its callback. Destructure the ones you use:
51
97
 
52
98
  ```typescript
53
99
  urls(
54
100
  ({
55
- path, // Define a route
56
- layout, // Wrap routes in a layout
57
- parallel, // Define parallel routes (slots)
58
- loader, // Add data loader
59
- loading, // Add loading skeleton
60
- cache, // Configure caching
61
- middleware, // Add middleware
62
- revalidate, // Control revalidation
63
- intercept, // Intercept routes for modals (conditional via intercept(..., { when }))
64
- errorBoundary, // Add an error boundary
65
- notFoundBoundary, // Add a not-found boundary
66
- transition, // Configure view transitions
101
+ path, // Define a route; also path.json/.text/.html/.xml/.md/.image/.stream/.any (/response-routes)
102
+ layout, // Wrap routes in a layout (/layout)
103
+ include, // Mount another urls() module under a prefix (/composability)
104
+ parallel, // Named slots rendered alongside the outlet (/parallel)
105
+ intercept, // Soft-navigation overlays such as modals (/intercept)
106
+ loader, // Attach a data loader (/loader)
107
+ loading, // Loading fallback for a segment
108
+ cache, // Segment caching (/caching)
109
+ middleware, // Route middleware (/middleware)
110
+ revalidate, // Choose which segments re-render after navigation/actions
111
+ errorBoundary, // Error fallback for a segment
112
+ notFoundBoundary, // Fallback for notFound() thrown below it
113
+ transition, // startTransition driving + view transitions (/view-transitions)
67
114
  }) => [
68
115
  // Route definitions here
69
116
  ],
70
117
  );
71
118
  ```
72
119
 
73
- ## Router Options
120
+ ## Router options
121
+
122
+ Every `createRouter()` option, grouped by concern. All are optional.
74
123
 
75
124
  ```typescript
76
125
  interface RangoOptions<TEnv> {
77
- // URL patterns from urls() function
78
- urls: UrlPatterns;
79
-
80
- // Document component wrapping entire app
81
- document?: ComponentType<{ children: ReactNode }>;
82
-
83
- // URL prefix for sub-path deployments (e.g. "/admin")
84
- // All routes, reverse(), href(), Link, redirect(), and router.use()
85
- // patterns are automatically prefixed. Route names stay unprefixed.
86
- basename?: string;
87
-
88
- // Enable per-request performance timeline (console waterfall + Server-Timing header)
89
- debugPerformance?: boolean;
90
-
91
- // Default error boundary
92
- defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler;
126
+ // --- Structure ---
127
+ urls?: UrlPatterns | UrlBuilder; // urls() result, or the builder fn directly
128
+ document?: ComponentType<{ children: ReactNode }>; // "use client" Document; a default is used when omitted
129
+ basename?: string; // Sub-path prefix, e.g. "/admin" (see Basename)
130
+ id?: string; // Router id; namespaces build output and route maps. Auto-generated.
131
+
132
+ // --- Fallbacks and errors ---
133
+ defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler; // When no errorBoundary() matches
134
+ defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler; // When no notFoundBoundary() matches
135
+ notFound?: ReactNode | ((props: { pathname: string }) => ReactNode); // 404 page
136
+ onError?: OnErrorCallback<TEnv>; // Logging/reporting hook; cannot change the response
137
+
138
+ // --- Caching ---
139
+ cache?:
140
+ | {
141
+ store: SegmentCacheStore;
142
+ enabled?: boolean;
143
+ searchParams?: CacheSearchParams;
144
+ }
145
+ | ((
146
+ env: TEnv,
147
+ ctx?: ExecutionContext,
148
+ ) => {
149
+ store: SegmentCacheStore;
150
+ enabled?: boolean;
151
+ searchParams?: CacheSearchParams;
152
+ });
153
+ cacheProfiles?: Record<
154
+ string,
155
+ { ttl: number; swr?: number; tags?: string[] }
156
+ >; // "use cache: <name>" (/use-cache)
157
+
158
+ // --- Client navigation and prefetch ---
159
+ defaultPrefetch?: "hover" | "viewport" | "render" | "adaptive" | "none"; // dev: "none", prod: "viewport"
160
+ prefetchCacheTTL?: number | false; // Seconds (default 300). false disables all prefetching.
161
+ prefetchCacheSize?: number; // Max cached prefetch payloads, FIFO (default 100)
162
+ prefetchConcurrency?: number; // Max concurrent viewport/render prefetches (default 2)
163
+ viewTransition?: "auto" | false; // Router-placed <ViewTransition> default (/view-transitions)
164
+ warmup?: boolean; // Connection warmup after idle (default true)
165
+ strictMode?: boolean; // Hydrate inside <React.StrictMode> (default true)
166
+ stateCookiePrefix?: string; // Prefix of the state cookie `{prefix}_{routerId}` (default "rango-state")
167
+
168
+ // --- Request handling ---
169
+ nonce?: (
170
+ request: Request,
171
+ env: TEnv,
172
+ ) => string | boolean | Promise<string | boolean>;
173
+ version?: string; // Client/server version string; defaults to the build VERSION
174
+ originCheck?:
175
+ | boolean
176
+ | ((
177
+ ctx: OriginCheckContext<TEnv>,
178
+ ) => boolean | Response | Promise<boolean | Response>); // default true
179
+ timeout?: number; // ms; shorthand for timeouts.actionMs + timeouts.renderStartMs
180
+ timeouts?: {
181
+ actionMs?: number;
182
+ renderStartMs?: number;
183
+ streamIdleMs?: number;
184
+ };
185
+ onTimeout?: (ctx: TimeoutContext<TEnv>) => Response | Promise<Response>;
186
+ ssr?: {
187
+ resolveStreaming?: (
188
+ ctx: ResolveStreamingContext<TEnv>,
189
+ ) => SSRStreamMode | Promise<SSRStreamMode>;
190
+ };
191
+ theme?: ThemeConfig | true; // Light/dark theme support (/theme)
192
+
193
+ // --- Observability and debugging ---
194
+ debugPerformance?: boolean; // Console waterfall + Server-Timing (/observability)
195
+ telemetry?: TelemetrySink; // Structured lifecycle events (/observability)
196
+ tracing?: RouterTracingConfig; // Phase spans: createOTelTracing / createCloudflareTracing / createVercelTracing
197
+ debugCacheSignal?: boolean; // Dev/test only: X-Rango-Cache header for cache assertions (/testing)
198
+ debugShellCapture?: boolean | ((event: ShellCaptureDebugEvent) => void); // PPR capture diagnostics (/ppr)
199
+ }
200
+ ```
93
201
 
94
- // Default not-found boundary for notFound() thrown in handlers/loaders
95
- defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler;
202
+ ## Vite plugin options
96
203
 
97
- // Component for 404 (no route match, or notFound() without a boundary)
98
- notFound?: ReactNode | ((props: { pathname: string }) => ReactNode);
204
+ `rango()` takes one options object. `preset` picks the deployment target; the
205
+ options below apply to every preset. Values shown are the defaults unless
206
+ noted.
99
207
 
100
- // Error logging callback
101
- onError?: OnErrorCallback<TEnv>;
208
+ ```typescript
209
+ import { rango } from "@rangojs/router/vite";
210
+
211
+ rango({
212
+ preset: "node", // "node" | "cloudflare" | "vercel"
213
+ banner: true, // Print the startup banner
214
+ clientChunks: true, // Per-route client chunk splitting; false, or a function
215
+ headScripts: "preinit", // "preinit" (executing head module scripts) | "preload" (modulepreload hints only)
216
+ prerender: { onError: "fail" }, // "fail" | "warn" when a Prerender/Static render throws (/prerender)
217
+ buildEnv: false, // Build-time ctx.env for Prerender/Static handlers (/prerender)
218
+ });
219
+ ```
102
220
 
103
- // Global cache configuration
104
- cache?: CacheConfig<TEnv>;
221
+ Preset-specific options:
222
+
223
+ - `buildEnv`: an object, or a factory `({ root, mode, command, preset }) =>
224
+ ({ env, dispose? })`, on every preset; `"auto"` (Wrangler platform proxy)
225
+ on `cloudflare` only.
226
+ - `hostRouter` (`node`, `vercel`): path to a module that exports a
227
+ `createHostRouter()` instance, for multi-app hosts (`/host-router`). Without
228
+ it, rango auto-detects a single `createHostRouter()` file when it finds
229
+ several `createRouter()` files.
230
+ - `vercel` (`vercel`): `{ runtime, maxDuration, memory, regions, functionName }`
231
+ for the generated function (`/vercel`).
232
+
233
+ Notes:
234
+
235
+ - `clientChunks` default groups app client components by the directory after a
236
+ route root (`routes/`, `app/`, `pages/`, `features/`, `handlers/`, ...), so
237
+ `routes/dashboard/**` becomes chunk `app-dashboard`. Flat folders such as
238
+ `src/components/` stay in the shared chunk. `false` restores
239
+ `@vitejs/plugin-rsc`'s grouping (one client chunk per router). A function
240
+ `(meta) => string | undefined` names the group per module (`undefined` keeps
241
+ the default); `directoryClientChunks(meta)`, the default strategy, is
242
+ exported from `@rangojs/router/vite` to fall back to.
243
+ - `discovery: { include?, exclude? }` filters which files the route-discovery
244
+ scan reads, as root-relative globs (e.g. `include: ["src/routes/**"]`).
245
+ `exclude` replaces the default excludes (`__tests__`, `__mocks__`, `dist`,
246
+ `coverage`, `*.test.*`, `*.spec.*`) rather than adding to them.
247
+ - `progressiveChunkSize` sets React's Fizz `progressiveChunkSize` for document
248
+ renders and PPR shell capture. Unset, documents whose route has a
249
+ `loader(Def, { ssr: false })` raise it automatically so that content stays
250
+ in place; see `/loader`.
251
+ - `@rangojs/router/vite` also exports `poke()`, a dev-server plugin: type `e`
252
+ then Enter (or Ctrl+R where the terminal passes it through) to full-reload
253
+ the browser.
254
+
255
+ ## Document component
256
+
257
+ The Document renders `<html>`, `<head>`, and `<body>` around every page and
258
+ every error state. It must be a client component. Include `<Html.Meta />` (it
259
+ emits the default `charset`/`viewport` tags, `Meta` handle output, and the
260
+ theme script), `<Html.Scripts />` (the `Script` handle, see `/scripts`) and
261
+ `<Html.ScrollRestoration />` (back/forward scroll restoration):
262
+
263
+ ```tsx
264
+ // src/document.tsx
265
+ "use client";
105
266
 
106
- // Theme configuration
107
- theme?: ThemeConfig | true;
267
+ import type { ReactNode } from "react";
268
+ import { Html } from "@rangojs/router/client";
108
269
 
109
- // SSR options (streaming policy)
110
- ssr?: SSROptions<TEnv>;
270
+ export function Document({ children }: { children: ReactNode }) {
271
+ return (
272
+ <html lang="en" suppressHydrationWarning>
273
+ <head>
274
+ <Html.Meta />
275
+ <Html.Scripts />
276
+ </head>
277
+ <body>
278
+ <Html.Scripts position="body" />
279
+ {children}
280
+ <Html.ScrollRestoration />
281
+ </body>
282
+ </html>
283
+ );
284
+ }
285
+ ```
111
286
 
112
- // Telemetry sink for structured lifecycle events
113
- telemetry?: TelemetrySink;
287
+ When `document` is omitted, the router uses exactly this default. To pass
288
+ `getKey` to `Html.ScrollRestoration`, or to leave back/forward scrolling to the
289
+ browser, provide your own document. Page titles
290
+ and other per-route tags come from the `Meta` handle, not from the Document.
291
+ Stylesheets and fonts: `/css`, `/tailwind`, `/fonts`.
114
292
 
115
- // Connection warmup (default: true)
116
- warmup?: boolean;
293
+ ## Serving requests
117
294
 
118
- // Prefetch cache TTL in seconds (default: 300)
119
- // Controls in-memory cache duration and Cache-Control max-age for prefetch responses.
120
- // Set to false to disable prefetch caching.
121
- prefetchCacheTTL?: number | false;
295
+ `router.fetch(request, input?)` is the request handler. `input` is
296
+ `{ env?, ctx?, vars? }`: bindings, the platform `ExecutionContext` (used for
297
+ `waitUntil`, `CFCacheStore`, and Cloudflare tracing), and optional initial
298
+ context variables.
122
299
 
123
- // Default prefetch strategy for Links without a `prefetch` prop and for
124
- // intercepted plain anchors inside basename. false/none opts out; true allows
125
- // an application route whose path has a common static-resource extension.
126
- // data-prefetch-scope="false"/"none" on a container is a hard subtree
127
- // opt-out for both Links and plain anchors, including explicit opt-ins.
128
- // (dev: "none", production: "viewport").
129
- // Per-Link props win over the router default, not a disabled container scope.
130
- defaultPrefetch?: "hover" | "viewport" | "render" | "adaptive" | "none";
300
+ - **Node and Vercel presets:** rango owns the server entry. It imports the
301
+ named `router` export and calls it for you. Write no entry file.
302
+ - **Cloudflare preset:** you own the Worker entry. Pass `env` and `ctx`
303
+ explicitly:
131
304
 
132
- // CSP nonce provider (for router.fetch)
133
- nonce?: (
134
- request: Request,
135
- env: TEnv,
136
- ) => string | true | Promise<string | true>;
305
+ ```typescript
306
+ // src/worker.rsc.tsx
307
+ import { router } from "./router";
308
+ import type { AppBindings } from "./env";
137
309
 
138
- // RSC version string (for router.fetch)
139
- version?: string;
140
- }
310
+ export default {
311
+ fetch(request, env, ctx) {
312
+ return router.fetch(request, { env, ctx });
313
+ },
314
+ } satisfies ExportedHandler<AppBindings>;
141
315
  ```
142
316
 
143
- ## Basename (Sub-Path Deployment)
317
+ Do not write `export default { fetch: router.fetch }` on Workers. The runtime
318
+ calls `fetch(request, env, ctx)`, so the bindings object would land in the
319
+ `input` slot: handlers see an empty `ctx.env`, and the `ExecutionContext` is
320
+ lost. See `/cloudflare` for
321
+ the full Workers setup.
144
322
 
145
- When your app is served under a sub-path (e.g. `/admin` or `/v2`), set `basename`:
323
+ ## Basename (sub-path deployment)
324
+
325
+ When the app is served under a sub-path (e.g. `/admin` or `/v2`), set `basename`:
146
326
 
147
327
  ```typescript
148
- const router = createRouter({
328
+ export const router = createRouter({
149
329
  basename: "/admin",
150
330
  document: Document,
151
331
  }).routes(({ path, include }) => [
@@ -167,79 +347,14 @@ Router-owned APIs are basename-aware:
167
347
  - `useRouter().push("/users")` navigates to `/admin/users`
168
348
  - Route names stay unprefixed (`"home"`, not `"admin.home"`)
169
349
 
170
- Note: `href()` is a raw path helper and does **not** auto-prefix with basename.
171
- Use `reverse()` or `<Link>` for basename-aware URLs.
172
-
173
- ## Using the Request Handler
174
-
175
- The router provides a `fetch` method to handle RSC requests:
176
-
177
- ```typescript
178
- // src/router.tsx
179
- import { createRouter } from "@rangojs/router";
180
- import { Document } from "./document";
181
- import { urlpatterns } from "./urls";
182
-
183
- export const router = createRouter({
184
- document: Document,
185
- urls: urlpatterns,
186
- nonce: () => true, // Auto-generate nonce for CSP
187
- });
188
-
189
- // src/worker.tsx (Cloudflare Workers)
190
- import { router } from "./router";
191
-
192
- export default { fetch: router.fetch };
193
- ```
194
-
195
- ## Document Component
196
-
197
- ```typescript
198
- // src/document.tsx
199
- import type { ReactNode } from "react";
200
-
201
- export function Document({ children }: { children: ReactNode }) {
202
- return (
203
- <html lang="en">
204
- <head>
205
- <meta charSet="utf-8" />
206
- <meta name="viewport" content="width=device-width, initial-scale=1" />
207
- <title>My App</title>
208
- </head>
209
- <body>
210
- <div id="root">{children}</div>
211
- </body>
212
- </html>
213
- );
214
- }
215
- ```
350
+ `href()` is a raw path helper and does **not** add the basename. Use
351
+ `reverse()` or `<Link>` for basename-aware URLs.
216
352
 
217
- ## Using with Cloudflare Workers
353
+ ## Cache store configuration
218
354
 
219
- ```typescript
220
- // src/router.tsx
221
- import { createRouter } from "@rangojs/router";
222
- import { Document } from "./document";
223
- import { urlpatterns } from "./urls";
224
-
225
- export const router = createRouter<AppBindings>({
226
- document: Document,
227
- urls: urlpatterns,
228
- });
229
-
230
- // src/worker.tsx
231
- import { router } from "./router";
232
-
233
- export default {
234
- async fetch(request: Request, env: Env, ctx: ExecutionContext) {
235
- return router.fetch(request, { env, ctx });
236
- },
237
- };
238
- ```
239
-
240
- ### With Dynamic Cache Configuration
241
-
242
- For per-request cache configuration (e.g., Cloudflare Workers with ExecutionContext):
355
+ Pass a static config, or a factory when the store needs per-request bindings.
356
+ The factory receives `(env, ctx)`; `ctx` is typed optional, so assert it where
357
+ the platform always provides it:
243
358
 
244
359
  ```typescript
245
360
  // src/router.tsx
@@ -249,37 +364,31 @@ import { CFCacheStore } from "@rangojs/router/cache";
249
364
  export const router = createRouter<AppBindings>({
250
365
  document: Document,
251
366
  urls: urlpatterns,
252
- // Cache config receives (env, ctx) separately
253
367
  cache: (_env, ctx) => ({
254
368
  store: new CFCacheStore({ ctx: ctx!, defaults: { ttl: 60 } }),
255
369
  }),
256
370
  });
257
-
258
- // src/worker.tsx
259
- import { router } from "./router";
260
-
261
- export default {
262
- async fetch(request: Request, env: Env, ctx: ExecutionContext) {
263
- return router.fetch(request, { env, ctx });
264
- },
265
- };
266
371
  ```
267
372
 
268
- ## Complete Example
373
+ `searchParams` controls which query params key the cache (`"all"` by default,
374
+ `"none"`, `{ include: [...] }`, or `{ exclude: [...] }`).
375
+ `TRACKING_SEARCH_PARAMS` from `@rangojs/router` is a ready-made exclude list
376
+ for utm/click-id params. See `/caching` and `/cache-guide`.
377
+
378
+ ## Complete example
269
379
 
270
380
  ```typescript
271
381
  // src/urls.tsx
272
382
  import { urls } from "@rangojs/router";
273
- import { Outlet } from "@rangojs/router/client";
274
383
 
275
384
  // Pages
276
385
  import { HomePage } from "./pages/home";
277
386
  import { AboutPage } from "./pages/about";
278
- import { BlogIndexPage, BlogPostPage } from "./pages/blog";
387
+ import { BlogIndexPage, BlogPostPage, BlogPostSkeleton } from "./pages/blog";
279
388
 
280
- // Layouts
281
- import { RootLayout } from "./layouts/RootLayout";
389
+ // Layouts and slots
282
390
  import { BlogLayout } from "./layouts/BlogLayout";
391
+ import { BlogSidebar } from "./components/BlogSidebar";
283
392
 
284
393
  // Loaders
285
394
  import { BlogPostLoader, BlogSidebarLoader } from "./loaders/blog";
@@ -291,7 +400,7 @@ export const urlpatterns = urls(({ path, layout, parallel, loader, loading, cach
291
400
 
292
401
  // Blog with layout and loaders
293
402
  layout(<BlogLayout />, () => [
294
- // Sidebar as parallel route
403
+ // Sidebar as a parallel slot (the layout renders <ParallelOutlet name="@sidebar" />)
295
404
  parallel({ "@sidebar": () => <BlogSidebar /> }, () => [
296
405
  loader(BlogSidebarLoader),
297
406
  ]),
@@ -314,14 +423,17 @@ import { createRouter } from "@rangojs/router";
314
423
  import { Document } from "./document";
315
424
  import { urlpatterns } from "./urls";
316
425
 
317
- const router = createRouter({
426
+ export const router = createRouter({
318
427
  document: Document,
319
428
  urls: urlpatterns,
320
429
 
321
- defaultErrorBoundary: ({ error, reset }) => (
430
+ // Server-rendered fallback: receives { error } only. There is no reset()
431
+ // for server-side errors, so offer a link or a reload instead.
432
+ defaultErrorBoundary: ({ error }) => (
322
433
  <div>
323
434
  <h1>Something went wrong</h1>
324
- <button onClick={reset}>Try again</button>
435
+ <p>{error.message}</p>
436
+ <a href="/">Go home</a>
325
437
  </div>
326
438
  ),
327
439
 
@@ -332,20 +444,20 @@ const router = createRouter({
332
444
  </div>
333
445
  ),
334
446
  });
335
-
336
- export default router;
337
447
  ```
338
448
 
339
- ## Not Found Handling
449
+ ## Not found handling
340
450
 
341
451
  Two distinct 404 scenarios:
342
452
 
343
- **1. No route matches the URL** — the router renders the `notFound` component from `createRouter()` config. This is automatic.
453
+ **1. No route matches the URL.** The router renders the `notFound` option from
454
+ `createRouter()` (or `<h1>Not Found</h1>` when unset) with status 404.
344
455
 
345
- **2. A handler/loader calls `notFound()`** — signals that the route matched but the data doesn't exist (e.g., invalid product ID).
456
+ **2. A handler or loader calls `notFound()`.** The route matched but the data
457
+ does not exist (e.g. an unknown product id).
346
458
 
347
459
  ```typescript
348
- import { notFound } from "@rangojs/router";
460
+ import { createLoader, notFound } from "@rangojs/router";
349
461
 
350
462
  // In a handler
351
463
  path("/product/:slug", async (ctx) => {
@@ -354,7 +466,7 @@ path("/product/:slug", async (ctx) => {
354
466
  return <ProductPage product={product} />;
355
467
  });
356
468
 
357
- // In a loader — data-dependent authority lives with the data
469
+ // In a loader: data-dependent authority lives with the data
358
470
  export const ProductLoader = createLoader(async (ctx) => {
359
471
  if (!(await exists(ctx.params.slug))) notFound("Product not found");
360
472
  return getProduct(ctx.params.slug);
@@ -363,20 +475,19 @@ export const ProductLoader = createLoader(async (ctx) => {
363
475
 
364
476
  ### Fallback chain for `notFound()`
365
477
 
366
- When `notFound()` is thrown, the router looks for a fallback in this order:
478
+ When `notFound()` is thrown, the router uses the first of:
367
479
 
368
- 1. **`notFoundBoundary()`** — nearest boundary in the route tree (route-level)
369
- 2. **`defaultNotFoundBoundary`** — from `createRouter()` config (app-level)
370
- 3. **`notFound`** — from `createRouter()` config (same component used for no-route-match)
371
- 4. **Default `<h1>Not Found</h1>`** — built-in fallback
480
+ 1. **`notFoundBoundary()`**: nearest boundary in the route tree
481
+ 2. **`defaultNotFoundBoundary`**: from `createRouter()`
482
+ 3. **`notFound`**: from `createRouter()` (the same page used for no-route-match)
483
+ 4. **`<h1>Not Found</h1>`**: built-in fallback
372
484
 
373
- Handler and no-match cases set HTTP 404 status. A LOADER-thrown `notFound()`
374
- on a document load always streams the resolved not-found UI, but the 404
375
- STATUS is opportunistic — real only when the rejection settles before the
376
- document Response is constructed (loaders stream). Register the loader as
377
- `loader(Def, { ssr: false })` to make the 404 status deterministic;
378
- on client navigations the 404 UI swaps in with the URL preserved (payload
379
- stays 200 — the client owns presentation there). See `/loader` → "Loader
485
+ Handler-thrown and no-match 404s always set HTTP status 404. A loader-thrown
486
+ `notFound()` on a document request always renders the not-found UI, but the
487
+ 404 status is set only if the loader rejects before the document Response is
488
+ constructed (loaders stream). Register the loader as `loader(Def, { ssr: false })` to
489
+ make the 404 status deterministic. On client navigations the 404 UI swaps in
490
+ with the URL preserved and the payload stays 200. See `/loader` → "Loader
380
491
  Authority".
381
492
 
382
493
  ### notFoundBoundary
@@ -384,7 +495,7 @@ Authority".
384
495
  Wrap routes with `notFoundBoundary()` for route-specific not-found UI:
385
496
 
386
497
  ```typescript
387
- urls(({ path, layout }) => [
498
+ urls(({ path, layout, notFoundBoundary }) => [
388
499
  layout(ShopLayout, () => [
389
500
  notFoundBoundary(({ notFound: info }) => (
390
501
  <div>
@@ -392,20 +503,21 @@ urls(({ path, layout }) => [
392
503
  <p>{info.message}</p>
393
504
  </div>
394
505
  )),
395
- path("/product/:slug", ProductPage),
506
+ path("/product/:slug", ProductPage, { name: "product" }),
396
507
  ]),
397
508
  ]);
398
509
  ```
399
510
 
400
- `notFoundBoundary` receives `{ notFound: NotFoundInfo }` where `NotFoundInfo` contains `message`, `segmentId`, `segmentType`, and `pathname`.
511
+ The handler receives `{ notFound: NotFoundInfo }`: `message`, `segmentId`,
512
+ `segmentType`, and `pathname` (optional).
401
513
 
402
- ## Including Sub-patterns
514
+ ## Including sub-patterns
403
515
 
404
516
  ```typescript
405
517
  // src/urls/shop.tsx
406
518
  import { urls } from "@rangojs/router";
407
519
 
408
- export const shopPatterns = urls(({ path, layout }) => [
520
+ export const shopPatterns = urls(({ path }) => [
409
521
  path("/", ShopIndex, { name: "index" }),
410
522
  path("/product/:slug", ProductPage, { name: "product" }),
411
523
  ]);
@@ -416,23 +528,24 @@ import { shopPatterns } from "./urls/shop";
416
528
 
417
529
  export const urlpatterns = urls(({ path, include }) => [
418
530
  path("/", HomePage, { name: "home" }),
419
- include("/shop", shopPatterns, { name: "shop" }),
531
+ include("/shop", shopPatterns, { name: "shop" }), // "shop.index", "shop.product"
420
532
  ]);
421
533
  ```
422
534
 
423
- `include()` also accepts an async provider to code-split that group into its own
424
- chunk, imported on the first request reaching the prefix instead of at startup:
535
+ `include()` also accepts an async provider that code-splits the group into its
536
+ own chunk, imported on the first request that reaches the prefix instead of at
537
+ startup:
425
538
 
426
539
  ```typescript
427
540
  // urls/shop.tsx: `export default shopPatterns`
428
541
  include("/shop", () => import("./urls/shop"), { name: "shop" }),
429
542
  ```
430
543
 
431
- Build-time discovery still `await`s the provider, so route types, `href()`, and
544
+ Build-time discovery still awaits the provider, so route types, `href()`, and
432
545
  prerender see every route in the split group. Reach for it when a group is a
433
- large, independently-loadable unit — see `/composability`.
546
+ large, independently loadable unit. See `/composability`.
434
547
 
435
- ## Environment Types
548
+ ## Environment types
436
549
 
437
550
  ```typescript
438
551
  // Bindings passed as TEnv to createRouter<TEnv>()
@@ -441,17 +554,17 @@ interface AppBindings {
441
554
  KV: KVNamespace;
442
555
  }
443
556
 
444
- // Variables declared via global namespace augmentation
557
+ // Context variables set by middleware via ctx.set()
445
558
  interface AppVariables {
446
559
  user?: { id: string; name: string };
447
560
  }
448
561
 
449
- const router = createRouter<AppBindings>({
562
+ export const router = createRouter<AppBindings>({
450
563
  document: Document,
451
564
  urls: urlpatterns,
452
565
  });
453
566
 
454
- // Register types globally for implicit typing
567
+ // Register once so ctx.env and ctx.get() are typed everywhere
455
568
  declare global {
456
569
  namespace Rango {
457
570
  interface Env extends AppBindings {}
@@ -460,44 +573,37 @@ declare global {
460
573
  }
461
574
  ```
462
575
 
463
- ## Connection Warmup
576
+ See `/typesafety` for route-name and search-param typing.
464
577
 
465
- Enabled by default. Keeps TCP+TLS connections alive so navigations after idle periods
466
- don't pay handshake costs.
578
+ ## Connection warmup
467
579
 
468
- After 60s of no user interaction, the connection is marked cold. When the user returns
469
- (tab becomes visible or first mouse/touch), a `HEAD ?_rsc_warmup` request re-establishes
470
- the TLS connection before the next navigation. The server responds with 204 No Content
471
- before any middleware or routing runs.
580
+ Enabled by default. It keeps the TCP+TLS connection warm so the first
581
+ navigation after an idle period does not pay the handshake.
472
582
 
473
- ```typescript
474
- // Enabled by default
475
- const router = createRouter({
476
- document: Document,
477
- urls: urlpatterns,
478
- });
583
+ After 60s without user interaction the connection is marked cold. When the user
584
+ returns (tab becomes visible, or first mouse move/touch), the client sends
585
+ `HEAD /?_rsc_warmup` to the page's origin. The router answers `204 No Content`
586
+ before middleware, nonce resolution, or routing runs. The request only needs to
587
+ reach the origin to warm the connection, so it works with `basename` too.
479
588
 
589
+ ```typescript
480
590
  // Disable warmup
481
- const router = createRouter({
591
+ export const router = createRouter({
482
592
  document: Document,
483
593
  urls: urlpatterns,
484
594
  warmup: false,
485
595
  });
486
596
  ```
487
597
 
488
- The warmup request is relative to the current page path, so it works correctly
489
- with subpath deployments (reverse proxy, base path).
598
+ ## Telemetry and tracing
490
599
 
491
- ## Telemetry
492
-
493
- The router emits structured lifecycle events through a pluggable telemetry sink.
494
- Zero overhead when not configured.
600
+ Two independent slots. `telemetry` receives discrete lifecycle events;
601
+ `tracing` wraps router phases in spans. Both cost nothing when unset.
495
602
 
496
603
  ```typescript
497
- // Console sink for development
498
604
  import { createRouter, createConsoleSink } from "@rangojs/router";
499
605
 
500
- const router = createRouter({
606
+ export const router = createRouter({
501
607
  document: Document,
502
608
  urls: urlpatterns,
503
609
  telemetry: createConsoleSink(),
@@ -505,8 +611,7 @@ const router = createRouter({
505
611
  ```
506
612
 
507
613
  ```typescript
508
- // OpenTelemetry for production: phase spans via the tracing slot,
509
- // discrete-fact spans via the telemetry sink.
614
+ // OpenTelemetry: phase spans via tracing, discrete-fact spans via telemetry.
510
615
  import {
511
616
  createRouter,
512
617
  createOTelTracing,
@@ -516,7 +621,7 @@ import { trace } from "@opentelemetry/api";
516
621
 
517
622
  const tracer = trace.getTracer("my-app");
518
623
 
519
- const router = createRouter({
624
+ export const router = createRouter({
520
625
  document: Document,
521
626
  urls: urlpatterns,
522
627
  tracing: createOTelTracing(tracer),
@@ -525,45 +630,46 @@ const router = createRouter({
525
630
  ```
526
631
 
527
632
  ```typescript
528
- // On Cloudflare Workers, swap the tracing factory for native custom spans
529
- // (no @opentelemetry/api dependency); the telemetry slot is unchanged.
633
+ // Cloudflare Workers: native custom spans, no @opentelemetry/api dependency.
530
634
  // On Vercel (Node runtime) use createVercelTracing() from @rangojs/router/vercel.
531
635
  import { createCloudflareTracing } from "@rangojs/router/cloudflare";
532
636
 
533
- const router = createRouter({
637
+ export const router = createRouter({
534
638
  document: Document,
535
639
  urls: urlpatterns,
536
- tracing: createCloudflareTracing(), // { spans: { ssr: false } } to toggle phases
640
+ tracing: createCloudflareTracing(), // { spans: { ssr: false } } to turn phases off
537
641
  });
538
642
  ```
539
643
 
540
644
  ```typescript
541
645
  // Custom sink
542
- const router = createRouter({
646
+ export const router = createRouter({
543
647
  telemetry: {
544
648
  emit(event) {
545
- // Send to any observability backend
546
- myTracer.record(event);
649
+ myMetrics.record(event);
547
650
  },
548
651
  },
549
652
  });
550
653
  ```
551
654
 
552
- Events emitted: `request.start/end/error`, `loader.start/end/error`,
553
- `handler.error`, `cache.decision`, `revalidation.decision`.
655
+ Events: `request.start/end/error`, `loader.start/end/error`, `handler.error`,
656
+ `cache.decision`, `revalidation.decision`, `request.timeout`, and
657
+ `request.origin-rejected`. Span names, attributes, and debugging recipes are in
658
+ `/observability`.
554
659
 
555
- ## SSR Streaming Policy
660
+ ## SSR streaming policy
556
661
 
557
- Control whether HTML SSR responses stream progressively or wait for all content:
662
+ HTML responses stream by default. `ssr.resolveStreaming` picks, per request,
663
+ whether to stream or wait for the whole page:
558
664
 
559
665
  ```typescript
560
666
  import { createRouter, type SSRStreamMode } from "@rangojs/router";
561
667
 
562
- const router = createRouter({
668
+ export const router = createRouter({
563
669
  ssr: {
564
- resolveStreaming: ({ request }) => {
670
+ resolveStreaming: ({ request }): SSRStreamMode => {
565
671
  const ua = request.headers.get("user-agent") ?? "";
566
- // Bots that can't process streamed HTML get a fully resolved page
672
+ // Crawlers that cannot process streamed HTML get a fully resolved page
567
673
  if (/Googlebot|bingbot/i.test(ua)) return "allReady";
568
674
  return "stream";
569
675
  },
@@ -573,9 +679,81 @@ const router = createRouter({
573
679
 
574
680
  `SSRStreamMode` is `"stream" | "allReady"`:
575
681
 
576
- - `"stream"` (default) — flush HTML as React renders. Suspense fallbacks appear first, then resolved content streams in. Best for real users (fastest TTFB).
577
- - `"allReady"` — await `stream.allReady` before flushing. The full page arrives in one shot. Use for bots that cannot execute JavaScript or process chunked HTML.
682
+ - `"stream"` (default): flush HTML as React renders. Suspense fallbacks appear
683
+ first, then resolved content streams in. Fastest TTFB for real users.
684
+ - `"allReady"`: wait for every Suspense boundary (`stream.allReady`) before
685
+ sending bytes. The full page arrives at once.
578
686
 
579
- The resolver receives `{ request, env, url }` and may be sync or async. It only runs on HTML SSR paths — RSC partials, `__rsc` requests, and response routes are unaffected.
687
+ The resolver receives `{ request, env, url }` and may be async. It runs only
688
+ for HTML document responses; RSC payloads (navigations, prefetches, `__rsc`
689
+ requests) and response routes are unaffected.
690
+
691
+ ## Timeouts
692
+
693
+ Off by default. A timed-out action or render start returns a `504` (with an
694
+ `X-Rango-Timeout-Phase` header) unless `onTimeout` returns its own response.
695
+
696
+ ```typescript
697
+ export const router = createRouter({
698
+ timeout: 10_000, // actionMs + renderStartMs
699
+ timeouts: {
700
+ renderStartMs: 8_000, // overrides the shorthand
701
+ streamIdleMs: 30_000, // opt-in; not covered by `timeout`
702
+ },
703
+ onTimeout: ({ phase }) =>
704
+ Response.json({ error: "timeout", phase }, { status: 504 }),
705
+ });
706
+ ```
707
+
708
+ - `actionMs`: server action execution.
709
+ - `renderStartMs`: time until the response is produced.
710
+ - `streamIdleMs`: after handoff, no chunk reached the client for this long.
711
+ The stream is errored and the render canceled; `onTimeout` does not run (the
712
+ response has already started). It is reported via `onError` and the
713
+ `request.timeout` event. A slow client counts as idle too, so use generous
714
+ budgets.
715
+
716
+ `0` disables a phase. `RouterTimeoutError` is exported from `@rangojs/router`.
717
+
718
+ ## Origin check
719
+
720
+ On by default. Before running a server action, a fetchable loader, or a
721
+ progressive-enhancement form post, the router compares the `Origin` header (or
722
+ `Referer`) with `Host` and the request protocol. Requests with neither header
723
+ are allowed. `X-Forwarded-*` headers are not trusted.
724
+
725
+ ```typescript
726
+ export const router = createRouter<AppBindings>({
727
+ // Behind a proxy that rewrites Host, supply your own rule:
728
+ originCheck: ({ request, url, env, defaultCheck }) => {
729
+ if (!env.TRUST_PROXY) return defaultCheck();
730
+ const origin = request.headers.get("origin");
731
+ if (!origin) return true;
732
+ const host = request.headers.get("x-forwarded-host") ?? url.host;
733
+ return origin === `${url.protocol}//${host}`;
734
+ },
735
+ });
736
+ ```
580
737
 
581
- When `resolveStreaming` is not configured, the default is `"stream"`.
738
+ Return `true` to allow, `false` for the default 403, or a `Response` to reject
739
+ with your own. `phase` is `"action" | "loader" | "pe-form"`. `false` disables
740
+ the check.
741
+
742
+ ## Other options
743
+
744
+ - `defaultPrefetch`: strategy for Links without a `prefetch` prop (`/links`).
745
+ Production default `"viewport"` renders every visible Link's route on the
746
+ server; choose `"hover"`, `"adaptive"`, or `"none"` when that cost matters.
747
+ - `prefetchCacheTTL`, `prefetchCacheSize`, `prefetchConcurrency`: lifetime,
748
+ entry count, and parallelism of the client prefetch cache.
749
+ `prefetchCacheTTL: false` turns prefetching off entirely, including per-Link
750
+ opt-ins and `useRouter().prefetch()`.
751
+ - `strictMode: false`: hydrate without `<React.StrictMode>`, e.g. to get exact
752
+ render counts in development. Production behavior is unchanged.
753
+ - `stateCookiePrefix`: rename the state cookie that keys the client caches. The
754
+ `_{routerId}` suffix is always kept so sibling apps on one origin do not
755
+ collide.
756
+ - `nonce: () => true`: generate a CSP nonce per request and apply it to the
757
+ router's inline scripts. Return a string to supply your own, or
758
+ `false`/`""` to skip it for one request. Middleware reads it with
759
+ `ctx.get(nonce)` (`nonce` token from `@rangojs/router`).