@rangojs/router 0.0.0-experimental.ede38110 → 0.0.0-experimental.f2d1a2f1

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 (66) hide show
  1. package/README.md +50 -20
  2. package/dist/vite/index.js +353 -49
  3. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  4. package/package.json +5 -3
  5. package/skills/breadcrumbs/SKILL.md +3 -1
  6. package/skills/hooks/SKILL.md +28 -20
  7. package/skills/links/SKILL.md +88 -16
  8. package/skills/loader/SKILL.md +35 -2
  9. package/skills/migrate-react-router/SKILL.md +1 -0
  10. package/skills/response-routes/SKILL.md +8 -0
  11. package/skills/streams-and-websockets/SKILL.md +283 -0
  12. package/skills/typesafety/SKILL.md +3 -1
  13. package/src/browser/app-shell.ts +52 -0
  14. package/src/browser/navigation-bridge.ts +51 -2
  15. package/src/browser/navigation-client.ts +33 -10
  16. package/src/browser/navigation-store.ts +25 -1
  17. package/src/browser/partial-update.ts +20 -1
  18. package/src/browser/prefetch/cache.ts +124 -26
  19. package/src/browser/prefetch/fetch.ts +114 -38
  20. package/src/browser/prefetch/queue.ts +36 -5
  21. package/src/browser/rango-state.ts +53 -13
  22. package/src/browser/react/Link.tsx +18 -13
  23. package/src/browser/react/NavigationProvider.tsx +50 -11
  24. package/src/browser/react/use-navigation.ts +30 -11
  25. package/src/browser/react/use-params.ts +11 -1
  26. package/src/browser/react/use-router.ts +8 -1
  27. package/src/browser/rsc-router.tsx +34 -6
  28. package/src/browser/types.ts +13 -0
  29. package/src/cache/cf/cf-cache-store.ts +5 -7
  30. package/src/index.rsc.ts +3 -0
  31. package/src/index.ts +3 -0
  32. package/src/outlet-context.ts +1 -1
  33. package/src/response-utils.ts +28 -0
  34. package/src/reverse.ts +3 -2
  35. package/src/route-definition/dsl-helpers.ts +16 -3
  36. package/src/route-definition/resolve-handler-use.ts +6 -0
  37. package/src/router/handler-context.ts +20 -3
  38. package/src/router/lazy-includes.ts +1 -1
  39. package/src/router/loader-resolution.ts +3 -0
  40. package/src/router/match-api.ts +3 -3
  41. package/src/router/middleware-types.ts +2 -22
  42. package/src/router/middleware.ts +32 -4
  43. package/src/router/pattern-matching.ts +60 -9
  44. package/src/router/trie-matching.ts +10 -4
  45. package/src/router/url-params.ts +49 -0
  46. package/src/router.ts +1 -2
  47. package/src/rsc/handler.ts +8 -4
  48. package/src/rsc/helpers.ts +69 -41
  49. package/src/rsc/progressive-enhancement.ts +2 -0
  50. package/src/rsc/response-route-handler.ts +14 -1
  51. package/src/rsc/rsc-rendering.ts +7 -0
  52. package/src/rsc/server-action.ts +2 -0
  53. package/src/server/request-context.ts +10 -42
  54. package/src/types/handler-context.ts +2 -34
  55. package/src/types/loader-types.ts +5 -6
  56. package/src/types/request-scope.ts +126 -0
  57. package/src/urls/response-types.ts +2 -10
  58. package/src/vite/debug.ts +86 -0
  59. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  60. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  61. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  62. package/src/vite/plugins/performance-tracks.ts +4 -6
  63. package/src/vite/rango.ts +49 -14
  64. package/src/vite/router-discovery.ts +161 -23
  65. package/src/vite/utils/banner.ts +1 -1
  66. package/src/vite/utils/package-resolution.ts +41 -1
@@ -0,0 +1,76 @@
1
+ // Node ESM loader hook that resolves `cloudflare:*` imports to the same
2
+ // stub ESM the Vite transform produces for rewritten specifiers.
3
+ //
4
+ // Why both? The Vite transform (cloudflare-protocol-stub.ts) catches
5
+ // imports in modules that flow through Vite's plugin pipeline — covers
6
+ // user source and any node_modules package Vite fetches and transforms.
7
+ // But Vite/Rollup externalize certain packages (e.g. `partyserver`,
8
+ // which has `import { DurableObject, env } from "cloudflare:workers"`
9
+ // at its top level, and similar "workerd-native" libraries). Externalized
10
+ // modules bypass the transform: Rollup hands their resolution to Node's
11
+ // native ESM loader, which rejects URL-scheme specifiers. This loader
12
+ // hook registers via `module.register()` from `createTempRscServer` and
13
+ // intercepts `cloudflare:*` at Node's resolve layer — before the default
14
+ // loader throws ERR_UNSUPPORTED_ESM_URL_SCHEME.
15
+ //
16
+ // Lifecycle: the hook runs in a dedicated worker thread (Node ESM loader
17
+ // architecture) with its own globalThis. It cannot see the main thread's
18
+ // `__rango_build_env__` bridge, so the `env` export here is always `{}`.
19
+ // That's fine in practice — externalized libraries don't typically touch
20
+ // `env` at module top level; they read it at request time in workerd
21
+ // where the real module exists. Build-time prerender handlers in user
22
+ // source DO read `env`, but they flow through the Vite transform (which
23
+ // does bridge `env` from `getPlatformProxy()`), not through this loader.
24
+ //
25
+ // Keep STUBS in sync with cloudflare-protocol-stub.ts — both paths need
26
+ // to hand out the same base classes.
27
+
28
+ const CF_PREFIX = "cloudflare:";
29
+
30
+ const STUBS = {
31
+ "cloudflare:workers": `
32
+ export class DurableObject { constructor(_ctx, _env) {} }
33
+ export class WorkerEntrypoint { constructor(_ctx, _env) {} }
34
+ export class WorkflowEntrypoint { constructor(_ctx, _env) {} }
35
+ export class RpcTarget {}
36
+ export const env = {};
37
+ export default {};
38
+ `,
39
+ "cloudflare:email": `
40
+ export class EmailMessage { constructor(_from, _to, _raw) {} }
41
+ export default {};
42
+ `,
43
+ "cloudflare:sockets": `
44
+ export function connect() { return {}; }
45
+ export default {};
46
+ `,
47
+ "cloudflare:workflows": `
48
+ export class NonRetryableError extends Error {
49
+ constructor(message, name) { super(message); this.name = name ?? "NonRetryableError"; }
50
+ }
51
+ export default {};
52
+ `,
53
+ };
54
+
55
+ // Policy: unknown `cloudflare:*` specifiers resolve permissively to an
56
+ // empty default export rather than throwing. Same reasoning as
57
+ // cloudflare-protocol-stub.ts's FALLBACK_STUB — we prioritize
58
+ // dependency-graph resilience over strict validation, because third-party
59
+ // packages can pull `cloudflare:*` modules we haven't curated.
60
+ const FALLBACK_STUB = `export default {};\n`;
61
+
62
+ function dataUrlFor(specifier) {
63
+ const body = STUBS[specifier] ?? FALLBACK_STUB;
64
+ return "data:text/javascript;base64," + Buffer.from(body).toString("base64");
65
+ }
66
+
67
+ export async function resolve(specifier, context, nextResolve) {
68
+ if (specifier.startsWith(CF_PREFIX)) {
69
+ return {
70
+ shortCircuit: true,
71
+ url: dataUrlFor(specifier),
72
+ format: "module",
73
+ };
74
+ }
75
+ return nextResolve(specifier, context);
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.ede38110",
3
+ "version": "0.0.0-experimental.f2d1a2f1",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -133,9 +133,9 @@
133
133
  "tag": "experimental"
134
134
  },
135
135
  "scripts": {
136
- "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
136
+ "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
137
137
  "prepublishOnly": "pnpm build",
138
- "typecheck": "tsc --noEmit",
138
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit",
139
139
  "test": "playwright test",
140
140
  "test:ui": "playwright test --ui",
141
141
  "test:unit": "vitest run",
@@ -143,12 +143,14 @@
143
143
  },
144
144
  "dependencies": {
145
145
  "@vitejs/plugin-rsc": "^0.5.23",
146
+ "debug": "^4.4.1",
146
147
  "magic-string": "^0.30.17",
147
148
  "picomatch": "^4.0.3",
148
149
  "rsc-html-stream": "^0.0.7"
149
150
  },
150
151
  "devDependencies": {
151
152
  "@playwright/test": "^1.49.1",
153
+ "@types/debug": "^4.1.12",
152
154
  "@types/node": "^24.10.1",
153
155
  "@types/react": "catalog:",
154
156
  "@types/react-dom": "catalog:",
@@ -141,9 +141,11 @@ path("/dashboard", (ctx) => {
141
141
  breadcrumb({ label: "Dashboard", href: "/dashboard" });
142
142
  return <DashboardNav handle={Breadcrumbs} />;
143
143
  });
144
+ ```
144
145
 
146
+ ```tsx
145
147
  // Client component
146
- ("use client");
148
+ "use client";
147
149
  import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
148
150
 
149
151
  function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
@@ -298,9 +298,11 @@ path("/dashboard", (ctx) => {
298
298
  push({ label: "Dashboard", href: "/dashboard" });
299
299
  return <DashboardNav handle={Breadcrumbs} />;
300
300
  });
301
+ ```
301
302
 
303
+ ```tsx
302
304
  // Client component — typeof infers the full Handle<T> type
303
- ("use client");
305
+ "use client";
304
306
  import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
305
307
 
306
308
  function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
@@ -593,6 +595,12 @@ function ProductPage() {
593
595
  return <h1>Product {params.productId}</h1>;
594
596
  }
595
597
 
598
+ // Annotate the expected shape via a generic
599
+ function ProductPageTyped() {
600
+ const { productId } = useParams<{ productId: string }>();
601
+ return <h1>Product {productId}</h1>;
602
+ }
603
+
596
604
  // With selector for performance (re-renders only when selected value changes)
597
605
  function ProductId() {
598
606
  const productId = useParams((p) => p.productId);
@@ -600,7 +608,7 @@ function ProductId() {
600
608
  }
601
609
  ```
602
610
 
603
- Returns merged params from all matched route segments. Updates on navigation commit (not during pending navigation).
611
+ Returns merged params from all matched route segments as a `Readonly<T>` map. Updates on navigation commit (not during pending navigation).
604
612
 
605
613
  ### usePathname()
606
614
 
@@ -681,24 +689,24 @@ function MountInfo() {
681
689
  }
682
690
  ```
683
691
 
684
- See `/links` for full URL generation guide including server-side `ctx.reverse`.
692
+ See `/links` for full URL generation guide. The default server API is `ctx.reverse()`; in client components, receive URLs as props, loader data, or server-action return values — `reverse()` is not available in the browser.
685
693
 
686
694
  ## Hook Summary
687
695
 
688
- | Hook | Purpose | Returns |
689
- | -------------------- | --------------------------------- | ----------------------------------------------- |
690
- | `useParams()` | Route params | `Record<string, string>` or selected value |
691
- | `usePathname()` | Current pathname | `string` |
692
- | `useSearchParams()` | URL search params | `ReadonlyURLSearchParams` |
693
- | `useHref()` | Mount-aware href | `(path) => string` |
694
- | `useMount()` | Current include() mount path | `string` |
695
- | `useNavigation()` | Reactive navigation state | state, location, isStreaming |
696
- | `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
697
- | `useSegments()` | URL path & segment IDs | path, segmentIds, location |
698
- | `useLinkStatus()` | Link pending state | { pending } |
699
- | `useLoader()` | Loader data (strict) | data, isLoading, error |
700
- | `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading |
701
- | `useHandle()` | Accumulated handle data | T (handle type) |
702
- | `useAction()` | Server action state | state, error, result |
703
- | `useLocationState()` | History state (persists or flash) | T \| undefined |
704
- | `useClientCache()` | Cache control | { clear } |
696
+ | Hook | Purpose | Returns |
697
+ | -------------------- | --------------------------------- | ------------------------------------------------------------------ |
698
+ | `useParams()` | Route params | `Readonly<T>` (default `Record<string, string>`) or selected value |
699
+ | `usePathname()` | Current pathname | `string` |
700
+ | `useSearchParams()` | URL search params | `ReadonlyURLSearchParams` |
701
+ | `useHref()` | Mount-aware href | `(path) => string` |
702
+ | `useMount()` | Current include() mount path | `string` |
703
+ | `useNavigation()` | Reactive navigation state | state, location, isStreaming |
704
+ | `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
705
+ | `useSegments()` | URL path & segment IDs | path, segmentIds, location |
706
+ | `useLinkStatus()` | Link pending state | { pending } |
707
+ | `useLoader()` | Loader data (strict) | data, isLoading, error |
708
+ | `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading |
709
+ | `useHandle()` | Accumulated handle data | T (handle type) |
710
+ | `useAction()` | Server action state | state, error, result |
711
+ | `useLocationState()` | History state (persists or flash) | T \| undefined |
712
+ | `useClientCache()` | Cache control | { clear } |
@@ -1,16 +1,20 @@
1
1
  ---
2
2
  name: links
3
- description: URL generation with ctx.reverse (server), href (client), useHref (mounted), useMount, and scopedReverse
4
- argument-hint: [href|useHref|useMount|scopedReverse]
3
+ description: URL generation with ctx.reverse (server default), href (client), useHref (mounted), useMount, and scopedReverse
4
+ argument-hint: [ctx.reverse|href|useHref|useMount|scopedReverse]
5
5
  ---
6
6
 
7
7
  # Links & URL Generation
8
8
 
9
9
  @rangojs/router provides different href APIs for server and client contexts.
10
10
 
11
+ **Default server API: `ctx.reverse()`.** Generate URLs from the handler context — it's typed, auto-fills mount params, and resolves local (`.name`) and absolute (`name.sub`) names.
12
+
13
+ **`reverse()` is server-only.** It depends on the route manifest and handler context, neither of which are available in the browser. Client components receive URLs as props, loader data, or server-action return values — they never call `reverse` directly.
14
+
11
15
  ## Server: ctx.reverse()
12
16
 
13
- Available in route handlers via HandlerContext. Resolves named routes using the full route map.
17
+ Available in route handlers via HandlerContext. Resolves named routes using the full route map. This is the default way to generate URLs on the server.
14
18
 
15
19
  ```typescript
16
20
  import { urls, scopedReverse } from "@rangojs/router";
@@ -103,7 +107,7 @@ path("/search", (ctx) => {
103
107
 
104
108
  ### scopedReverse() - type-safe ctx.reverse
105
109
 
106
- Wraps `ctx.reverse` with local route type information for autocomplete and validation:
110
+ Wraps `ctx.reverse` with local route type information for autocomplete and validation. Runtime behavior is identical to `ctx.reverse` — `scopedReverse` is a type-only cast. The same dot-prefix rule applies: local names use `.name`, global names use `name.sub`.
107
111
 
108
112
  ```typescript
109
113
  import { scopedReverse } from "@rangojs/router";
@@ -111,18 +115,83 @@ import { scopedReverse } from "@rangojs/router";
111
115
  path("/product/:slug", (ctx) => {
112
116
  const reverse = scopedReverse<typeof shopPatterns>(ctx.reverse);
113
117
 
114
- reverse("cart"); // Type-safe local name
115
- reverse("product", { slug: "widget" }); // Type-safe with params
116
- reverse("blog.post"); // Absolute names (dot notation) always allowed
117
- reverse("/about"); // Path-based always allowed
118
+ reverse(".cart"); // Local name (dot-prefixed) resolves in include scope
119
+ reverse(".product", { slug: "widget" }); // Local name with params
120
+ reverse("blog.post", { slug: "hi" }); // Global name (dotted) full route map
118
121
 
119
122
  return <ProductPage slug={ctx.params.slug} />;
120
123
  }, { name: "product" })
121
124
  ```
122
125
 
126
+ `reverse()` does not accept raw path strings (`"/about"`). For static paths in client components, use `href("/about")`; on the server, look up the route by name.
127
+
128
+ ## Client components: receive URLs as props
129
+
130
+ `reverse()` is not available inside `"use client"` modules — there is no handler context and no route manifest in the browser bundle. Generate the URL on the server and hand it to the client component.
131
+
132
+ Three patterns, in order of preference:
133
+
134
+ 1. Pass as a prop from a server component:
135
+
136
+ ```tsx
137
+ // server
138
+ function BlogPostPage(ctx: HandlerContext) {
139
+ return <ShareButton url={ctx.reverse(".post", { slug: ctx.params.slug })} />;
140
+ }
141
+ ```
142
+
143
+ ```tsx
144
+ "use client";
145
+
146
+ export function ShareButton({ url }: { url: string }) {
147
+ return (
148
+ <button onClick={() => navigator.clipboard.writeText(url)}>Share</button>
149
+ );
150
+ }
151
+ ```
152
+
153
+ 2. Return from a loader (attached to the route via the DSL):
154
+
155
+ ```tsx
156
+ // server — loaders/nav.ts
157
+ export const NavLoader = createLoader((ctx) => ({
158
+ home: ctx.reverse("home"),
159
+ blog: ctx.reverse("blog.index"),
160
+ }));
161
+
162
+ // server — urls.tsx: attach the loader so useLoader has data in context
163
+ const urlpatterns = urls(({ path, loader }) => [
164
+ path("/", HomePage, { name: "home" }, () => [loader(NavLoader)]),
165
+ ]);
166
+ ```
167
+
168
+ ```tsx
169
+ "use client";
170
+
171
+ function Nav() {
172
+ const { data } = useLoader(NavLoader);
173
+ return <Link to={data.home}>Home</Link>;
174
+ }
175
+ ```
176
+
177
+ `useLoader()` requires the loader to be attached to an active route. If you need on-demand fetching instead, use `useFetchLoader()`.
178
+
179
+ 3. Return from a server action:
180
+
181
+ ```tsx
182
+ "use server";
183
+
184
+ export async function getProductUrl(slug: string) {
185
+ const ctx = getRequestContext();
186
+ return ctx.reverse("product", { slug });
187
+ }
188
+ ```
189
+
190
+ For static path strings (not named routes), client components can use `href()` — see below.
191
+
123
192
  ## Client: href()
124
193
 
125
- Plain function for absolute path-based URLs. No hook needed - works anywhere.
194
+ Plain function for absolute path-based URLs. No hook needed - works anywhere in client components. `href()` validates paths at compile time, but does **not** resolve named routes — for named routes, use one of the patterns above.
126
195
 
127
196
  ```typescript
128
197
  "use client";
@@ -187,13 +256,16 @@ function MountInfo() {
187
256
 
188
257
  ## When to use what
189
258
 
190
- | Context | API | Resolves | Use for |
191
- | ---------------- | ------------------------------- | ------------------------------- | ----------------------------------- |
192
- | Server handler | `ctx.reverse("name")` | Named routes (local + absolute) | Server-side URL generation |
193
- | Server handler | `scopedReverse<T>(ctx.reverse)` | Same, with type safety | Type-safe server URLs |
194
- | Client component | `href("/path")` | Absolute paths | Global navigation |
195
- | Client component | `useHref()` | Mount-prefixed paths | Local navigation inside `include()` |
196
- | Client component | `useMount()` | Raw mount path | Custom mount-aware logic |
259
+ | Context | API | Resolves | Use for |
260
+ | ---------------- | -------------------------------------------------- | ------------------------------- | ---------------------------------------------------------------- |
261
+ | Server handler | `ctx.reverse("name")` | Named routes (local + absolute) | **Default** server-side URL generation |
262
+ | Server handler | `scopedReverse<T>(ctx.reverse)` | Same, with type safety | Type-safe server URLs |
263
+ | Client component | (URL passed as prop / loader data / action return) | Named routes | Any URL derived from a named route — generate on server, pass in |
264
+ | Client component | `href("/path")` | Absolute paths (static strings) | Static navigation where no named-route lookup is needed |
265
+ | Client component | `useHref()` | Mount-prefixed paths | Local navigation inside `include()` |
266
+ | Client component | `useMount()` | Raw mount path | Custom mount-aware logic |
267
+
268
+ > `reverse()` is server-only. Client components never import or call it — they receive the already-resolved string.
197
269
 
198
270
  ## Complete example: mounted module
199
271
 
@@ -139,7 +139,29 @@ same memoized result — loaders never run twice per request.
139
139
 
140
140
  ## Loader Context
141
141
 
142
- Loaders receive the same context as route handlers:
142
+ Loaders receive the same context shape as route handlers.
143
+
144
+ ### Full field surface
145
+
146
+ | Field | Type | Notes |
147
+ | -------------- | ------------------------------ | --------------------------------------------------------------------------------------------------- |
148
+ | `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`. |
149
+ | `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden. |
150
+ | `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort). |
151
+ | `url` | `URL` | Parsed request URL. |
152
+ | `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`). |
153
+ | `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`. |
154
+ | `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise. |
155
+ | `env` | `TEnv` | Plain bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). |
156
+ | `get` | `(key \| ContextVar) => value` | Reads variables/context-vars set by middleware. |
157
+ | `use` | `(loader \| handle) => T` | Access another loader's data (Promise) or a handle's collected data (after `await ctx.rendered()`). |
158
+ | `rendered` | `() => Promise<void>` | **Experimental.** DSL loaders only — waits for non-loader segments before reading handle data. |
159
+ | `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
160
+ | `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
161
+ | `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
162
+ | `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers). |
163
+
164
+ ### Example
143
165
 
144
166
  ```typescript
145
167
  export const ProductLoader = createLoader(async (ctx) => {
@@ -163,10 +185,21 @@ export const ProductLoader = createLoader(async (ctx) => {
163
185
  // Variables set by middleware (from RSCRouter.Vars augmentation)
164
186
  const user = ctx.get("user");
165
187
 
166
- return { product: await fetchProduct(slug) };
188
+ // Type-checked URLs for payloads. `.name` resolves within the current
189
+ // include() scope; a bare `name` resolves globally. See /route and
190
+ // /typesafety for scope rules and route-name autocomplete.
191
+ const detailUrl = ctx.reverse(".detail", { slug });
192
+
193
+ return {
194
+ product: await fetchProduct(slug),
195
+ links: { self: detailUrl },
196
+ };
167
197
  });
168
198
  ```
169
199
 
200
+ See `/route` for the full handler-context contract (shared with loaders) and
201
+ `/typesafety` for route-name typing that powers `ctx.reverse` autocomplete.
202
+
170
203
  ### params vs routeParams
171
204
 
172
205
  - `ctx.params` — merged route params + explicit loader params. For fetchable
@@ -635,6 +635,7 @@ layout(<ShopLayout />, () => [
635
635
  | `useLocation().pathname` | `usePathname()` from `@rangojs/router/client` |
636
636
  | `useSearchParams()` | `useSearchParams()` from `@rangojs/router/client` |
637
637
  | `useParams()` | `useParams()` from `@rangojs/router/client` (or `ctx.params` in server handlers) |
638
+ | `useParams<T>()` | `useParams<T>()` — same generic annotation pattern |
638
639
  | `<NavLink>` | `<Link>` with `usePathname()` for active state |
639
640
 
640
641
  ### useNavigate → useRouter
@@ -400,6 +400,14 @@ path(
400
400
  Multiple response types can share the same URL pattern. See `/mime-routes` for the
401
401
  full content negotiation API (Accept header matching, Vary: Accept, multi-variant routes).
402
402
 
403
+ ## Long-Lived Responses (SSE / WebSocket)
404
+
405
+ For Server-Sent Events (`path.stream`) and WebSocket upgrades (`path.any`
406
+ returning a 101 / `webSocket` Response), see `/streams-and-websockets`.
407
+ Upgrade responses flow through without reconstruction; `Vary` and
408
+ `Server-Timing` are skipped, and stub headers are applied in place on a
409
+ best-effort basis.
410
+
403
411
  ## How It Works
404
412
 
405
413
  1. `path.json()` tags the route at the trie level with a MIME type