@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2

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 (222) hide show
  1. package/AGENTS.md +6 -10
  2. package/README.md +289 -938
  3. package/dist/bin/rango.js +271 -46
  4. package/dist/vite/index.js +673 -193
  5. package/package.json +10 -8
  6. package/skills/api-client/SKILL.md +1 -1
  7. package/skills/breadcrumbs/SKILL.md +31 -14
  8. package/skills/cache-guide/SKILL.md +5 -2
  9. package/skills/caching/SKILL.md +59 -4
  10. package/skills/catalog.json +271 -0
  11. package/skills/comparison/SKILL.md +50 -0
  12. package/skills/comparison/agents/openai.yaml +4 -0
  13. package/skills/comparison/references/framework-comparison.md +837 -0
  14. package/skills/composability/SKILL.md +83 -2
  15. package/skills/debug-manifest/SKILL.md +1 -1
  16. package/skills/defer-hydration/SKILL.md +235 -0
  17. package/skills/document-cache/SKILL.md +9 -1
  18. package/skills/fonts/SKILL.md +1 -1
  19. package/skills/handler-use/SKILL.md +8 -8
  20. package/skills/hooks/SKILL.md +54 -892
  21. package/skills/hooks/data.md +273 -0
  22. package/skills/hooks/handle-and-actions.md +103 -0
  23. package/skills/hooks/navigation.md +110 -0
  24. package/skills/hooks/outlets.md +41 -0
  25. package/skills/hooks/state.md +228 -0
  26. package/skills/hooks/urls.md +135 -0
  27. package/skills/host-router/SKILL.md +4 -4
  28. package/skills/i18n/SKILL.md +1 -1
  29. package/skills/intercept/SKILL.md +46 -14
  30. package/skills/layout/SKILL.md +27 -10
  31. package/skills/links/SKILL.md +1 -1
  32. package/skills/loader/SKILL.md +23 -1
  33. package/skills/middleware/SKILL.md +7 -3
  34. package/skills/migrate-nextjs/SKILL.md +167 -6
  35. package/skills/migrate-react-router/SKILL.md +59 -677
  36. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  37. package/skills/migrate-react-router/component-migration.md +196 -0
  38. package/skills/migrate-react-router/data-and-actions.md +225 -0
  39. package/skills/migrate-react-router/route-mapping.md +271 -0
  40. package/skills/mime-routes/SKILL.md +1 -1
  41. package/skills/observability/SKILL.md +9 -1
  42. package/skills/parallel/SKILL.md +23 -4
  43. package/skills/ppr/SKILL.md +622 -0
  44. package/skills/prerender/SKILL.md +28 -18
  45. package/skills/rango/SKILL.md +84 -25
  46. package/skills/response-routes/SKILL.md +15 -1
  47. package/skills/route/SKILL.md +71 -4
  48. package/skills/router-setup/SKILL.md +14 -3
  49. package/skills/scripts/SKILL.md +1 -1
  50. package/skills/server-actions/SKILL.md +3 -2
  51. package/skills/shell-manifest/SKILL.md +185 -0
  52. package/skills/streams-and-websockets/SKILL.md +1 -1
  53. package/skills/tailwind/SKILL.md +1 -1
  54. package/skills/testing/SKILL.md +2 -1
  55. package/skills/testing/handles.md +4 -2
  56. package/skills/testing/render-handler.md +15 -14
  57. package/skills/testing/reverse-and-types.md +8 -7
  58. package/skills/theme/SKILL.md +1 -1
  59. package/skills/typesafety/SKILL.md +45 -919
  60. package/skills/typesafety/env-and-bindings.md +254 -0
  61. package/skills/typesafety/generated-files-and-cli.md +335 -0
  62. package/skills/typesafety/params-and-search.md +153 -0
  63. package/skills/typesafety/route-types.md +209 -0
  64. package/skills/use-cache/SKILL.md +30 -3
  65. package/skills/vercel/SKILL.md +1 -1
  66. package/skills/view-transitions/SKILL.md +44 -1
  67. package/src/browser/event-controller.ts +62 -10
  68. package/src/browser/logging.ts +28 -0
  69. package/src/browser/merge-segment-loaders.ts +6 -4
  70. package/src/browser/navigation-bridge.ts +65 -16
  71. package/src/browser/navigation-client.ts +32 -2
  72. package/src/browser/navigation-store.ts +128 -14
  73. package/src/browser/network-error-handler.ts +34 -7
  74. package/src/browser/partial-update.ts +76 -17
  75. package/src/browser/prefetch/cache.ts +51 -11
  76. package/src/browser/prefetch/fetch.ts +59 -21
  77. package/src/browser/prefetch/queue.ts +19 -4
  78. package/src/browser/react/Link.tsx +13 -3
  79. package/src/browser/react/NavigationProvider.tsx +108 -4
  80. package/src/browser/response-adapter.ts +38 -9
  81. package/src/browser/rsc-router.tsx +54 -4
  82. package/src/browser/scroll-restoration.ts +7 -5
  83. package/src/browser/segment-reconciler.ts +31 -21
  84. package/src/browser/server-action-bridge.ts +22 -10
  85. package/src/browser/types.ts +54 -1
  86. package/src/build/generate-manifest.ts +155 -131
  87. package/src/build/index.ts +3 -1
  88. package/src/build/route-trie.ts +35 -7
  89. package/src/build/route-types/include-resolution.ts +347 -47
  90. package/src/build/runtime-discovery.ts +4 -1
  91. package/src/cache/cache-key-utils.ts +29 -0
  92. package/src/cache/cache-runtime.ts +262 -71
  93. package/src/cache/cache-scope.ts +2 -17
  94. package/src/cache/cache-tag.ts +60 -14
  95. package/src/cache/cf/cf-cache-store.ts +243 -20
  96. package/src/cache/document-cache.ts +54 -21
  97. package/src/cache/index.ts +1 -0
  98. package/src/cache/memory-segment-store.ts +110 -3
  99. package/src/cache/profile-registry.ts +15 -0
  100. package/src/cache/read-through-swr.ts +15 -1
  101. package/src/cache/segment-codec.ts +4 -4
  102. package/src/cache/shell-snapshot.ts +417 -0
  103. package/src/cache/types.ts +158 -0
  104. package/src/cache/vercel/vercel-cache-store.ts +401 -124
  105. package/src/client.rsc.tsx +0 -3
  106. package/src/client.tsx +0 -3
  107. package/src/cloudflare/tracing.ts +7 -8
  108. package/src/defer.ts +11 -22
  109. package/src/handle.ts +37 -15
  110. package/src/handles/MetaTags.tsx +16 -82
  111. package/src/handles/breadcrumbs.ts +12 -14
  112. package/src/handles/deferred-resolution.ts +127 -0
  113. package/src/handles/is-thenable.ts +7 -8
  114. package/src/handles/meta.ts +7 -44
  115. package/src/host/errors.ts +15 -0
  116. package/src/host/index.ts +1 -0
  117. package/src/index.rsc.ts +8 -2
  118. package/src/index.ts +19 -13
  119. package/src/internal-debug.ts +11 -8
  120. package/src/prerender.ts +17 -4
  121. package/src/redirect-origin.ts +14 -0
  122. package/src/render-error-thrower.tsx +20 -0
  123. package/src/route-content-wrapper.tsx +12 -5
  124. package/src/route-definition/dsl-helpers.ts +21 -32
  125. package/src/route-definition/helper-factories.ts +0 -2
  126. package/src/route-definition/helpers-types.ts +43 -43
  127. package/src/route-definition/index.ts +1 -2
  128. package/src/route-definition/resolve-handler-use.ts +0 -1
  129. package/src/route-definition/use-item-types.ts +3 -6
  130. package/src/route-map-builder.ts +41 -4
  131. package/src/route-types.ts +0 -5
  132. package/src/router/find-match.ts +86 -8
  133. package/src/router/instrument.ts +9 -4
  134. package/src/router/lazy-includes.ts +72 -12
  135. package/src/router/loader-resolution.ts +14 -2
  136. package/src/router/manifest.ts +56 -11
  137. package/src/router/match-api.ts +76 -32
  138. package/src/router/match-handlers.ts +181 -135
  139. package/src/router/match-middleware/background-revalidation.ts +40 -23
  140. package/src/router/match-middleware/cache-store.ts +39 -24
  141. package/src/router/match-result.ts +35 -15
  142. package/src/router/middleware.ts +64 -38
  143. package/src/router/navigation-snapshot.ts +7 -5
  144. package/src/router/parse-pattern.ts +115 -0
  145. package/src/router/pattern-matching.ts +53 -64
  146. package/src/router/prefetch-limits.ts +37 -0
  147. package/src/router/prerender-match.ts +11 -5
  148. package/src/router/preview-match.ts +3 -1
  149. package/src/router/request-classification.ts +23 -8
  150. package/src/router/route-snapshot.ts +14 -2
  151. package/src/router/router-context.ts +3 -1
  152. package/src/router/router-interfaces.ts +32 -1
  153. package/src/router/router-options.ts +30 -0
  154. package/src/router/segment-resolution/fresh.ts +39 -3
  155. package/src/router/segment-resolution/loader-cache.ts +93 -2
  156. package/src/router/segment-resolution/loader-mask.ts +60 -0
  157. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  158. package/src/router/segment-resolution/mask-nested.ts +83 -0
  159. package/src/router/segment-resolution/revalidation.ts +3 -0
  160. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  161. package/src/router/substitute-pattern-params.ts +54 -35
  162. package/src/router/telemetry-otel.ts +6 -8
  163. package/src/router/telemetry.ts +9 -1
  164. package/src/router/tracing.ts +14 -5
  165. package/src/router/trie-matching.ts +19 -11
  166. package/src/router/url-params.ts +13 -0
  167. package/src/router.ts +47 -16
  168. package/src/rsc/full-payload.ts +70 -0
  169. package/src/rsc/handler.ts +60 -33
  170. package/src/rsc/manifest-init.ts +1 -1
  171. package/src/rsc/nonce.ts +10 -1
  172. package/src/rsc/progressive-enhancement.ts +61 -4
  173. package/src/rsc/redirect-guard.ts +2 -1
  174. package/src/rsc/rsc-rendering.ts +429 -37
  175. package/src/rsc/server-action.ts +25 -2
  176. package/src/rsc/shell-capture.ts +1190 -0
  177. package/src/rsc/shell-serve.ts +181 -0
  178. package/src/rsc/transition-gate.ts +89 -0
  179. package/src/rsc/types.ts +30 -0
  180. package/src/segment-loader-promise.ts +18 -0
  181. package/src/segment-system.tsx +149 -14
  182. package/src/server/context.ts +67 -9
  183. package/src/server/cookie-store.ts +73 -1
  184. package/src/server/loader-registry.ts +13 -1
  185. package/src/server/request-context.ts +169 -10
  186. package/src/ssr/index.tsx +462 -178
  187. package/src/ssr/inject-rsc-eager.ts +167 -0
  188. package/src/ssr/ssr-root.tsx +228 -0
  189. package/src/testing/collect-handle.ts +14 -8
  190. package/src/testing/dispatch.ts +152 -40
  191. package/src/testing/generated-routes.ts +27 -11
  192. package/src/testing/index.ts +6 -0
  193. package/src/testing/render-handler.ts +14 -0
  194. package/src/testing/render-route.tsx +13 -10
  195. package/src/testing/run-transition-when.ts +164 -0
  196. package/src/theme/ThemeProvider.tsx +36 -26
  197. package/src/types/handler-context.ts +1 -1
  198. package/src/types/index.ts +2 -0
  199. package/src/types/route-config.ts +19 -7
  200. package/src/types/segments.ts +100 -0
  201. package/src/urls/include-helper.ts +10 -8
  202. package/src/urls/include-provider.ts +71 -0
  203. package/src/urls/index.ts +1 -0
  204. package/src/urls/path-helper-types.ts +44 -12
  205. package/src/urls/path-helper.ts +5 -0
  206. package/src/urls/pattern-types.ts +36 -0
  207. package/src/urls/type-extraction.ts +43 -18
  208. package/src/urls/urls-function.ts +0 -1
  209. package/src/vercel/tracing.ts +7 -7
  210. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  211. package/src/vite/discovery/discover-routers.ts +1 -1
  212. package/src/vite/discovery/discovery-errors.ts +61 -0
  213. package/src/vite/index.ts +7 -0
  214. package/src/vite/inject-client-debug.ts +88 -0
  215. package/src/vite/plugins/vercel-output.ts +114 -25
  216. package/src/vite/plugins/version-injector.ts +22 -7
  217. package/src/vite/plugins/virtual-entries.ts +80 -22
  218. package/src/vite/rango.ts +29 -19
  219. package/src/vite/router-discovery.ts +171 -43
  220. package/src/vite/utils/prerender-utils.ts +17 -4
  221. package/src/vite/utils/shared-utils.ts +47 -0
  222. package/src/network-error-thrower.tsx +0 -18
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: composability
3
- description: Reusable composition patterns with globally importable route helpers in @rangojs/router
3
+ description: Reusable composition patterns with globally importable route helpers in @rangojs/router. Use when sharing loaders, middleware, or handler logic across multiple route files, or avoiding copy-pasting the same route setup everywhere.
4
4
  argument-hint: "pattern-name"
5
5
  ---
6
6
 
@@ -22,7 +22,6 @@ import {
22
22
  loading,
23
23
  parallel,
24
24
  intercept,
25
- when,
26
25
  errorBoundary,
27
26
  notFoundBoundary,
28
27
  } from "@rangojs/router";
@@ -161,6 +160,88 @@ export const adminPatterns = urls(({ path }) => [
161
160
  ]);
162
161
  ```
163
162
 
163
+ ## Code-splitting a route group with async include()
164
+
165
+ `include()` takes a route module two ways. Eager — the module is already in the
166
+ graph:
167
+
168
+ ```typescript
169
+ import { shopPatterns } from "./shop-patterns";
170
+
171
+ urls(({ include }) => [include("/shop", shopPatterns, { name: "shop" })]);
172
+ ```
173
+
174
+ Async — the module is code-split behind a `() => import()` thunk. It becomes its
175
+ own chunk that is NOT evaluated at startup; the router imports it on the first
176
+ request that reaches the prefix, then caches it:
177
+
178
+ ```typescript
179
+ urls(({ include }) => [
180
+ include("/shop", () => import("./shop-patterns"), { name: "shop" }),
181
+ ]);
182
+ ```
183
+
184
+ The split module exposes its `urls()` value as the default export (convention):
185
+
186
+ ```typescript
187
+ // src/shop-patterns.ts
188
+ import { urls } from "@rangojs/router";
189
+
190
+ export const shopPatterns = urls(({ path, include }) => [
191
+ path("/", ShopHome, { name: "home" }),
192
+ include("/product", productPatterns, { name: "product" }), // nesting is fine
193
+ ]);
194
+
195
+ export default shopPatterns; // async include() resolves this
196
+ ```
197
+
198
+ **Prefer the async form** for any route group that is a natural,
199
+ independently-loadable unit (a localized section, an admin area, an API surface
200
+ with heavy handlers) — it trims the eagerly-parsed entry bundle and keeps that
201
+ subgraph off the cold-start path. The **eager form is still fully valid** (not
202
+ deprecated): keep it for small groups, or ones that share most of their module
203
+ graph with the entry (the bundler keeps shared modules common regardless, so
204
+ splitting a thin group buys little). Both match identically at runtime — only the
205
+ module's runtime evaluation timing differs.
206
+
207
+ What you do NOT lose by splitting: build-time discovery `await`s the provider, so
208
+ `href()`, `reverse()`, generated route types, and prerender still see every route
209
+ in the group — including nested `include()`s inside the split module. Only the
210
+ module's runtime evaluation defers. `rango generate` resolves the `() => import()`
211
+ the same way, so a code-split group is still fully typed.
212
+
213
+ ### Sizing async include groups (measured)
214
+
215
+ The first request into an async group pays that group's chunk import; every
216
+ request after that is flat. Measured on a deployed Cloudflare worker with
217
+ 26k routes (2026-07, warm RTT floor ~23 ms):
218
+
219
+ | Group size | First-hit latency |
220
+ | -------------------------- | ------------------------------------ |
221
+ | ~240 routes | ~75 ms (≈ RTT + eval) |
222
+ | ~5,000 routes | ~137 ms |
223
+ | ~9,000 routes | ~188 ms |
224
+ | 3-level nested async chain | ~464 ms (levels import sequentially) |
225
+
226
+ Three rules fall out of those numbers:
227
+
228
+ 1. **Prefer more, smaller groups over few giant ones.** First-hit cost scales
229
+ with routes-per-chunk; fifty 250-route groups each cost a fraction of one
230
+ 9k-route group, and only the group actually visited pays anything.
231
+ 2. **Keep async-include chains shallow on latency-sensitive paths.** Each
232
+ nested `() => import()` level awaits in sequence, so depth multiplies the
233
+ first hit. Nesting eager includes inside one async module costs one chunk;
234
+ nesting async inside async costs one chunk per level.
235
+ 3. **Give sibling groups distinct static prefixes.** Siblings that share a
236
+ static prefix (`include("/x/:a", …)` next to `include("/x/:b", …)`) all
237
+ import on the first hit to that prefix — the router cannot tell which one
238
+ matches before loading them.
239
+
240
+ Warm-path matching is O(path segments) via the precomputed trie regardless of
241
+ group layout — this sizing only shapes cold/first-hit behavior. For
242
+ latency-critical prefixes, a post-deploy warmup ping (one request per prefix)
243
+ erases first-hit cost for the isolate entirely.
244
+
164
245
  ## Composition Types
165
246
 
166
247
  For typed factories, import the composition types:
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: debug-manifest
3
- description: Debug and inspect route manifest structure
3
+ description: Debug and inspect route manifest structure. Use when routes aren't matching as expected, you need to see the generated route tree, or a path resolves to the wrong handler.
4
4
  argument-hint:
5
5
  ---
6
6
 
@@ -0,0 +1,235 @@
1
+ ---
2
+ name: defer-hydration
3
+ description: Keep the full body HTML in a PPR shell's first paint while moving a heavy subtree's hydration off the initial main-thread task — a gated Suspense boundary with the content as its own fallback, released on first idle. Use when a shell HIT paints fast but one long hydration task blocks the main thread, TTI/INP is poor despite instant paint, or a plain Suspense boundary left an empty hole in the frozen prelude.
4
+ argument-hint:
5
+ ---
6
+
7
+ # Deferred hydration: gated boundary, content-as-fallback
8
+
9
+ PPR (`/ppr`) makes first paint instant — the frozen prelude flushes before any
10
+ render work. It does nothing about what happens next: React hydrates the page
11
+ as one synchronous main-thread task, and on a content-heavy page that task can
12
+ block every click and scroll handler for seconds. Measured on a production
13
+ storefront homepage (shell HIT, production build, M-series laptop): a single
14
+ 2543ms task. On the dev server the same page produced a 7.2s task — the page
15
+ was dead for ~11s. Fast paint, frozen page.
16
+
17
+ The obvious fix — wrap the heavy subtree in `<Suspense>` so it hydrates later —
18
+ trades the paint away: under shell capture that boundary postpones, and the
19
+ frozen prelude ships an empty `<main>`. This recipe gets both: the full body
20
+ HTML in the prelude AND its hydration off the initial task, released on first
21
+ idle at retry-lane priority.
22
+
23
+ | | baseline | plain `fallback={null}` boundary | gated content-as-fallback |
24
+ | -------------------------------- | -------- | -------------------------------- | ------------------------- |
25
+ | body in first paint | yes | **no — empty `<main>`** | yes |
26
+ | worst main-thread task | 2543ms | 844ms | **386ms** |
27
+ | total blocked time (>50ms tasks) | 2705ms | 1436ms | **554ms** |
28
+ | interactive (menu click works) | ~3.5s | ~1.6s | ~1.6s |
29
+
30
+ Measure your own page before and after — the win depends on how much of the
31
+ hydration cost lives under the boundary (see "Verifying and measuring").
32
+
33
+ ## The recipe
34
+
35
+ ~40 lines, plain React, no rango imports — copy it into your app:
36
+
37
+ ```tsx
38
+ "use client";
39
+ import type { ReactNode } from "react";
40
+ import { Suspense, startTransition, use, useEffect } from "react";
41
+
42
+ let released = false;
43
+ let releaseFn: (() => void) | undefined;
44
+ const gate = new Promise<void>((resolve) => {
45
+ releaseFn = resolve;
46
+ });
47
+
48
+ function releaseHydrationGate() {
49
+ if (released) return;
50
+ released = true;
51
+ // Transition so the boundary retry/hydration is scheduled non-urgent,
52
+ // never as a sync flush from the idle callback.
53
+ startTransition(() => releaseFn?.());
54
+ }
55
+
56
+ function HydrationGate() {
57
+ // Server: inert (children SSR normally). Client before release: suspend,
58
+ // so React skips hydrating the boundary and KEEPS the server DOM.
59
+ if (typeof window !== "undefined" && !released) use(gate);
60
+ return null;
61
+ }
62
+
63
+ function ReleaseHydrationGate() {
64
+ // Sibling of the boundary — NEVER under it (its effect would deadlock
65
+ // holding its own key). Hydrates with the early pass.
66
+ useEffect(() => {
67
+ if (released) return;
68
+ if ("requestIdleCallback" in window) {
69
+ requestIdleCallback(releaseHydrationGate, { timeout: 1500 });
70
+ } else {
71
+ setTimeout(releaseHydrationGate, 200);
72
+ }
73
+ }, []);
74
+ return null;
75
+ }
76
+
77
+ export function DeferredHydration({ children }: { children: ReactNode }) {
78
+ return (
79
+ <>
80
+ <ReleaseHydrationGate />
81
+ <Suspense fallback={children}>
82
+ <HydrationGate />
83
+ {children}
84
+ </Suspense>
85
+ </>
86
+ );
87
+ }
88
+ ```
89
+
90
+ Wrap the heavy subtree — typically the page body under the app chrome:
91
+
92
+ ```tsx
93
+ <DeferredHydration>
94
+ <HomePageBody />
95
+ </DeferredHydration>
96
+ ```
97
+
98
+ The chrome (header, nav — whatever must respond to the first click) stays
99
+ outside the boundary and hydrates in the early, now-small task. Everything
100
+ inside hydrates after first idle.
101
+
102
+ ## Why `fallback={children}` is load-bearing
103
+
104
+ This is not a style choice; it is the half of the recipe that makes it
105
+ PPR-compatible.
106
+
107
+ Shell capture aborts on flight byte-quiet (`src/rsc/shell-capture.ts`,
108
+ `FLIGHT_QUIET_HOPS`): once the Flight source has been byte-silent for the
109
+ quiet window, the fizz render freezes. A big HTML subtree under _any_
110
+ `<Suspense>` boundary cannot finish inside that window, so the boundary always
111
+ postpones — boundary placement cannot fix it. Verified both ways: wrapping the
112
+ client island from outside AND placing the boundary inside the island both
113
+ baked `<!--$?--><template id="B:…">` into `<main>`, i.e. an empty body in the
114
+ frozen prelude.
115
+
116
+ With the content as the fallback, the unavoidable postpone _becomes the
117
+ delivery mechanism_: the shell bakes the fallback, and the fallback IS the
118
+ body. In `/ppr` hole-doctrine terms, this is the PHYSICS class exploited
119
+ deliberately — you cannot stop the boundary from becoming a hole, so you make
120
+ the hole's baked fallback carry the real markup.
121
+
122
+ ## Why the client gate is free
123
+
124
+ Suspending during hydration keeps the **server DOM**, not the fallback. When
125
+ `HydrationGate` suspends on the client, React skips hydrating that boundary
126
+ and leaves the baked HTML in place — visible, styled, inert. On release, the
127
+ boundary retries on the retry lane (interruptible, non-urgent thanks to the
128
+ `startTransition` in `releaseHydrationGate`), and the existing DOM hydrates in
129
+ place. No blank, no flicker, no re-paint.
130
+
131
+ ## The sync-update trap (scar tissue)
132
+
133
+ A **synchronous** update that reaches into a dehydrated boundary makes React
134
+ abandon hydration and client-render it instead. That client render suspends on
135
+ the gate and renders the fallback. With `fallback={children}` this is a visual
136
+ no-op (but wasted work); with `fallback={null}` it blanks the page.
137
+
138
+ The corollary: provider data syncs that land right after the chrome hydrates —
139
+ basket, wishlist, auth state read from storage in an effect — MUST be
140
+ `startTransition`-wrapped. This was measured, not theorized: without the
141
+ transitions, the boundary was force-hydrated synchronously and the split
142
+ evaporated (the 2543ms task survived intact).
143
+
144
+ ```tsx
145
+ useEffect(() => {
146
+ const stored = readBasketFromStorage();
147
+ startTransition(() => setBasket(stored)); // NOT a bare setBasket(stored)
148
+ }, []);
149
+ ```
150
+
151
+ ## Pre-release interaction semantics
152
+
153
+ Between paint and release (window ≈ one idle, capped by the `requestIdleCallback`
154
+ timeout — 1500ms in the reference):
155
+
156
+ - **Native anchors work** — they are plain HTML in the server DOM, plus any
157
+ click-delegation living above the boundary.
158
+ - **React `onClick`s inside the gated subtree queue** via React's event replay
159
+ and fire on hydration after release.
160
+
161
+ If the gated subtree's first-click latency matters more than idle timing,
162
+ release on interaction instead (see Variations).
163
+
164
+ ## Known cost: the body rides twice (measure it, don't guess)
165
+
166
+ On a shell HIT the gated subtree's HTML is in the response twice — once as the
167
+ baked fallback in the prelude, once as the resumed hole content (the resume
168
+ re-renders and re-ships it; there is no bake-through). Homepage measurement:
169
+ 234KB → 302KB gzipped (+68KB, +29%; raw +1.05MB). It is post-paint bandwidth,
170
+ not render-blocking — the visible prelude streams first — but it is real bytes
171
+ on every document GET. Weigh it per page; on a small body the recipe may not
172
+ pay for itself.
173
+
174
+ ## Verifying and measuring
175
+
176
+ Production build only — dev-server hydration numbers are noise (module
177
+ transforms dominate; the 7.2s dev task above vs 2543ms in production).
178
+
179
+ **Body in the prelude.** Fetch the document and check a distinctive piece of
180
+ body markup appears BEFORE the first resumed segment:
181
+
182
+ ```
183
+ curl -s -H "Accept: text/html" http://localhost:4173/ \
184
+ | awk '{ if (match($0, /<div hidden id="S:/)) { print substr($0, 1, RSTART); exit } print }' \
185
+ | grep -c "Best Sellers" # any string unique to the gated body
186
+ ```
187
+
188
+ `0` with `<!--$?--><template id="B:` markers inside `<main>` means an empty
189
+ hole baked instead — the fallback is not the content (wrong boundary, or a
190
+ plain `fallback={null}`/skeleton boundary).
191
+
192
+ **Main-thread tasks.** Paste a longtask observer in the console before
193
+ reloading, then compare the worst task with the recipe on and off:
194
+
195
+ ```js
196
+ new PerformanceObserver((l) =>
197
+ l
198
+ .getEntries()
199
+ .forEach((e) => console.log("longtask", Math.round(e.duration))),
200
+ ).observe({ entryTypes: ["longtask"] });
201
+ ```
202
+
203
+ **Interactivity.** Click the chrome (menu, nav) immediately after paint — it
204
+ should respond while the gated body is still inert.
205
+
206
+ ## Variations
207
+
208
+ - **Release on visible** — an `IntersectionObserver` per boundary instead of
209
+ `requestIdleCallback`: below-the-fold sections hydrate only when scrolled
210
+ near.
211
+ - **Release on first interaction** — a capture-phase listener
212
+ (`pointerdown`/`keydown` on `window`) that releases immediately: the queued
213
+ event replays into the touched boundary and React's selective hydration
214
+ prioritizes it. Best when the gated subtree is itself the interaction
215
+ target.
216
+ - **One shared gate vs per-boundary gates** — the reference uses one
217
+ module-level gate (first release wins, all boundaries hydrate together).
218
+ Multiple independent boundaries (visible-based, per-section) need one
219
+ gate/`released` pair per boundary — factor the module into a
220
+ `createHydrationGate()` if you go there.
221
+
222
+ ## What this deliberately is not
223
+
224
+ The duplicated payload has an obvious framework-level fix: a "bake-through"
225
+ boundary that bakes the boundary _content_ into the prelude and skips the
226
+ redundant hole resume. That is deliberately NOT part of this recipe — a recipe
227
+ has zero API commitment, and React's `<Activity>`/postpone work may land under
228
+ this exact space. The recipe survives that future; a primitive might not.
229
+
230
+ ## Related
231
+
232
+ - `/ppr` — the shell/hole mechanics this recipe rides on (hole doctrine:
233
+ PHYSICS class), and why the capture postpones any big Suspense subtree
234
+ - `src/rsc/shell-capture.ts` — the byte-quiet capture window
235
+ (`FLIGHT_QUIET_HOPS`) that makes `fallback={children}` mandatory
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: document-cache
3
- description: Cache full HTTP responses at the edge with Cache-Control headers
3
+ description: Cache the whole HTTP response at the edge with Cache-Control headers. Use when caching an entire page or response at a CDN edge, setting Cache-Control headers, or cutting origin hits for public pages — not for caching a single segment or function.
4
4
  argument-hint: [setup]
5
5
  ---
6
6
 
@@ -8,6 +8,14 @@ argument-hint: [setup]
8
8
 
9
9
  Caches complete HTTP responses (HTML/RSC) at the edge based on Cache-Control headers. Routes opt-in by setting `s-maxage`.
10
10
 
11
+ ## Not this skill if…
12
+
13
+ - You want loaders to stay live while rendered segments are reused — document
14
+ caching freezes the WHOLE response, loader output included; segment caching
15
+ is `cache()`: see `/caching`.
16
+ - You want a cached HTML shell with per-request live holes — see `/ppr`.
17
+ - You are unsure which cache layer you need — start at `/cache-guide`.
18
+
11
19
  ## Setup
12
20
 
13
21
  Document caching is a middleware. Add `createDocumentCacheMiddleware()` to the
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fonts
3
- description: Load and configure web fonts with preload hints for optimal performance
3
+ description: Load and configure web fonts with preload hints for optimal performance. Use when adding a custom font to a Rango app, fonts flash or swap on load (FOUT/FOIT), or you want font preload for performance.
4
4
  argument-hint: [provider]
5
5
  ---
6
6
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: handler-use
3
- description: Attach default loaders, middleware, parallels, and other use items directly to handlers via handler.use, and compose them with explicit use() at mount sites
3
+ description: Attach default loaders, middleware, parallels, and other use items directly to handlers via handler.use, and compose them with explicit use() at mount sites. Use when a handler needs its own default middleware/loader without repeating use() at every mount site, or deciding between handler-level defaults and per-route use().
4
4
  argument-hint: "[handler]"
5
5
  ---
6
6
 
@@ -51,13 +51,13 @@ Now `ProductPage` carries its loader, loading state, and response-header middlew
51
51
 
52
52
  `handler.use()` is the same callback shape regardless of where the handler runs, but the runtime validates that the items it returns are valid for the mount site. Driven by `MOUNT_SITE_ALLOWED_TYPES` in [resolve-handler-use.ts](../../src/route-definition/resolve-handler-use.ts):
53
53
 
54
- | Mount site | Allowed item types |
55
- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
56
- | `path()` / `route()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
57
- | `layout()` | All of the above, plus `route`, `include` |
58
- | `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
59
- | `intercept()` | `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `layout`, `route`, `when`, `transition` |
60
- | Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
54
+ | Mount site | Allowed item types |
55
+ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56
+ | `path()` / `route()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
57
+ | `layout()` | All of the above, plus `route`, `include` |
58
+ | `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
59
+ | `intercept()` | `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `layout`, `route`, `transition` (conditional activation is config-only: `intercept(..., { when })`) |
60
+ | Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
61
61
 
62
62
  For per-item semantics see the dedicated skills: [middleware](../middleware/SKILL.md), [loader](../loader/SKILL.md), [parallel](../parallel/SKILL.md), [intercept](../intercept/SKILL.md), [layout](../layout/SKILL.md), [view-transitions](../view-transitions/SKILL.md).
63
63