@rangojs/router 0.0.0-experimental.10

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 (172) hide show
  1. package/CLAUDE.md +43 -0
  2. package/README.md +19 -0
  3. package/dist/bin/rango.js +227 -0
  4. package/dist/vite/index.js +3039 -0
  5. package/package.json +171 -0
  6. package/skills/caching/SKILL.md +191 -0
  7. package/skills/debug-manifest/SKILL.md +108 -0
  8. package/skills/document-cache/SKILL.md +180 -0
  9. package/skills/fonts/SKILL.md +165 -0
  10. package/skills/hooks/SKILL.md +442 -0
  11. package/skills/intercept/SKILL.md +190 -0
  12. package/skills/layout/SKILL.md +213 -0
  13. package/skills/links/SKILL.md +180 -0
  14. package/skills/loader/SKILL.md +246 -0
  15. package/skills/middleware/SKILL.md +202 -0
  16. package/skills/mime-routes/SKILL.md +124 -0
  17. package/skills/parallel/SKILL.md +228 -0
  18. package/skills/prerender/SKILL.md +283 -0
  19. package/skills/rango/SKILL.md +54 -0
  20. package/skills/response-routes/SKILL.md +358 -0
  21. package/skills/route/SKILL.md +173 -0
  22. package/skills/router-setup/SKILL.md +346 -0
  23. package/skills/tailwind/SKILL.md +129 -0
  24. package/skills/theme/SKILL.md +78 -0
  25. package/skills/typesafety/SKILL.md +394 -0
  26. package/src/__internal.ts +175 -0
  27. package/src/bin/rango.ts +24 -0
  28. package/src/browser/event-controller.ts +876 -0
  29. package/src/browser/index.ts +18 -0
  30. package/src/browser/link-interceptor.ts +121 -0
  31. package/src/browser/lru-cache.ts +69 -0
  32. package/src/browser/merge-segment-loaders.ts +126 -0
  33. package/src/browser/navigation-bridge.ts +913 -0
  34. package/src/browser/navigation-client.ts +165 -0
  35. package/src/browser/navigation-store.ts +823 -0
  36. package/src/browser/partial-update.ts +600 -0
  37. package/src/browser/react/Link.tsx +248 -0
  38. package/src/browser/react/NavigationProvider.tsx +346 -0
  39. package/src/browser/react/ScrollRestoration.tsx +94 -0
  40. package/src/browser/react/context.ts +53 -0
  41. package/src/browser/react/index.ts +52 -0
  42. package/src/browser/react/location-state-shared.ts +120 -0
  43. package/src/browser/react/location-state.ts +62 -0
  44. package/src/browser/react/mount-context.ts +32 -0
  45. package/src/browser/react/use-action.ts +240 -0
  46. package/src/browser/react/use-client-cache.ts +56 -0
  47. package/src/browser/react/use-handle.ts +203 -0
  48. package/src/browser/react/use-href.tsx +40 -0
  49. package/src/browser/react/use-link-status.ts +134 -0
  50. package/src/browser/react/use-mount.ts +31 -0
  51. package/src/browser/react/use-navigation.ts +140 -0
  52. package/src/browser/react/use-segments.ts +188 -0
  53. package/src/browser/request-controller.ts +164 -0
  54. package/src/browser/rsc-router.tsx +352 -0
  55. package/src/browser/scroll-restoration.ts +324 -0
  56. package/src/browser/segment-structure-assert.ts +67 -0
  57. package/src/browser/server-action-bridge.ts +762 -0
  58. package/src/browser/shallow.ts +35 -0
  59. package/src/browser/types.ts +478 -0
  60. package/src/build/generate-manifest.ts +377 -0
  61. package/src/build/generate-route-types.ts +828 -0
  62. package/src/build/index.ts +36 -0
  63. package/src/build/route-trie.ts +239 -0
  64. package/src/cache/cache-scope.ts +563 -0
  65. package/src/cache/cf/cf-cache-store.ts +428 -0
  66. package/src/cache/cf/index.ts +19 -0
  67. package/src/cache/document-cache.ts +340 -0
  68. package/src/cache/index.ts +58 -0
  69. package/src/cache/memory-segment-store.ts +150 -0
  70. package/src/cache/memory-store.ts +253 -0
  71. package/src/cache/types.ts +392 -0
  72. package/src/client.rsc.tsx +83 -0
  73. package/src/client.tsx +643 -0
  74. package/src/component-utils.ts +76 -0
  75. package/src/components/DefaultDocument.tsx +23 -0
  76. package/src/debug.ts +233 -0
  77. package/src/default-error-boundary.tsx +88 -0
  78. package/src/deps/browser.ts +8 -0
  79. package/src/deps/html-stream-client.ts +2 -0
  80. package/src/deps/html-stream-server.ts +2 -0
  81. package/src/deps/rsc.ts +10 -0
  82. package/src/deps/ssr.ts +2 -0
  83. package/src/errors.ts +295 -0
  84. package/src/handle.ts +130 -0
  85. package/src/handles/MetaTags.tsx +193 -0
  86. package/src/handles/index.ts +6 -0
  87. package/src/handles/meta.ts +247 -0
  88. package/src/host/cookie-handler.ts +159 -0
  89. package/src/host/errors.ts +97 -0
  90. package/src/host/index.ts +56 -0
  91. package/src/host/pattern-matcher.ts +214 -0
  92. package/src/host/router.ts +330 -0
  93. package/src/host/testing.ts +79 -0
  94. package/src/host/types.ts +138 -0
  95. package/src/host/utils.ts +25 -0
  96. package/src/href-client.ts +202 -0
  97. package/src/href-context.ts +33 -0
  98. package/src/index.rsc.ts +121 -0
  99. package/src/index.ts +165 -0
  100. package/src/loader.rsc.ts +207 -0
  101. package/src/loader.ts +47 -0
  102. package/src/network-error-thrower.tsx +21 -0
  103. package/src/outlet-context.ts +15 -0
  104. package/src/prerender/param-hash.ts +35 -0
  105. package/src/prerender/store.ts +40 -0
  106. package/src/prerender.ts +156 -0
  107. package/src/reverse.ts +267 -0
  108. package/src/root-error-boundary.tsx +277 -0
  109. package/src/route-content-wrapper.tsx +193 -0
  110. package/src/route-definition.ts +1431 -0
  111. package/src/route-map-builder.ts +242 -0
  112. package/src/route-types.ts +220 -0
  113. package/src/router/error-handling.ts +287 -0
  114. package/src/router/handler-context.ts +158 -0
  115. package/src/router/intercept-resolution.ts +387 -0
  116. package/src/router/loader-resolution.ts +327 -0
  117. package/src/router/manifest.ts +216 -0
  118. package/src/router/match-api.ts +621 -0
  119. package/src/router/match-context.ts +264 -0
  120. package/src/router/match-middleware/background-revalidation.ts +236 -0
  121. package/src/router/match-middleware/cache-lookup.ts +382 -0
  122. package/src/router/match-middleware/cache-store.ts +276 -0
  123. package/src/router/match-middleware/index.ts +81 -0
  124. package/src/router/match-middleware/intercept-resolution.ts +281 -0
  125. package/src/router/match-middleware/segment-resolution.ts +184 -0
  126. package/src/router/match-pipelines.ts +214 -0
  127. package/src/router/match-result.ts +213 -0
  128. package/src/router/metrics.ts +62 -0
  129. package/src/router/middleware.ts +791 -0
  130. package/src/router/pattern-matching.ts +407 -0
  131. package/src/router/revalidation.ts +190 -0
  132. package/src/router/router-context.ts +301 -0
  133. package/src/router/segment-resolution.ts +1315 -0
  134. package/src/router/trie-matching.ts +172 -0
  135. package/src/router/types.ts +163 -0
  136. package/src/router.gen.ts +6 -0
  137. package/src/router.ts +2423 -0
  138. package/src/rsc/handler.ts +1443 -0
  139. package/src/rsc/helpers.ts +64 -0
  140. package/src/rsc/index.ts +56 -0
  141. package/src/rsc/nonce.ts +18 -0
  142. package/src/rsc/types.ts +236 -0
  143. package/src/segment-system.tsx +442 -0
  144. package/src/server/context.ts +466 -0
  145. package/src/server/handle-store.ts +229 -0
  146. package/src/server/loader-registry.ts +174 -0
  147. package/src/server/request-context.ts +554 -0
  148. package/src/server/root-layout.tsx +10 -0
  149. package/src/server/tsconfig.json +14 -0
  150. package/src/server.ts +171 -0
  151. package/src/ssr/index.tsx +296 -0
  152. package/src/theme/ThemeProvider.tsx +291 -0
  153. package/src/theme/ThemeScript.tsx +61 -0
  154. package/src/theme/constants.ts +59 -0
  155. package/src/theme/index.ts +58 -0
  156. package/src/theme/theme-context.ts +70 -0
  157. package/src/theme/theme-script.ts +152 -0
  158. package/src/theme/types.ts +182 -0
  159. package/src/theme/use-theme.ts +44 -0
  160. package/src/types.ts +1757 -0
  161. package/src/urls.gen.ts +8 -0
  162. package/src/urls.ts +1282 -0
  163. package/src/use-loader.tsx +346 -0
  164. package/src/vite/expose-action-id.ts +344 -0
  165. package/src/vite/expose-handle-id.ts +209 -0
  166. package/src/vite/expose-loader-id.ts +426 -0
  167. package/src/vite/expose-location-state-id.ts +177 -0
  168. package/src/vite/expose-prerender-handler-id.ts +429 -0
  169. package/src/vite/index.ts +2068 -0
  170. package/src/vite/package-resolution.ts +125 -0
  171. package/src/vite/version.d.ts +12 -0
  172. package/src/vite/virtual-entries.ts +114 -0
@@ -0,0 +1,246 @@
1
+ ---
2
+ name: loader
3
+ description: Define data loaders for fetching data in routes with createLoader
4
+ argument-hint: [name]
5
+ ---
6
+
7
+ # Data Loaders with loader()
8
+
9
+ Loaders fetch data on the server and stream it to the client.
10
+
11
+ ## Creating a Loader
12
+
13
+ ```typescript
14
+ import { createLoader } from "@rangojs/router";
15
+
16
+ export const ProductLoader = createLoader("product", async (ctx) => {
17
+ const product = await ctx.env.Bindings.DB
18
+ .prepare("SELECT * FROM products WHERE slug = ?")
19
+ .bind(ctx.params.slug)
20
+ .first();
21
+
22
+ return { product };
23
+ });
24
+ ```
25
+
26
+ ## Using Loaders in Routes
27
+
28
+ ```typescript
29
+ import { urls } from "@rangojs/router";
30
+ import { ProductLoader } from "./loaders/product";
31
+
32
+ export const urlpatterns = urls(({ path, loader }) => [
33
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
34
+ loader(ProductLoader),
35
+ ]),
36
+ ]);
37
+ ```
38
+
39
+ ## Consuming Loader Data
40
+
41
+ ### In Server Components
42
+
43
+ ```typescript
44
+ import { useLoader } from "@rangojs/router";
45
+ import { ProductLoader } from "./loaders/product";
46
+
47
+ async function ProductPage() {
48
+ const { product } = await useLoader(ProductLoader);
49
+ return <h1>{product.name}</h1>;
50
+ }
51
+ ```
52
+
53
+ ### In Client Components
54
+
55
+ ```typescript
56
+ "use client";
57
+ import { useLoaderData } from "@rangojs/router/client";
58
+ import { ProductLoader } from "./loaders/product";
59
+
60
+ function ProductDetails() {
61
+ const { product } = useLoaderData(ProductLoader);
62
+ return <div>{product.description}</div>;
63
+ }
64
+ ```
65
+
66
+ ## Loader Context
67
+
68
+ Loaders receive the same context as route handlers:
69
+
70
+ ```typescript
71
+ export const ProductLoader = createLoader("product", async (ctx) => {
72
+ // URL params
73
+ const { slug } = ctx.params;
74
+
75
+ // Query params
76
+ const variant = ctx.url.searchParams.get("variant");
77
+
78
+ // Environment (DB, KV, etc.)
79
+ const db = ctx.env.Bindings.DB;
80
+
81
+ // Request headers
82
+ const auth = ctx.request.headers.get("Authorization");
83
+
84
+ // Variables set by middleware
85
+ const user = ctx.env.Variables.user;
86
+
87
+ return { product: await fetchProduct(slug) };
88
+ });
89
+ ```
90
+
91
+ ## Loader with Children
92
+
93
+ Add caching or revalidation to specific loaders:
94
+
95
+ ```typescript
96
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
97
+ // Cached loader
98
+ loader(ProductLoader, () => [
99
+ cache({ ttl: 300 }),
100
+ ]),
101
+
102
+ // Loader with revalidation control
103
+ loader(RelatedProductsLoader, () => [
104
+ revalidate(() => false), // Never revalidate
105
+ ]),
106
+
107
+ // Loader that revalidates after cart actions
108
+ loader(CartLoader, () => [
109
+ revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
110
+ ]),
111
+ ])
112
+ ```
113
+
114
+ ## Multiple Loaders
115
+
116
+ Routes can have multiple loaders that run in parallel:
117
+
118
+ ```typescript
119
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
120
+ loader(ProductLoader),
121
+ loader(RelatedProductsLoader),
122
+ loader(ReviewsLoader),
123
+ ])
124
+ ```
125
+
126
+ ## Layout Loaders
127
+
128
+ Loaders on layouts are shared by all child routes:
129
+
130
+ ```typescript
131
+ layout(<ShopLayout />, () => [
132
+ // These loaders are available to all shop routes
133
+ loader(CartLoader),
134
+ loader(CategoriesLoader),
135
+
136
+ path("/shop", ShopIndex, { name: "index" }),
137
+ path("/shop/product/:slug", ProductPage, { name: "product" }),
138
+ ])
139
+ ```
140
+
141
+ ## Passing Loaders as Props
142
+
143
+ Loaders can be passed as props from server to client components. RSC serialization
144
+ uses `toJSON()` to send only `{ __brand, $$id }` — the loader function is stripped.
145
+
146
+ ```typescript
147
+ // Server component (route handler)
148
+ import { SlowLoader } from "../loaders";
149
+
150
+ path("/dashboard", () => <DashboardContent loader={SlowLoader} />, { name: "dashboard" }, () => [
151
+ loader(SlowLoader),
152
+ loading(<DashboardSkeleton />),
153
+ ])
154
+
155
+ // Client component — use typeof for type-safe props
156
+ "use client";
157
+ import { useLoader } from "@rangojs/router/client";
158
+ import type { SlowLoader } from "../loaders";
159
+
160
+ function DashboardContent({ loader }: { loader: typeof SlowLoader }) {
161
+ const { data } = useLoader(loader);
162
+ return <div>{data.message}</div>;
163
+ }
164
+ ```
165
+
166
+ Use `typeof MyLoader` for the prop type — it infers the full generic automatically.
167
+
168
+ ## Streaming with Suspense
169
+
170
+ Loaders stream data. Use Suspense for loading states:
171
+
172
+ ```typescript
173
+ // In route definition
174
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
175
+ loader(ProductLoader),
176
+ loading(<ProductSkeleton />), // Shows while loader streams
177
+ ])
178
+
179
+ // Or in component
180
+ function ProductPage() {
181
+ return (
182
+ <Suspense fallback={<ProductSkeleton />}>
183
+ <ProductDetails />
184
+ </Suspense>
185
+ );
186
+ }
187
+ ```
188
+
189
+ ## Complete Example
190
+
191
+ ```typescript
192
+ // loaders/shop.ts
193
+ import { createLoader } from "@rangojs/router";
194
+
195
+ export const ProductLoader = createLoader("product", async (ctx) => {
196
+ const product = await ctx.env.Bindings.DB
197
+ .prepare("SELECT * FROM products WHERE slug = ?")
198
+ .bind(ctx.params.slug)
199
+ .first();
200
+
201
+ if (!product) {
202
+ throw new Response("Product not found", { status: 404 });
203
+ }
204
+
205
+ return { product };
206
+ });
207
+
208
+ export const CartLoader = createLoader("cart", async (ctx) => {
209
+ const user = ctx.env.Variables.user;
210
+ if (!user) return { cart: null };
211
+
212
+ const cart = await ctx.env.Bindings.KV.get(`cart:${user.id}`, "json");
213
+ return { cart };
214
+ });
215
+
216
+ // urls.tsx
217
+ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
218
+ layout(<ShopLayout />, () => [
219
+ // Shared cart loader for all shop routes
220
+ loader(CartLoader, () => [
221
+ revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
222
+ ]),
223
+
224
+ path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
225
+ loader(ProductLoader, () => [cache({ ttl: 60 })]),
226
+ loading(<ProductSkeleton />),
227
+ ]),
228
+ ]),
229
+ ]);
230
+
231
+ // pages/product.tsx
232
+ import { useLoader } from "@rangojs/router";
233
+ import { ProductLoader, CartLoader } from "./loaders/shop";
234
+
235
+ async function ProductPage() {
236
+ const { product } = await useLoader(ProductLoader);
237
+ const { cart } = await useLoader(CartLoader);
238
+
239
+ return (
240
+ <div>
241
+ <h1>{product.name}</h1>
242
+ <AddToCartButton productId={product.id} inCart={cart?.items.includes(product.id)} />
243
+ </div>
244
+ );
245
+ }
246
+ ```
@@ -0,0 +1,202 @@
1
+ ---
2
+ name: middleware
3
+ description: Define middleware for authentication, logging, and request processing in @rangojs/router
4
+ argument-hint: [middleware-name]
5
+ ---
6
+
7
+ # Middleware
8
+
9
+ Middleware runs before/after route handlers using the onion model.
10
+
11
+ ## Basic Middleware
12
+
13
+ ```typescript
14
+ import { createMiddleware } from "@rangojs/router";
15
+
16
+ export const authMiddleware = createMiddleware(async (ctx, next) => {
17
+ const token = ctx.request.headers.get("Authorization");
18
+
19
+ if (!token) {
20
+ throw new Response("Unauthorized", { status: 401 });
21
+ }
22
+
23
+ const user = await verifyToken(token);
24
+ ctx.env.Variables.user = user;
25
+
26
+ await next();
27
+ });
28
+ ```
29
+
30
+ ## Using Middleware in Routes
31
+
32
+ ```typescript
33
+ import { urls } from "@rangojs/router";
34
+ import { authMiddleware, loggerMiddleware } from "./middleware";
35
+
36
+ export const urlpatterns = urls(({ path, layout, middleware }) => [
37
+ // Global middleware for all routes in this file
38
+ middleware(loggerMiddleware),
39
+
40
+ // Layout with scoped middleware
41
+ layout(<AdminLayout />, () => [
42
+ middleware(authMiddleware), // Only for admin routes
43
+
44
+ path("/admin", AdminDashboard, { name: "admin.index" }),
45
+ path("/admin/users", AdminUsers, { name: "admin.users" }),
46
+ ]),
47
+
48
+ // Public routes (no auth middleware)
49
+ path("/", HomePage, { name: "home" }),
50
+ path("/about", AboutPage, { name: "about" }),
51
+ ]);
52
+ ```
53
+
54
+ ## Middleware with Multiple Handlers
55
+
56
+ ```typescript
57
+ // Spread multiple middleware from a single export
58
+ export const shopMiddleware = [loggerMiddleware, mockAuthMiddleware];
59
+
60
+ // In routes
61
+ layout(<ShopLayout />, () => [
62
+ middleware(...shopMiddleware),
63
+
64
+ path("/shop", ShopIndex, { name: "shop" }),
65
+ ])
66
+ ```
67
+
68
+ ## Middleware Context
69
+
70
+ ```typescript
71
+ export const myMiddleware = createMiddleware(async (ctx, next) => {
72
+ // Access request
73
+ ctx.request; // Request object
74
+ ctx.url; // Parsed URL
75
+ ctx.params; // Route parameters
76
+
77
+ // Access environment
78
+ ctx.env.Bindings.DB; // Cloudflare bindings
79
+ ctx.env.Variables; // Mutable variables
80
+
81
+ // Set variables for downstream handlers
82
+ ctx.env.Variables.user = { id: "123", name: "John" };
83
+
84
+ // Continue to next middleware/handler
85
+ await next();
86
+
87
+ // After handler (response intercepting)
88
+ console.log("Handler completed");
89
+ });
90
+ ```
91
+
92
+ ## Authentication Middleware
93
+
94
+ ```typescript
95
+ export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
96
+ const user = ctx.env.Variables.user;
97
+
98
+ if (!user) {
99
+ throw new Response("Unauthorized", { status: 401 });
100
+ }
101
+
102
+ await next();
103
+ });
104
+
105
+ export const permissionsMiddleware = createMiddleware(async (ctx, next) => {
106
+ const user = ctx.env.Variables.user;
107
+ const requiredPermission = "admin";
108
+
109
+ if (!user?.permissions?.includes(requiredPermission)) {
110
+ throw new Response("Forbidden", { status: 403 });
111
+ }
112
+
113
+ await next();
114
+ });
115
+ ```
116
+
117
+ ## Logger Middleware
118
+
119
+ ```typescript
120
+ export const loggerMiddleware = createMiddleware(async (ctx, next) => {
121
+ const start = Date.now();
122
+
123
+ console.log(`[${ctx.request.method}] ${ctx.url.pathname}`);
124
+
125
+ await next();
126
+
127
+ const duration = Date.now() - start;
128
+ console.log(`[${ctx.request.method}] ${ctx.url.pathname} - ${duration}ms`);
129
+ });
130
+ ```
131
+
132
+ ## Rate Limiting Middleware
133
+
134
+ ```typescript
135
+ export const rateLimitMiddleware = createMiddleware(async (ctx, next) => {
136
+ const ip = ctx.request.headers.get("CF-Connecting-IP") ?? "unknown";
137
+ const key = `rate-limit:${ip}`;
138
+
139
+ const count = await ctx.env.Bindings.KV.get(key);
140
+ const requests = count ? parseInt(count) : 0;
141
+
142
+ if (requests > 100) {
143
+ throw new Response("Too Many Requests", { status: 429 });
144
+ }
145
+
146
+ await ctx.env.Bindings.KV.put(key, String(requests + 1), {
147
+ expirationTtl: 60,
148
+ });
149
+
150
+ await next();
151
+ });
152
+ ```
153
+
154
+ ## Complete Example
155
+
156
+ ```typescript
157
+ // middleware/index.ts
158
+ import { createMiddleware } from "@rangojs/router";
159
+
160
+ export const loggerMiddleware = createMiddleware(async (ctx, next) => {
161
+ console.log(`[${ctx.request.method}] ${ctx.url.pathname}`);
162
+ await next();
163
+ });
164
+
165
+ export const mockAuthMiddleware = createMiddleware(async (ctx, next) => {
166
+ // Mock user for development
167
+ ctx.env.Variables.user = { id: "1", name: "Demo User" };
168
+ await next();
169
+ });
170
+
171
+ export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
172
+ if (!ctx.env.Variables.user) {
173
+ throw new Response("Unauthorized", { status: 401 });
174
+ }
175
+ await next();
176
+ });
177
+
178
+ // urls.tsx
179
+ import { urls } from "@rangojs/router";
180
+ import {
181
+ loggerMiddleware,
182
+ mockAuthMiddleware,
183
+ requireAuthMiddleware,
184
+ } from "./middleware";
185
+
186
+ export const urlpatterns = urls(({ path, layout, middleware }) => [
187
+ // Global middleware
188
+ middleware(loggerMiddleware),
189
+ middleware(mockAuthMiddleware),
190
+
191
+ // Public routes
192
+ path("/", HomePage, { name: "home" }),
193
+
194
+ // Protected routes
195
+ layout(<AccountLayout />, () => [
196
+ middleware(requireAuthMiddleware),
197
+
198
+ path("/account", AccountPage, { name: "account" }),
199
+ path("/account/settings", SettingsPage, { name: "settings" }),
200
+ ]),
201
+ ]);
202
+ ```
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: mime-routes
3
+ description: Content negotiation — serve different response types (RSC, JSON, text, XML) from the same URL based on Accept header
4
+ argument-hint: [negotiate|vary|accept]
5
+ ---
6
+
7
+ # Content Negotiation (MIME Routes)
8
+
9
+ Content negotiation lets you register multiple response types on the same URL pattern.
10
+ The router inspects the `Accept` header and dispatches to the matching handler.
11
+ All negotiated responses include `Vary: Accept` for correct CDN/cache behavior.
12
+
13
+ See also: `/response-routes` for the base response route API (path.json, path.text, etc.).
14
+
15
+ ## Defining Negotiated Routes
16
+
17
+ Declare the same URL pattern with both an RSC route and one or more response-type routes.
18
+ Order within the `urls()` array does not matter — the trie merges them at build time.
19
+
20
+ ```typescript
21
+ import { urls } from "@rangojs/router/server";
22
+
23
+ export const urlpatterns = urls(({ path, layout, include }) => [
24
+ // RSC page + JSON API on the same URL
25
+ path("/products/:id", ProductPage, { name: "product" }),
26
+ path.json("/products/:id", (ctx) => {
27
+ return db.getProduct(ctx.params.id);
28
+ }, { name: "productJson" }),
29
+ ]);
30
+ ```
31
+
32
+ When a browser requests `/products/42` (`Accept: text/html`), the RSC page renders.
33
+ When an API client requests the same URL (`Accept: application/json`), the JSON handler runs.
34
+
35
+ ## Negotiation Rules
36
+
37
+ 1. **Q-value priority** — higher `q` wins (`Accept: application/json;q=0.9, text/html;q=1.0` serves RSC)
38
+ 2. **Client order tiebreaker** — when q-values are equal, the type listed first in Accept wins (matches Express/Hono behavior)
39
+ 3. **Specific MIME match** — the variant whose MIME type appears in Accept wins
40
+ 4. **Wildcard / empty Accept** — `*/*` and missing Accept fall back to route definition order (the first-defined variant wins)
41
+ 5. **All responses** on a negotiated URL get `Vary: Accept` header, including the RSC side
42
+
43
+ RSC participates as a `text/html` candidate alongside response-type variants.
44
+ There is no special short-circuit — RSC follows the same negotiation rules as other types.
45
+
46
+ The MIME mapping used for matching:
47
+
48
+ | Tag | MIME type |
49
+ |-----|-----------|
50
+ | RSC (plain `path()`) | `text/html` (negotiation) / `text/x-component` (wire format) |
51
+ | `json` | `application/json` |
52
+ | `text` | `text/plain` |
53
+ | `xml` | `application/xml` |
54
+ | `html` | `text/html` |
55
+ | `md` | `text/markdown` |
56
+
57
+ RSC routes negotiate as `text/html` but respond with `text/x-component` (the RSC wire format).
58
+ The browser's RSC runtime decodes this transparently — clients requesting `text/html` get
59
+ the RSC page rendered normally.
60
+
61
+ Tags `image`, `stream`, and `any` are pass-through and do not participate in Accept matching.
62
+
63
+ ## Multiple Response Types
64
+
65
+ A single URL can have an RSC route plus multiple response-type variants:
66
+
67
+ ```typescript
68
+ export const urlpatterns = urls(({ path }) => [
69
+ path("/data", DataPage, { name: "data" }),
70
+ path.json("/data", () => ({ format: "json" }), { name: "dataJson" }),
71
+ path.text("/data", () => "plain text", { name: "dataText" }),
72
+ path.xml("/data", () => "<root>xml</root>", { name: "dataXml" }),
73
+ ]);
74
+ ```
75
+
76
+ - `Accept: text/html` — RSC page
77
+ - `Accept: application/json` — JSON handler
78
+ - `Accept: text/plain` — text handler
79
+ - `Accept: application/xml` — XML handler
80
+ - `Accept: */*` — first variant (JSON, since it was registered first)
81
+
82
+ ## Wildcard Routes
83
+
84
+ Content negotiation works with wildcard `/*` patterns:
85
+
86
+ ```typescript
87
+ path("/files/*", FileBrowserPage, { name: "files" }),
88
+ path.json("/files/*", (ctx) => {
89
+ const filePath = ctx.params["*"];
90
+ return { entries: listDir(filePath) };
91
+ }, { name: "filesJson" }),
92
+ ```
93
+
94
+ ## Response-Only Negotiation (No RSC Primary)
95
+
96
+ Two or more response-type routes can share a URL without an RSC route.
97
+ The last registered route becomes the primary; earlier ones become variants:
98
+
99
+ ```typescript
100
+ path.json("/api/data", () => ({ format: "json" }), { name: "dataJson" }),
101
+ path.text("/api/data", () => "plain text version", { name: "dataText" }),
102
+ ```
103
+
104
+ Without an RSC primary, there is no `text/html` candidate — the Accept header
105
+ picks among the response-type candidates directly.
106
+
107
+ ## How It Works
108
+
109
+ 1. **Build time**: `buildRouteTrie()` calls `mergeLeaves()` when multiple routes share a pattern.
110
+ RSC routes become the primary trie leaf; response-type routes are stored in the `nv`
111
+ (negotiate variants) array on the leaf. The `rf` (rsc-first) flag tracks definition order.
112
+ 2. **Runtime**: `previewRoute()` reads `negotiateVariants` from the trie match result.
113
+ It parses the `Accept` header (extracting q-values and order), builds a candidate list
114
+ (RSC as `text/html` + response-type variants), and calls `pickNegotiateVariant()`.
115
+ 3. **Candidate matching**: walks the client's sorted Accept list (by q desc, then order asc),
116
+ matching each entry against candidates. Wildcards (`*/*`, `text/*`) fall back to definition order.
117
+ 4. **Vary header**: both the response-route handler wrapper and the RSC handler wrapper
118
+ append `Vary: Accept` when the `negotiated` flag is set on the preview result.
119
+
120
+ ## Caching Considerations
121
+
122
+ `Vary: Accept` is set automatically on all negotiated responses. This tells CDNs and
123
+ HTTP caches to store separate entries per Accept header value. No additional cache
124
+ configuration is needed for negotiated routes — the framework handles it.