@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1fa245e2

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 (160) hide show
  1. package/{CLAUDE.md → AGENTS.md} +4 -0
  2. package/README.md +122 -30
  3. package/dist/bin/rango.js +245 -63
  4. package/dist/vite/index.js +859 -418
  5. package/package.json +3 -3
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +32 -0
  8. package/skills/caching/SKILL.md +49 -8
  9. package/skills/document-cache/SKILL.md +2 -2
  10. package/skills/hooks/SKILL.md +33 -31
  11. package/skills/host-router/SKILL.md +218 -0
  12. package/skills/links/SKILL.md +3 -1
  13. package/skills/loader/SKILL.md +72 -22
  14. package/skills/middleware/SKILL.md +2 -0
  15. package/skills/parallel/SKILL.md +126 -0
  16. package/skills/prerender/SKILL.md +112 -70
  17. package/skills/rango/SKILL.md +0 -1
  18. package/skills/route/SKILL.md +34 -4
  19. package/skills/router-setup/SKILL.md +95 -5
  20. package/skills/typesafety/SKILL.md +35 -23
  21. package/src/__internal.ts +92 -0
  22. package/src/bin/rango.ts +18 -0
  23. package/src/browser/app-version.ts +14 -0
  24. package/src/browser/event-controller.ts +5 -0
  25. package/src/browser/link-interceptor.ts +4 -0
  26. package/src/browser/navigation-bridge.ts +114 -18
  27. package/src/browser/navigation-client.ts +126 -44
  28. package/src/browser/navigation-store.ts +43 -8
  29. package/src/browser/navigation-transaction.ts +11 -9
  30. package/src/browser/partial-update.ts +80 -15
  31. package/src/browser/prefetch/cache.ts +166 -27
  32. package/src/browser/prefetch/fetch.ts +52 -39
  33. package/src/browser/prefetch/policy.ts +6 -0
  34. package/src/browser/prefetch/queue.ts +92 -20
  35. package/src/browser/prefetch/resource-ready.ts +77 -0
  36. package/src/browser/react/Link.tsx +70 -14
  37. package/src/browser/react/NavigationProvider.tsx +40 -4
  38. package/src/browser/react/context.ts +7 -2
  39. package/src/browser/react/use-handle.ts +9 -58
  40. package/src/browser/react/use-router.ts +21 -8
  41. package/src/browser/rsc-router.tsx +143 -59
  42. package/src/browser/scroll-restoration.ts +41 -42
  43. package/src/browser/segment-reconciler.ts +6 -1
  44. package/src/browser/server-action-bridge.ts +454 -436
  45. package/src/browser/types.ts +60 -5
  46. package/src/build/generate-manifest.ts +6 -6
  47. package/src/build/generate-route-types.ts +5 -0
  48. package/src/build/route-trie.ts +19 -3
  49. package/src/build/route-types/include-resolution.ts +8 -1
  50. package/src/build/route-types/router-processing.ts +346 -87
  51. package/src/build/route-types/scan-filter.ts +8 -1
  52. package/src/cache/cache-runtime.ts +15 -11
  53. package/src/cache/cache-scope.ts +48 -7
  54. package/src/cache/cf/cf-cache-store.ts +453 -11
  55. package/src/cache/cf/index.ts +5 -1
  56. package/src/cache/document-cache.ts +17 -7
  57. package/src/cache/index.ts +1 -0
  58. package/src/cache/taint.ts +55 -0
  59. package/src/client.rsc.tsx +2 -1
  60. package/src/client.tsx +3 -102
  61. package/src/context-var.ts +72 -2
  62. package/src/debug.ts +2 -2
  63. package/src/handle.ts +40 -0
  64. package/src/handles/breadcrumbs.ts +66 -0
  65. package/src/handles/index.ts +1 -0
  66. package/src/host/index.ts +0 -3
  67. package/src/index.rsc.ts +8 -37
  68. package/src/index.ts +40 -66
  69. package/src/prerender/store.ts +57 -15
  70. package/src/prerender.ts +138 -77
  71. package/src/reverse.ts +22 -1
  72. package/src/route-definition/dsl-helpers.ts +73 -25
  73. package/src/route-definition/helpers-types.ts +10 -6
  74. package/src/route-definition/index.ts +3 -3
  75. package/src/route-definition/redirect.ts +11 -3
  76. package/src/route-definition/resolve-handler-use.ts +149 -0
  77. package/src/route-map-builder.ts +7 -1
  78. package/src/route-types.ts +11 -0
  79. package/src/router/content-negotiation.ts +100 -1
  80. package/src/router/find-match.ts +4 -2
  81. package/src/router/handler-context.ts +108 -25
  82. package/src/router/intercept-resolution.ts +11 -4
  83. package/src/router/lazy-includes.ts +4 -1
  84. package/src/router/loader-resolution.ts +123 -11
  85. package/src/router/logging.ts +5 -2
  86. package/src/router/manifest.ts +9 -3
  87. package/src/router/match-api.ts +125 -190
  88. package/src/router/match-middleware/background-revalidation.ts +30 -2
  89. package/src/router/match-middleware/cache-lookup.ts +88 -16
  90. package/src/router/match-middleware/cache-store.ts +53 -10
  91. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  92. package/src/router/match-middleware/segment-resolution.ts +61 -5
  93. package/src/router/match-result.ts +22 -15
  94. package/src/router/metrics.ts +238 -13
  95. package/src/router/middleware-types.ts +53 -12
  96. package/src/router/middleware.ts +172 -85
  97. package/src/router/navigation-snapshot.ts +182 -0
  98. package/src/router/pattern-matching.ts +20 -5
  99. package/src/router/prerender-match.ts +114 -10
  100. package/src/router/preview-match.ts +30 -102
  101. package/src/router/request-classification.ts +310 -0
  102. package/src/router/revalidation.ts +27 -7
  103. package/src/router/route-snapshot.ts +245 -0
  104. package/src/router/router-context.ts +6 -1
  105. package/src/router/router-interfaces.ts +50 -5
  106. package/src/router/router-options.ts +50 -19
  107. package/src/router/segment-resolution/fresh.ts +200 -19
  108. package/src/router/segment-resolution/helpers.ts +30 -25
  109. package/src/router/segment-resolution/loader-cache.ts +1 -0
  110. package/src/router/segment-resolution/revalidation.ts +429 -301
  111. package/src/router/segment-wrappers.ts +2 -0
  112. package/src/router/trie-matching.ts +20 -2
  113. package/src/router/types.ts +1 -0
  114. package/src/router.ts +88 -15
  115. package/src/rsc/handler.ts +546 -359
  116. package/src/rsc/index.ts +0 -20
  117. package/src/rsc/manifest-init.ts +5 -1
  118. package/src/rsc/progressive-enhancement.ts +25 -8
  119. package/src/rsc/rsc-rendering.ts +35 -43
  120. package/src/rsc/server-action.ts +16 -10
  121. package/src/rsc/ssr-setup.ts +128 -0
  122. package/src/rsc/types.ts +10 -1
  123. package/src/search-params.ts +16 -13
  124. package/src/segment-system.tsx +140 -4
  125. package/src/server/context.ts +148 -16
  126. package/src/server/loader-registry.ts +9 -8
  127. package/src/server/request-context.ts +182 -34
  128. package/src/server.ts +6 -0
  129. package/src/ssr/index.tsx +4 -0
  130. package/src/static-handler.ts +18 -6
  131. package/src/theme/index.ts +4 -13
  132. package/src/types/cache-types.ts +4 -4
  133. package/src/types/handler-context.ts +149 -49
  134. package/src/types/loader-types.ts +36 -9
  135. package/src/types/route-config.ts +17 -8
  136. package/src/types/route-entry.ts +8 -1
  137. package/src/types/segments.ts +2 -5
  138. package/src/urls/path-helper-types.ts +9 -2
  139. package/src/urls/path-helper.ts +48 -13
  140. package/src/urls/pattern-types.ts +12 -0
  141. package/src/urls/response-types.ts +16 -6
  142. package/src/use-loader.tsx +73 -4
  143. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  144. package/src/vite/discovery/discover-routers.ts +23 -5
  145. package/src/vite/discovery/prerender-collection.ts +48 -15
  146. package/src/vite/discovery/state.ts +17 -13
  147. package/src/vite/index.ts +8 -3
  148. package/src/vite/plugin-types.ts +51 -79
  149. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  150. package/src/vite/plugins/expose-action-id.ts +1 -3
  151. package/src/vite/plugins/performance-tracks.ts +88 -0
  152. package/src/vite/plugins/refresh-cmd.ts +127 -0
  153. package/src/vite/plugins/version-plugin.ts +13 -1
  154. package/src/vite/rango.ts +174 -211
  155. package/src/vite/router-discovery.ts +169 -42
  156. package/src/vite/utils/banner.ts +3 -3
  157. package/src/vite/utils/prerender-utils.ts +78 -0
  158. package/src/vite/utils/shared-utils.ts +3 -2
  159. package/skills/testing/SKILL.md +0 -226
  160. package/src/route-definition/route-function.ts +0 -119
@@ -0,0 +1,218 @@
1
+ ---
2
+ name: host-router
3
+ description: Multi-app host routing with domain/subdomain patterns
4
+ argument-hint:
5
+ ---
6
+
7
+ # Host Router
8
+
9
+ Route requests to different apps based on domain, subdomain, or path prefix patterns. Supports middleware, lazy loading, cookie-based host override for dev, and a fallback handler.
10
+
11
+ ## Import
12
+
13
+ ```typescript
14
+ import { createHostRouter, defineHosts } from "@rangojs/router/host";
15
+ ```
16
+
17
+ ## Basic Setup
18
+
19
+ ```typescript
20
+ // host-router.ts
21
+ import { createHostRouter } from "@rangojs/router/host";
22
+
23
+ const router = createHostRouter();
24
+
25
+ router.host(["."]).map(() => import("./apps/main"));
26
+ router.host(["admin.*"]).map(() => import("./apps/admin"));
27
+ router.host(["api.*"]).map(() => import("./apps/api"));
28
+
29
+ export default {
30
+ fetch(request: Request, env: Env, ctx: ExecutionContext) {
31
+ return router.match(request, { env, ctx });
32
+ },
33
+ };
34
+ ```
35
+
36
+ Each `.map()` receives either a direct handler `(request, input) => Response` or a lazy import `() => import(...)`. Lazy imports resolve a module with a `default` export that is either a handler function or another `HostRouter` (for nesting).
37
+
38
+ ## Pattern Syntax
39
+
40
+ | Pattern | Matches |
41
+ | ----------------- | ---------------------------------------------- |
42
+ | `.` or `*` | Any apex domain (`example.com`) |
43
+ | `**` | Any domain (apex + all subdomains) |
44
+ | `*.` | Any single-level subdomain (`www.example.com`) |
45
+ | `**. ` | Any multi-level subdomain (`a.b.example.com`) |
46
+ | `example.com` | Exact domain |
47
+ | `*.com` | Any apex `.com` domain |
48
+ | `*.example.com` | Single subdomain of `example.com` |
49
+ | `**.example.com` | Any depth subdomain of `example.com` |
50
+ | `admin.*` | `admin` subdomain of any apex domain |
51
+ | `admin.**` | `admin` subdomain of any domain |
52
+ | `admin.` | `admin` subdomain of any apex (no wildcard) |
53
+ | `example.com/api` | Domain + path prefix (prefix match) |
54
+
55
+ Patterns are tested in registration order. First match wins.
56
+
57
+ ## `defineHosts` for Type Safety
58
+
59
+ ```typescript
60
+ import { defineHosts } from "@rangojs/router/host";
61
+
62
+ const hosts = defineHosts({
63
+ admin: "admin.*",
64
+ api: "api.*",
65
+ app: [".", "www.*"],
66
+ });
67
+
68
+ router.host(hosts.admin).map(() => import("./apps/admin"));
69
+ router.host(hosts.app).map(() => import("./apps/main"));
70
+ ```
71
+
72
+ Returns a frozen object — keys are autocompleted by TypeScript.
73
+
74
+ ## Middleware
75
+
76
+ Global middleware runs for every matched route. Per-route middleware runs only for that host pattern.
77
+
78
+ ```typescript
79
+ const router = createHostRouter();
80
+
81
+ // Global — runs for all routes
82
+ router.use(async (request, input, next) => {
83
+ console.log(`[${new Date().toISOString()}] ${request.url}`);
84
+ return next();
85
+ });
86
+
87
+ // Per-route
88
+ router
89
+ .host(["admin.*"])
90
+ .use(requireAuth)
91
+ .map(() => import("./apps/admin"));
92
+ ```
93
+
94
+ Middleware signature: `(request: Request, input: RouterRequestInput, next: () => Promise<Response>) => Promise<Response>`
95
+
96
+ Calling `next()` more than once throws.
97
+
98
+ ## Fallback Handler
99
+
100
+ Handles cookie-override errors when `hostOverride` is configured (e.g., override from a disallowed host, invalid cookie hostname). The fallback does **not** catch unmatched hosts — those throw `NoRouteMatchError`. Catch that at the worker level if you need a 404.
101
+
102
+ ```typescript
103
+ const router = createHostRouter({
104
+ hostOverride: { cookieName: "x-dev-host", allowedHosts: ["localhost"] },
105
+ });
106
+
107
+ // Called when cookie override fails (not for general unmatched hosts)
108
+ router.fallback().map((request) => {
109
+ return new Response("Invalid host override", { status: 400 });
110
+ });
111
+ ```
112
+
113
+ For unmatched hosts without `hostOverride`, catch `NoRouteMatchError` in your worker fetch:
114
+
115
+ ```typescript
116
+ import { NoRouteMatchError } from "@rangojs/router/host";
117
+
118
+ export default {
119
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
120
+ try {
121
+ return await router.match(request, { env, ctx });
122
+ } catch (err) {
123
+ if (err instanceof NoRouteMatchError) {
124
+ return new Response("Not Found", { status: 404 });
125
+ }
126
+ throw err;
127
+ }
128
+ },
129
+ };
130
+ ```
131
+
132
+ ## Cookie-Based Host Override
133
+
134
+ For development: route requests to a different app based on a cookie value, allowing developers to test different host routes from a single domain.
135
+
136
+ ```typescript
137
+ const router = createHostRouter({
138
+ hostOverride: {
139
+ cookieName: "x-dev-host",
140
+ allowedHosts: ["localhost", "**.dev.example.com"],
141
+ validate: (request, cookieValue, input) => {
142
+ // Optional custom validation — return the effective hostname
143
+ return cookieValue;
144
+ },
145
+ },
146
+ });
147
+ ```
148
+
149
+ When a request arrives:
150
+
151
+ 1. If no cookie → use actual hostname
152
+ 2. If cookie present and host is in `allowedHosts` → use cookie value as hostname
153
+ 3. If cookie present but host not allowed → throw `HostOverrideNotAllowedError`
154
+
155
+ Without a custom `validate`, the cookie value is validated as a hostname via `new URL()`.
156
+
157
+ ## Debug Mode
158
+
159
+ ```typescript
160
+ const router = createHostRouter({ debug: true });
161
+ ```
162
+
163
+ Logs pattern matching, route registration, and cookie override decisions to console.
164
+
165
+ ## Testing
166
+
167
+ ```typescript
168
+ import { createTestRequest, testPattern } from "@rangojs/router/host/testing";
169
+
170
+ // Test pattern matching
171
+ testPattern("admin.*", "admin.example.com"); // true
172
+ testPattern([".", "www.*"], "example.com"); // true
173
+
174
+ // Create requests for integration tests
175
+ const request = createTestRequest({
176
+ host: "admin.example.com",
177
+ path: "/dashboard",
178
+ cookies: { "x-dev-host": "api.example.com" },
179
+ });
180
+
181
+ // Test which route would match (without executing)
182
+ router.test("admin.example.com"); // { pattern, handler } | null
183
+ ```
184
+
185
+ ## Error Types
186
+
187
+ All errors extend `HostRouterError`:
188
+
189
+ | Error | When |
190
+ | ----------------------------- | ------------------------------------------- |
191
+ | `InvalidPatternError` | Pattern is empty, non-string, or has spaces |
192
+ | `HostOverrideNotAllowedError` | Cookie override from disallowed host |
193
+ | `InvalidHostnameError` | Cookie value isn't a valid hostname |
194
+ | `HostValidationError` | Custom `validate` function threw |
195
+ | `NoRouteMatchError` | No host pattern matched the request |
196
+ | `InvalidHandlerError` | Handler is not a function |
197
+
198
+ See the fallback section above for a `NoRouteMatchError` catch example.
199
+
200
+ ## Nesting Host Routers
201
+
202
+ A lazy handler can resolve to another `HostRouter`:
203
+
204
+ ```typescript
205
+ // apps/regional.ts
206
+ import { createHostRouter } from "@rangojs/router/host";
207
+
208
+ const regional = createHostRouter();
209
+ regional.host(["us.*"]).map(() => import("./regions/us"));
210
+ regional.host(["eu.*"]).map(() => import("./regions/eu"));
211
+
212
+ export default regional;
213
+ ```
214
+
215
+ ```typescript
216
+ // host-router.ts
217
+ router.host(["**.regional.example.com"]).map(() => import("./apps/regional"));
218
+ ```
@@ -139,7 +139,9 @@ function GlobalNav() {
139
139
  }
140
140
  ```
141
141
 
142
- `href()` is an identity function at runtime but provides compile-time validation via `ValidPaths` type. Paths are validated against registered route patterns using `PatternToPath`.
142
+ `href()` provides compile-time validation via `ValidPaths` type. Paths are validated against registered route patterns using `PatternToPath`.
143
+
144
+ `href()` is a raw path helper — it is **not** basename-aware. It returns the path as-is (or with the include mount prefix via `useHref()`). For basename-aware navigation, use `Link`, `useRouter().push()`, or `reverse()`, which auto-prefix root-relative paths with the router's basename.
143
145
 
144
146
  ## Client: useHref()
145
147
 
@@ -65,19 +65,10 @@ export const urlpatterns = urls(({ path, loader }) => [
65
65
 
66
66
  ## Consuming Loader Data
67
67
 
68
- ### In Server Components
69
-
70
- ```typescript
71
- import { useLoader } from "@rangojs/router";
72
- import { ProductLoader } from "./loaders/product";
73
-
74
- async function ProductPage() {
75
- const { product } = await useLoader(ProductLoader);
76
- return <h1>{product.name}</h1>;
77
- }
78
- ```
79
-
80
- ### In Client Components
68
+ Register loaders with `loader()` in the DSL and consume them in client
69
+ components with `useLoader()`. This is the recommended pattern — it keeps
70
+ data fetching on the server and consumption on the client, with a clean
71
+ separation that works correctly with `cache()`.
81
72
 
82
73
  ```typescript
83
74
  "use client";
@@ -90,6 +81,62 @@ function ProductDetails() {
90
81
  }
91
82
  ```
92
83
 
84
+ ```typescript
85
+ // Route definition — loader() registration required
86
+ path("/product/:slug", ProductPage, { name: "product" }, () => [
87
+ loader(ProductLoader),
88
+ ]);
89
+ ```
90
+
91
+ DSL loaders are the **live data layer** — they resolve fresh on every
92
+ request, even when the route is inside a `cache()` boundary. The router
93
+ excludes them from the segment cache at storage time and re-resolves them
94
+ on retrieval. This means `cache()` gives you cached UI + fresh data by
95
+ default.
96
+
97
+ ### Cache safety
98
+
99
+ DSL loaders can safely read `createVar({ cache: false })` variables
100
+ because they are always resolved fresh. The read guard is bypassed for
101
+ loader functions — they never produce stale data.
102
+
103
+ ### ctx.use(Loader) — escape hatch
104
+
105
+ For cases where you need loader data in the server handler itself (e.g.,
106
+ to set ctx variables or make routing decisions), use `ctx.use(Loader)`:
107
+
108
+ ```typescript
109
+ path("/product/:slug", async (ctx) => {
110
+ const { product } = await ctx.use(ProductLoader);
111
+ ctx.set(Product, product); // make available to children
112
+ return <ProductPage />;
113
+ }, { name: "product" }, () => [
114
+ loader(ProductLoader), // still register for client consumption
115
+ ])
116
+ ```
117
+
118
+ When you register with `loader()` in the DSL, `ctx.use()` returns the
119
+ same memoized result — loaders never run twice per request.
120
+
121
+ **Limitations of ctx.use(Loader):**
122
+
123
+ - The handler output depends on the loader data. If the route is inside
124
+ `cache()`, the handler is cached with the loader result baked in —
125
+ defeating the live data guarantee.
126
+ - Non-cacheable variable reads (`createVar({ cache: false })`) inside the
127
+ handler still throw, even if the data came from a loader.
128
+ - Prefer DSL `loader()` + client `useLoader()` for data that depends on
129
+ non-cacheable context variables.
130
+
131
+ **Never use `useLoader()` in server components** — it is a client-only API.
132
+
133
+ ### Summary
134
+
135
+ | Pattern | API | Cache-safe | Recommended |
136
+ | ---------------------- | ------------------- | ---------- | ----------- |
137
+ | DSL + client component | `useLoader(Loader)` | Yes | Yes |
138
+ | Handler escape hatch | `ctx.use(Loader)` | No | When needed |
139
+
93
140
  ## Loader Context
94
141
 
95
142
  Loaders receive the same context as route handlers:
@@ -507,7 +554,7 @@ export const ProductLoader = createLoader(async (ctx) => {
507
554
  .first();
508
555
 
509
556
  if (!product) {
510
- throw new Response("Product not found", { status: 404 });
557
+ notFound("Product not found");
511
558
  }
512
559
 
513
560
  return { product };
@@ -523,10 +570,9 @@ export const CartLoader = createLoader(async (ctx) => {
523
570
  return { cart };
524
571
  });
525
572
 
526
- // urls.tsx
573
+ // urls.tsx — register loaders in the DSL
527
574
  export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
528
575
  layout(<ShopLayout />, () => [
529
- // Shared cart loader for all shop routes
530
576
  loader(CartLoader, () => [
531
577
  revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
532
578
  ]),
@@ -538,18 +584,22 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
538
584
  ]),
539
585
  ]);
540
586
 
541
- // pages/product.tsx
542
- import { useLoader } from "@rangojs/router";
587
+ // components/ProductDetails.tsx — consume in client component
588
+ "use client";
589
+ import { useLoader } from "@rangojs/router/client";
543
590
  import { ProductLoader, CartLoader } from "./loaders/shop";
544
591
 
545
- async function ProductPage() {
546
- const { product } = await useLoader(ProductLoader);
547
- const { cart } = await useLoader(CartLoader);
592
+ function ProductDetails() {
593
+ const { data: { product } } = useLoader(ProductLoader);
594
+ const { data: { cart } } = useLoader(CartLoader);
548
595
 
549
596
  return (
550
597
  <div>
551
598
  <h1>{product.name}</h1>
552
- <AddToCartButton productId={product.id} inCart={cart?.items.includes(product.id)} />
599
+ <AddToCartButton
600
+ productId={product.id}
601
+ inCart={cart?.items.includes(product.id)}
602
+ />
553
603
  </div>
554
604
  );
555
605
  }
@@ -26,6 +26,8 @@ const router = createRouter<AppEnv>({})
26
26
  .routes(urlpatterns);
27
27
  ```
28
28
 
29
+ When the router has a `basename`, pattern-scoped `.use()` patterns are automatically prefixed. For example, with `basename: "/app"`, `.use("/admin/*", mw)` matches `/app/admin/*`.
30
+
29
31
  ### Route middleware (`middleware()` in `urls()`)
30
32
 
31
33
  Registered inside `urls()` callback. Wraps **rendering only** -- it does NOT wrap server action execution. Actions run before route middleware, so when route middleware executes during post-action revalidation, it can observe state that the action set (cookies, context variables, headers).
@@ -92,6 +92,73 @@ path("/dashboard/:id", (ctx) => {
92
92
  ])
93
93
  ```
94
94
 
95
+ ## Setting Handles (Meta, Breadcrumbs)
96
+
97
+ Parallel slot handlers can call `ctx.use(Meta)` or `ctx.use(Breadcrumbs)` to
98
+ push handle data. The data is associated with the **parent** layout or route
99
+ segment, not the parallel segment itself. This is because parallels execute
100
+ after their parent handler and inherit its segment scope.
101
+
102
+ This works well for document-level metadata — the handle data follows the
103
+ parent's lifecycle (appears when the parent is mounted, removed when it
104
+ unmounts).
105
+
106
+ ```typescript
107
+ parallel({
108
+ "@meta": (ctx) => {
109
+ const meta = ctx.use(Meta);
110
+ meta({ title: "Product Detail" });
111
+ meta({ name: "description", content: "..." });
112
+ return null; // UI-less slot, only sets metadata
113
+ },
114
+ "@sidebar": (ctx) => <Sidebar />,
115
+ })
116
+ ```
117
+
118
+ Multiple parallels on the same parent can each push handle data — they all
119
+ accumulate under the parent segment ID.
120
+
121
+ ### Pattern: `@meta` slot for per-route metadata overrides
122
+
123
+ A dedicated `@meta` parallel slot lets routes define metadata separately from
124
+ their handler logic. The layout sets defaults via a title template, and each
125
+ route overrides via its own `@meta` slot. Since child segments push after
126
+ parents and `collectMeta` uses last-wins deduplication, overrides work
127
+ naturally.
128
+
129
+ ```typescript
130
+ // Layout sets defaults
131
+ layout((ctx) => {
132
+ ctx.use(Meta)({ title: { template: "%s | Store", default: "Store" } });
133
+ return <StoreLayout />;
134
+ }, () => [
135
+ // Route with @meta override — decoupled from handler rendering
136
+ path("/:slug", ProductPage, { name: "product" }, () => [
137
+ parallel({
138
+ "@meta": async (ctx) => {
139
+ const product = await ctx.use(ProductLoader);
140
+ const meta = ctx.use(Meta);
141
+ meta({ title: product.name });
142
+ meta({ name: "description", content: product.description });
143
+ meta({
144
+ "script:ld+json": {
145
+ "@context": "https://schema.org",
146
+ "@type": "Product",
147
+ name: product.name,
148
+ description: product.description,
149
+ },
150
+ });
151
+ return null; // UI-less slot
152
+ },
153
+ }),
154
+ ]),
155
+ ])
156
+ ```
157
+
158
+ This keeps the route handler focused on rendering UI while metadata
159
+ (title, description, Open Graph, JSON-LD) lives in a composable slot that
160
+ can be added, removed, or swapped per route without touching the handler.
161
+
95
162
  ## Parallel Routes with Loaders
96
163
 
97
164
  Add loaders and loading states to parallel routes:
@@ -109,6 +176,65 @@ parallel(
109
176
  )
110
177
  ```
111
178
 
179
+ ### Streaming Behavior
180
+
181
+ Parallels with `loading()` are **independent streaming units**. They don't
182
+ block the parent layout or sibling routes during SSR:
183
+
184
+ - **With `loading()`**: The skeleton renders immediately. The loader runs
185
+ in the background and streams data to the client when ready. The rest
186
+ of the page (layout, route content, other parallels) renders without
187
+ waiting.
188
+ - **Without `loading()`**: The parallel's loaders block the parent layout's
189
+ rendering. Use this when the data must be available before the page
190
+ paints (e.g., critical above-the-fold content).
191
+ - **SPA navigation**: Parallel loaders resolve in the background. The
192
+ existing parallel UI stays visible — no skeleton flash on route changes
193
+ within the same layout.
194
+
195
+ ```typescript
196
+ // Sidebar streams independently — page renders immediately
197
+ parallel(
198
+ { "@sidebar": () => <Sidebar /> },
199
+ () => [loader(SlowSidebarLoader), loading(<SidebarSkeleton />)]
200
+ )
201
+
202
+ // Cart data blocks layout — must be ready before paint
203
+ parallel(
204
+ { "@cartBadge": () => <CartBadge /> },
205
+ () => [loader(CartCountLoader)] // No loading() = awaited
206
+ )
207
+ ```
208
+
209
+ ## Slot Override Semantics
210
+
211
+ When multiple `parallel()` calls define the same slot name, **the last
212
+ definition wins**. Earlier definitions of that slot are removed. Other
213
+ slots from the earlier call are preserved.
214
+
215
+ This enables composition patterns where included routes override
216
+ parent-defined slots:
217
+
218
+ ```typescript
219
+ layout(DashboardLayout, () => [
220
+ // Base slots
221
+ parallel({
222
+ "@sidebar": () => <DefaultSidebar />,
223
+ "@footer": () => <Footer />,
224
+ }),
225
+
226
+ // Override just @sidebar — @footer is preserved
227
+ parallel({ "@sidebar": () => <CustomSidebar /> }),
228
+
229
+ path("/", DashboardIndex, { name: "index" }),
230
+ ])
231
+ ```
232
+
233
+ After resolution, the layout has two parallel entries:
234
+
235
+ - `{ "@footer": () => <Footer /> }` (first call, `@sidebar` removed)
236
+ - `{ "@sidebar": () => <CustomSidebar /> }` (second call, wins)
237
+
112
238
  ## Multiple Parallel Slots
113
239
 
114
240
  ```typescript