@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,358 @@
1
+ ---
2
+ name: response-routes
3
+ description: Response routes (path.json, path.text, etc.) for non-RSC endpoints with typed responses
4
+ argument-hint: [json|text|html|xml|md|image|stream]
5
+ ---
6
+
7
+ # Response Routes
8
+
9
+ Response routes skip the RSC pipeline entirely. Use them for JSON APIs, plain text endpoints,
10
+ XML feeds, image proxies, and any route that returns a `Response` instead of React components.
11
+
12
+ ## Route-Level Tags: path.json(), path.text(), etc.
13
+
14
+ Inside any `urls()` callback, use `path.json()`, `path.text()`, or other tags alongside regular RSC routes:
15
+
16
+ ```typescript
17
+ import { urls, RouterError } from "@rangojs/router/server";
18
+
19
+ export const urlpatterns = urls(({ path, layout, include }) => [
20
+ // RSC routes (normal)
21
+ path("/", HomePage, { name: "home" }),
22
+ path("/about", AboutPage, { name: "about" }),
23
+
24
+ // JSON API route (inline, alongside RSC routes)
25
+ path.json("/api/status", (ctx) => ({
26
+ status: "ok",
27
+ timestamp: Date.now(),
28
+ }), { name: "status" }),
29
+
30
+ // Text route
31
+ path.text("/robots.txt", (ctx) => {
32
+ return "User-agent: *\nAllow: /\nDisallow: /api/\n";
33
+ }, { name: "robots" }),
34
+
35
+ // Markdown route
36
+ path.md("/docs/:slug.md", (ctx) => {
37
+ return `# ${ctx.params.slug}\n\nDocumentation content here.`;
38
+ }, { name: "docs" }),
39
+
40
+ // Response route (full control, returns Response directly)
41
+ path.image("/og/:slug.png", async (ctx) => {
42
+ const image = await generateOgImage(ctx.params.slug);
43
+ return new Response(image, {
44
+ headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=86400" },
45
+ });
46
+ }, { name: "ogImage" }),
47
+ ]);
48
+ ```
49
+
50
+ ## Available Tags
51
+
52
+ | Tag | Usage | Handler returns | Auto-wrap |
53
+ |-----|-------|-----------------|-----------|
54
+ | `json` | `path.json()` | plain object/array | `{ data: T }` envelope |
55
+ | `text` | `path.text()` | string | text/plain Response |
56
+ | `html` | `path.html()` | string | text/html Response |
57
+ | `xml` | `path.xml()` | string | application/xml Response |
58
+ | `md` | `path.md()` | string | text/markdown Response |
59
+ | `image` | `path.image()` | Response | pass-through |
60
+ | `stream` | `path.stream()` | Response | pass-through |
61
+ | `any` | `path.any()` | Response | pass-through |
62
+
63
+ ## ResponseHandlerContext
64
+
65
+ Response route handlers receive a lighter context (no `ctx.use()`, no `ctx.res`):
66
+
67
+ ```typescript
68
+ interface ResponseHandlerContext<TParams, TEnv> {
69
+ request: Request;
70
+ params: TParams; // Typed from URL pattern
71
+ env: Bindings; // Extracted from RouterEnv (DB, KV, etc.)
72
+ searchParams: URLSearchParams;
73
+ url: URL;
74
+ pathname: string;
75
+ href: (name: string, params?: Record<string, string>) => string;
76
+ header: (name: string, value: string) => void;
77
+ setCookie: (name: string, value: string, options?: CookieOptions) => void;
78
+ }
79
+ ```
80
+
81
+ ### Setting Headers and Cookies
82
+
83
+ String-returning handlers (json, text, html, xml, md) can set custom headers and cookies
84
+ without constructing a full Response:
85
+
86
+ ```typescript
87
+ path.md("/docs/:slug.md", (ctx) => {
88
+ ctx.header("Cache-Control", "public, max-age=3600");
89
+ ctx.setCookie("last-doc", ctx.params.slug, { path: "/" });
90
+ return `# ${ctx.params.slug}\n\nContent here.`;
91
+ }, { name: "docs" });
92
+ ```
93
+
94
+ Headers and cookies set via `ctx.header()` / `ctx.setCookie()` are merged into the
95
+ auto-wrapped Response. If the handler returns a `Response` directly, these are ignored
96
+ (use the Response headers instead).
97
+
98
+ ### Environment Type Extraction
99
+
100
+ `env` extracts bindings from `RouterEnv`, not the full env:
101
+
102
+ ```typescript
103
+ type AppEnv = RouterEnv<{ DB: D1Database; KV: KVNamespace }, { user: User }>;
104
+
105
+ // In a response handler:
106
+ path.json("/api/data", (ctx) => {
107
+ ctx.env.DB; // D1Database (bindings extracted)
108
+ ctx.env.KV; // KVNamespace
109
+ // ctx.env.user -- NOT available (variables are not on response ctx.env)
110
+ return { data: "ok" };
111
+ }, { name: "data" });
112
+ ```
113
+
114
+ ## JSON Envelope
115
+
116
+ `path.json()` handlers return plain data. The framework auto-wraps it
117
+ in a `ResponseEnvelope<T>` discriminated union:
118
+
119
+ ```typescript
120
+ // Success: HTTP 200
121
+ { "data": { "status": "ok", "timestamp": 1700000000 } }
122
+
123
+ // Error: HTTP 404 (or whatever status RouterError specifies)
124
+ { "error": { "message": "Product 999 not found", "code": "NOT_FOUND" } }
125
+ ```
126
+
127
+ ### Error Handling with RouterError
128
+
129
+ Throw `RouterError` to return structured error envelopes:
130
+
131
+ ```typescript
132
+ import { RouterError } from "@rangojs/router/server";
133
+
134
+ path.json("/api/users/:id", (ctx) => {
135
+ const user = users.get(ctx.params.id);
136
+ if (!user) {
137
+ throw new RouterError("NOT_FOUND", `User ${ctx.params.id} not found`, { status: 404 });
138
+ }
139
+ if (!hasPermission(ctx)) {
140
+ throw new RouterError("FORBIDDEN", "Access denied", { status: 403 });
141
+ }
142
+ return user;
143
+ }, { name: "user" });
144
+ ```
145
+
146
+ ### Returning Response Directly
147
+
148
+ JSON handlers can return `Response` to bypass auto-wrap (custom status, headers, streaming):
149
+
150
+ ```typescript
151
+ path.json("/api/export", (ctx) => {
152
+ const csv = generateCsv();
153
+ return new Response(csv, {
154
+ headers: {
155
+ "Content-Type": "text/csv",
156
+ "Content-Disposition": "attachment; filename=export.csv",
157
+ },
158
+ });
159
+ }, { name: "export" });
160
+ ```
161
+
162
+ ## Client-Side Type Safety
163
+
164
+ ### ResponseEnvelope and isResponseError
165
+
166
+ ```typescript
167
+ "use client";
168
+ import type { ResponseEnvelope, ResponseError } from "@rangojs/router/client";
169
+ import { isResponseError } from "@rangojs/router/client";
170
+
171
+ // Fetch a typed response
172
+ const res = await fetch("/api/products/1");
173
+ const result: ResponseEnvelope<Product> = await res.json();
174
+
175
+ if (isResponseError(result)) {
176
+ // result.error: ResponseError (message, code?, type?)
177
+ // result.data: undefined
178
+ console.error(result.error.message);
179
+ } else {
180
+ // result.data: Product
181
+ // result.error: undefined
182
+ console.log(result.data.name);
183
+ }
184
+ ```
185
+
186
+ ### RouteResponse (scoped lookup by route name)
187
+
188
+ Look up response type from a `path.json()` or `path.text()` module by route name:
189
+
190
+ ```typescript
191
+ import type { RouteResponse } from "@rangojs/router/server";
192
+
193
+ // From the apiPatterns module (before include)
194
+ type HealthData = RouteResponse<typeof apiPatterns, "health">;
195
+ // = ResponseEnvelope<{ status: string; timestamp: number }>
196
+
197
+ type ProductsData = RouteResponse<typeof apiPatterns, "products">;
198
+ // = ResponseEnvelope<{ id: string; name: string; price: number }[]>
199
+ ```
200
+
201
+ ### PathResponse (global lookup by URL pattern)
202
+
203
+ Look up response type from the merged route map by URL pattern:
204
+
205
+ ```typescript
206
+ import type { PathResponse } from "@rangojs/router/client";
207
+
208
+ // After include("/api", apiPatterns) in main urls
209
+ type Health = PathResponse<"/api/health">;
210
+ // = ResponseEnvelope<{ status: string; timestamp: number }>
211
+
212
+ // RSC routes return ResponseEnvelope<never>
213
+ type Home = PathResponse<"/">;
214
+ // = ResponseEnvelope<never>
215
+ ```
216
+
217
+ ### ParamsFor with Response Routes
218
+
219
+ ```typescript
220
+ import type { ParamsFor } from "@rangojs/router/client";
221
+
222
+ // Works for both RSC and response routes
223
+ type ProductParams = ParamsFor<"api.productDetail">;
224
+ // = { id: string }
225
+ ```
226
+
227
+ ## Links to Response Routes
228
+
229
+ ### Client: href.json(), href.text(), etc.
230
+
231
+ Response route links need `data-external` to trigger hard navigation (skip RSC fetch).
232
+ Use `href.json()` which returns props to spread on `<Link>`:
233
+
234
+ ```typescript
235
+ "use client";
236
+ import { href, Link } from "@rangojs/router/client";
237
+
238
+ function Nav() {
239
+ return (
240
+ <>
241
+ {/* RSC link (client-side navigation) */}
242
+ <Link to={href("/about")}>About</Link>
243
+
244
+ {/* Response route link (hard navigation via data-external) */}
245
+ <Link {...href.json("/api/health")}>API Health</Link>
246
+ <Link {...href.text("/robots.txt")}>Robots</Link>
247
+ </>
248
+ );
249
+ }
250
+
251
+ // href.json("/api/health") returns:
252
+ // { to: "/api/health", "data-external": "" }
253
+ ```
254
+
255
+ ## Use Items
256
+
257
+ Response routes support only `middleware()` and `cache()` as use items.
258
+ No `loader`, `loading`, `layout`, or `parallel`.
259
+
260
+ ```typescript
261
+ path.json("/api/users", handler, { name: "users" }, () => [
262
+ cache({ ttl: 60, swr: 300 }),
263
+ ]);
264
+ ```
265
+
266
+ ## Mountable Module Pattern
267
+
268
+ A self-contained module with RSC pages + JSON APIs, mountable via `include()`:
269
+
270
+ ```typescript
271
+ // blog/api/urls.tsx
272
+ import { urls, RouterError } from "@rangojs/router/server";
273
+
274
+ export const blogApiPatterns = urls(({ path }) => [
275
+ path.json("/stats", (ctx) => ({
276
+ views: 1200, visitors: 450,
277
+ }), { name: "stats" }),
278
+
279
+ path.json("/:slug/likes", (ctx) => ({
280
+ slug: ctx.params.slug,
281
+ count: 42,
282
+ }), { name: "likes" }),
283
+
284
+ path.json("/:slug/comments", (ctx) => ([
285
+ { id: "c1", body: "Great post", author: "alice" },
286
+ ]), { name: "comments" }),
287
+ ]);
288
+
289
+ // blog/urls.tsx
290
+ import { urls } from "@rangojs/router/server";
291
+ import { blogApiPatterns } from "./api/urls";
292
+
293
+ export const blogPatterns = urls(({ path, include }) => [
294
+ path("/", BlogIndex, { name: "index" }),
295
+ path("/:slug", BlogPost, { name: "post" }),
296
+ path("/category/:catId", BlogCategory, { name: "category" }),
297
+
298
+ include("/api", blogApiPatterns, { name: "api" }),
299
+ ]);
300
+
301
+ // app/urls.tsx
302
+ import { urls } from "@rangojs/router/server";
303
+ import { blogPatterns } from "./blog/urls";
304
+
305
+ export const urlpatterns = urls(({ path, include }) => [
306
+ path("/", HomePage, { name: "home" }),
307
+ include("/blog", blogPatterns, { name: "blog" }),
308
+ ]);
309
+ ```
310
+
311
+ ### Type safety after mounting
312
+
313
+ ```typescript
314
+ import type { RouteResponse } from "@rangojs/router/server";
315
+ import type { PathResponse, ParamsFor } from "@rangojs/router/client";
316
+
317
+ // Scoped (before mount) -- use the module directly
318
+ type Stats = RouteResponse<typeof blogApiPatterns, "stats">;
319
+ // = ResponseEnvelope<{ views: number; visitors: number }>
320
+
321
+ // After mounting -- names get prefixed
322
+ type BlogStats = PathResponse<"/blog/api/stats">;
323
+ // = ResponseEnvelope<{ views: number; visitors: number }>
324
+
325
+ // Params work through nested includes
326
+ type LikesParams = ParamsFor<"blog.api.likes">;
327
+ // = { slug: string }
328
+ ```
329
+
330
+ ### ctx.reverse inside mounted modules
331
+
332
+ Response route handlers inside a mounted module can reference local names:
333
+
334
+ ```typescript
335
+ // Inside blogApiPatterns handler
336
+ path("/:slug/likes", (ctx) => {
337
+ // ctx.reverse resolves names relative to the mount point
338
+ const commentsUrl = ctx.reverse("comments", { slug: ctx.params.slug });
339
+ // -> "/blog/api/my-post/comments"
340
+
341
+ return { slug: ctx.params.slug, count: 42, commentsUrl };
342
+ }, { name: "likes" });
343
+ ```
344
+
345
+ ## Content Negotiation
346
+
347
+ Multiple response types can share the same URL pattern. See `/mime-routes` for the
348
+ full content negotiation API (Accept header matching, Vary: Accept, multi-variant routes).
349
+
350
+ ## How It Works
351
+
352
+ 1. `path.json()` tags the route at the trie level with a MIME type
353
+ 2. `coreRequestHandler()` checks the tag before the RSC pipeline
354
+ 3. Tagged routes short-circuit: handler runs, Response is returned directly
355
+ 4. JSON routes auto-wrap return values in `{ data }` / `{ error }` envelope
356
+ 5. Client-side navigation to response routes gets `X-RSC-Reload` header, triggering hard navigation
357
+ 6. Response types flow through `_responses` phantom type on `UrlPatterns`, propagated by `include()`
358
+ 7. When multiple routes share a URL pattern, the trie merges them for content negotiation (see `/mime-routes`)
@@ -0,0 +1,173 @@
1
+ ---
2
+ name: route
3
+ description: Define routes with path() in @rangojs/router
4
+ argument-hint: [pattern]
5
+ ---
6
+
7
+ # Defining Routes with path()
8
+
9
+ ## Basic Route
10
+
11
+ ```typescript
12
+ import { urls } from "@rangojs/router";
13
+
14
+ export const urlpatterns = urls(({ path }) => [
15
+ path("/", HomePage, { name: "home" }),
16
+ path("/about", AboutPage, { name: "about" }),
17
+ path("/contact", ContactPage, { name: "contact" }),
18
+ ]);
19
+ ```
20
+
21
+ ## Route with Parameters
22
+
23
+ ```typescript
24
+ urls(({ path }) => [
25
+ // Single parameter
26
+ path("/product/:slug", ProductPage, { name: "product" }),
27
+
28
+ // Multiple parameters
29
+ path("/blog/:year/:month/:slug", BlogPostPage, { name: "blogPost" }),
30
+
31
+ // Optional parameter (add ? suffix)
32
+ path("/search/:query?", SearchPage, { name: "search" }),
33
+ ]);
34
+ ```
35
+
36
+ ## Route Handler Patterns
37
+
38
+ ### Component Function
39
+
40
+ ```typescript
41
+ path("/about", AboutPage, { name: "about" })
42
+
43
+ // AboutPage receives context
44
+ function AboutPage(ctx: HandlerContext) {
45
+ return <div>About Us</div>;
46
+ }
47
+ ```
48
+
49
+ ### Inline JSX
50
+
51
+ ```typescript
52
+ path("/about", () => <AboutPage />, { name: "about" })
53
+ ```
54
+
55
+ ### Handler with Context Access
56
+
57
+ ```typescript
58
+ path("/product/:slug", (ctx) => {
59
+ const { slug } = ctx.params;
60
+ return <ProductPage slug={slug} />;
61
+ }, { name: "product" })
62
+ ```
63
+
64
+ ### Async Handler (Streaming)
65
+
66
+ ```typescript
67
+ path("/product/:slug", async (ctx) => {
68
+ const product = await fetchProduct(ctx.params.slug);
69
+ return <ProductPage product={product} />;
70
+ }, { name: "product" })
71
+ ```
72
+
73
+ ## Route Options
74
+
75
+ ```typescript
76
+ path("/product/:slug", ProductPage, {
77
+ name: "product", // Route name for href() and navigation
78
+ })
79
+ ```
80
+
81
+ ## Route Children
82
+
83
+ Add loaders, loading states, and other features as children:
84
+
85
+ ```typescript
86
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
87
+ loader(ProductLoader),
88
+ loading(<ProductSkeleton />),
89
+ revalidate(productRevalidation),
90
+ ])
91
+ ```
92
+
93
+ ## Handler Context
94
+
95
+ Every handler receives a context object:
96
+
97
+ ```typescript
98
+ interface HandlerContext<TParams = Record<string, string>> {
99
+ params: TParams; // URL parameters
100
+ request: Request; // Original request
101
+ url: URL; // Parsed URL
102
+ env: TEnv; // Environment (bindings + variables)
103
+ use<T>(handle: Handle<T>): T; // Access handles
104
+ }
105
+ ```
106
+
107
+ ### Using Context
108
+
109
+ ```typescript
110
+ path("/product/:slug", (ctx) => {
111
+ // Access URL params
112
+ const { slug } = ctx.params;
113
+
114
+ // Access query params
115
+ const tab = ctx.url.searchParams.get("tab");
116
+
117
+ // Access environment
118
+ const db = ctx.env.Bindings.DB;
119
+
120
+ // Access handles
121
+ const breadcrumbs = ctx.use(Breadcrumbs);
122
+ breadcrumbs.push({ label: "Product", href: `/product/${slug}` });
123
+
124
+ return <ProductPage slug={slug} tab={tab} />;
125
+ }, { name: "product" })
126
+ ```
127
+
128
+ ## Nested Routes
129
+
130
+ Use layouts to nest routes:
131
+
132
+ ```typescript
133
+ urls(({ path, layout }) => [
134
+ layout(<ShopLayout />, () => [
135
+ path("/shop", ShopIndex, { name: "shop.index" }),
136
+ path("/shop/cart", CartPage, { name: "shop.cart" }),
137
+ path("/shop/product/:slug", ProductPage, { name: "shop.product" }),
138
+ ]),
139
+ ])
140
+ ```
141
+
142
+ ## Complete Example
143
+
144
+ ```typescript
145
+ import { urls } from "@rangojs/router";
146
+ import { Breadcrumbs } from "./handles/breadcrumbs";
147
+
148
+ export const urlpatterns = urls(({ path, layout, loader, loading }) => [
149
+ // Simple route
150
+ path("/", HomePage, { name: "home" }),
151
+
152
+ // Route with loader
153
+ path("/about", AboutPage, { name: "about" }, () => [
154
+ loader(TeamLoader),
155
+ ]),
156
+
157
+ // Dynamic route with handler
158
+ path("/product/:slug", (ctx) => {
159
+ const push = ctx.use(Breadcrumbs);
160
+ push({ label: ctx.params.slug, href: `/product/${ctx.params.slug}` });
161
+ return <ProductPage slug={ctx.params.slug} />;
162
+ }, { name: "product" }, () => [
163
+ loader(ProductLoader),
164
+ loading(<ProductSkeleton />, { ssr: true }),
165
+ ]),
166
+
167
+ // Nested routes in layout
168
+ layout(<BlogLayout />, () => [
169
+ path("/blog", BlogIndex, { name: "blog.index" }),
170
+ path("/blog/:slug", BlogPost, { name: "blog.post" }),
171
+ ]),
172
+ ]);
173
+ ```