@rangojs/router 0.21.0 → 0.22.1

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 (244) hide show
  1. package/dist/bin/rango.js +13 -1
  2. package/dist/types/browser/event-controller.d.ts +30 -13
  3. package/dist/types/browser/navigation-store.d.ts +9 -0
  4. package/dist/types/browser/partial-update.d.ts +2 -1
  5. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  6. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  7. package/dist/types/browser/react/context.d.ts +14 -0
  8. package/dist/types/browser/rsc-router.d.ts +13 -2
  9. package/dist/types/browser/types.d.ts +10 -0
  10. package/dist/types/build/generate-manifest.d.ts +2 -0
  11. package/dist/types/cache/background-task.d.ts +22 -0
  12. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  13. package/dist/types/cache/cache-policy.d.ts +17 -0
  14. package/dist/types/cache/cache-scope.d.ts +1 -1
  15. package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
  16. package/dist/types/cache/document-cache.d.ts +8 -5
  17. package/dist/types/cache/handle-snapshot.d.ts +23 -0
  18. package/dist/types/cache/index.d.ts +1 -1
  19. package/dist/types/cache/invalidation-order.d.ts +14 -0
  20. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  21. package/dist/types/cache/shell-snapshot.d.ts +10 -1
  22. package/dist/types/cache/store-scope.d.ts +22 -0
  23. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  24. package/dist/types/cache/types.d.ts +63 -14
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
  26. package/dist/types/errors.d.ts +6 -0
  27. package/dist/types/handles/deferred-resolution.d.ts +7 -0
  28. package/dist/types/internal-suspense-audit.d.ts +1 -0
  29. package/dist/types/loader-store.d.ts +4 -8
  30. package/dist/types/outlet-provider.d.ts +2 -1
  31. package/dist/types/prerender/cloudflare.d.ts +49 -0
  32. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  33. package/dist/types/prerender/index.d.ts +12 -0
  34. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  35. package/dist/types/prerender/on-demand.d.ts +272 -0
  36. package/dist/types/prerender/producer-guard.d.ts +33 -0
  37. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  38. package/dist/types/prerender/warm-request.d.ts +65 -0
  39. package/dist/types/prerender/warm.d.ts +65 -0
  40. package/dist/types/prerender/writable-store.d.ts +170 -0
  41. package/dist/types/prerender.d.ts +27 -0
  42. package/dist/types/reverse.d.ts +1 -1
  43. package/dist/types/route-content-wrapper.d.ts +3 -1
  44. package/dist/types/router/error-handling.d.ts +2 -0
  45. package/dist/types/router/handler-context.d.ts +1 -1
  46. package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
  47. package/dist/types/router/pattern-matching.d.ts +2 -0
  48. package/dist/types/router/prefetch-default.d.ts +4 -4
  49. package/dist/types/router/prerender-match.d.ts +23 -1
  50. package/dist/types/router/route-trie-builder.d.ts +8 -1
  51. package/dist/types/router/router-context.d.ts +1 -0
  52. package/dist/types/router/router-interfaces.d.ts +30 -0
  53. package/dist/types/router/router-options.d.ts +24 -0
  54. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
  56. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  57. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  58. package/dist/types/router/trie-matching.d.ts +2 -0
  59. package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
  60. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  61. package/dist/types/rsc/shell-capture.d.ts +14 -1
  62. package/dist/types/rsc/shell-serve.d.ts +2 -2
  63. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  64. package/dist/types/rsc/types.d.ts +8 -0
  65. package/dist/types/segment-boundary-content.d.ts +17 -0
  66. package/dist/types/segment-loader-promise.d.ts +2 -6
  67. package/dist/types/server/build-version-table.d.ts +9 -0
  68. package/dist/types/server/context.d.ts +12 -4
  69. package/dist/types/server/handle-store.d.ts +2 -0
  70. package/dist/types/server/request-context.d.ts +63 -2
  71. package/dist/types/suspense-audit-react.d.ts +26 -0
  72. package/dist/types/suspense-audit.d.ts +157 -0
  73. package/dist/types/testing/action-form.d.ts +34 -0
  74. package/dist/types/testing/flight.entry.d.ts +2 -0
  75. package/dist/types/testing/index.d.ts +4 -0
  76. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  77. package/dist/types/testing/serve-shell-request.d.ts +34 -4
  78. package/dist/types/testing/shell-status.d.ts +14 -6
  79. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  80. package/dist/types/thenable-status.d.ts +6 -0
  81. package/dist/types/types/handler-context.d.ts +7 -0
  82. package/dist/types/types/route-entry.d.ts +5 -0
  83. package/dist/types/urls/path-helper.d.ts +18 -0
  84. package/dist/types/vite/discovery/state.d.ts +11 -0
  85. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  86. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  87. package/dist/vite/index.js +180 -497
  88. package/package.json +13 -3
  89. package/skills/cache-guide/SKILL.md +18 -17
  90. package/skills/caching/SKILL.md +40 -4
  91. package/skills/catalog.json +1 -1
  92. package/skills/cloudflare/SKILL.md +131 -0
  93. package/skills/comparison/references/framework-comparison.md +1 -1
  94. package/skills/deployment-caching/SKILL.md +33 -12
  95. package/skills/document-cache/SKILL.md +16 -14
  96. package/skills/hooks/SKILL.md +22 -0
  97. package/skills/hooks/handle-and-actions.md +3 -4
  98. package/skills/hooks/navigation.md +10 -0
  99. package/skills/hooks/state.md +5 -4
  100. package/skills/host-router/SKILL.md +18 -0
  101. package/skills/loader/SKILL.md +6 -4
  102. package/skills/middleware/SKILL.md +1 -1
  103. package/skills/migrate-nextjs/SKILL.md +82 -9
  104. package/skills/ppr/SKILL.md +31 -8
  105. package/skills/prerender/SKILL.md +645 -8
  106. package/skills/rango/SKILL.md +1 -0
  107. package/skills/response-routes/SKILL.md +5 -5
  108. package/skills/route/SKILL.md +29 -0
  109. package/skills/testing/SKILL.md +2 -1
  110. package/skills/testing/cache-prerender.md +18 -17
  111. package/skills/testing/serve-shell-request.md +104 -8
  112. package/skills/use-cache/SKILL.md +9 -5
  113. package/skills/vercel/SKILL.md +2 -0
  114. package/skills/view-transitions/SKILL.md +9 -4
  115. package/src/browser/event-controller.ts +69 -25
  116. package/src/browser/link-interceptor.ts +2 -2
  117. package/src/browser/navigation-bridge.ts +75 -35
  118. package/src/browser/navigation-store.ts +26 -0
  119. package/src/browser/navigation-transaction.ts +9 -2
  120. package/src/browser/network-error-handler.ts +2 -0
  121. package/src/browser/partial-update.ts +48 -12
  122. package/src/browser/prefetch/default-strategy.ts +22 -42
  123. package/src/browser/react/Link.tsx +37 -42
  124. package/src/browser/react/NavigationProvider.tsx +63 -3
  125. package/src/browser/react/context.ts +17 -0
  126. package/src/browser/react/location-state.ts +5 -19
  127. package/src/browser/react/use-action.ts +4 -1
  128. package/src/browser/react/use-handle.ts +4 -20
  129. package/src/browser/react/use-link-status.ts +4 -3
  130. package/src/browser/react/use-navigation.ts +4 -1
  131. package/src/browser/react/use-params.ts +4 -1
  132. package/src/browser/react/use-pathname.ts +4 -1
  133. package/src/browser/react/use-search-params.ts +5 -3
  134. package/src/browser/react/use-segments.ts +36 -54
  135. package/src/browser/rsc-router.tsx +79 -39
  136. package/src/browser/segment-reconciler.ts +3 -3
  137. package/src/browser/server-action-bridge.ts +26 -6
  138. package/src/browser/types.ts +11 -0
  139. package/src/build/generate-manifest.ts +17 -0
  140. package/src/build/merge-full-manifests.ts +5 -0
  141. package/src/build/route-trie.ts +1 -0
  142. package/src/cache/background-task.ts +61 -0
  143. package/src/cache/cache-key-utils.ts +91 -12
  144. package/src/cache/cache-policy.ts +11 -2
  145. package/src/cache/cache-runtime.ts +23 -23
  146. package/src/cache/cache-scope.ts +38 -36
  147. package/src/cache/cf/cf-cache-store.ts +16 -11
  148. package/src/cache/document-cache.ts +35 -14
  149. package/src/cache/handle-snapshot.ts +50 -0
  150. package/src/cache/index.ts +1 -0
  151. package/src/cache/invalidation-order.ts +53 -0
  152. package/src/cache/memory-segment-store.ts +52 -1
  153. package/src/cache/shell-snapshot.ts +17 -0
  154. package/src/cache/store-scope.ts +36 -0
  155. package/src/cache/tag-invalidation.ts +7 -5
  156. package/src/cache/types.ts +69 -14
  157. package/src/cache/vercel/vercel-cache-store.ts +20 -6
  158. package/src/client.tsx +40 -3
  159. package/src/errors.ts +13 -6
  160. package/src/handles/deferred-resolution.ts +7 -2
  161. package/src/internal-suspense-audit.ts +12 -0
  162. package/src/loader-store.ts +4 -9
  163. package/src/outlet-provider.tsx +25 -1
  164. package/src/prerender/cloudflare.ts +130 -0
  165. package/src/prerender/create-prerender-trigger.ts +780 -0
  166. package/src/prerender/index.ts +39 -0
  167. package/src/prerender/memory-prerender-store.ts +115 -0
  168. package/src/prerender/on-demand.ts +339 -0
  169. package/src/prerender/producer-guard.ts +66 -0
  170. package/src/prerender/shell-manifest-key.ts +12 -17
  171. package/src/prerender/warm-request.ts +154 -0
  172. package/src/prerender/warm.ts +193 -0
  173. package/src/prerender/writable-store.ts +278 -0
  174. package/src/prerender.ts +29 -0
  175. package/src/reverse.ts +1 -1
  176. package/src/route-content-wrapper.tsx +107 -13
  177. package/src/router/error-handling.ts +12 -12
  178. package/src/router/find-match.ts +1 -0
  179. package/src/router/handler-context.ts +2 -0
  180. package/src/router/intercept-resolution.ts +15 -8
  181. package/src/router/loader-resolution.ts +2 -10
  182. package/src/router/match-middleware/background-revalidation.ts +2 -0
  183. package/src/router/match-middleware/cache-lookup.ts +230 -38
  184. package/src/router/pattern-matching.ts +14 -0
  185. package/src/router/prefetch-default.ts +4 -4
  186. package/src/router/prerender-match.ts +460 -352
  187. package/src/router/route-trie-builder.ts +9 -0
  188. package/src/router/router-context.ts +1 -0
  189. package/src/router/router-interfaces.ts +33 -0
  190. package/src/router/router-options.ts +30 -0
  191. package/src/router/segment-resolution/fresh.ts +20 -19
  192. package/src/router/segment-resolution/loader-cache.ts +15 -17
  193. package/src/router/segment-resolution/revalidation.ts +34 -24
  194. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  195. package/src/router/trie-matching.ts +3 -0
  196. package/src/router.ts +107 -1
  197. package/src/rsc/handler.ts +51 -4
  198. package/src/rsc/progressive-enhancement.ts +5 -1
  199. package/src/rsc/response-cache-serve.ts +10 -4
  200. package/src/rsc/rsc-rendering.ts +53 -11
  201. package/src/rsc/shell-build-manifest.ts +10 -8
  202. package/src/rsc/shell-capture-constants.ts +39 -7
  203. package/src/rsc/shell-capture.ts +108 -19
  204. package/src/rsc/shell-serve.ts +12 -6
  205. package/src/rsc/ssr-module-loader.ts +26 -0
  206. package/src/rsc/types.ts +8 -0
  207. package/src/segment-boundary-content.ts +25 -0
  208. package/src/segment-loader-promise.ts +28 -38
  209. package/src/segment-system.tsx +117 -57
  210. package/src/server/build-version-table.ts +15 -0
  211. package/src/server/context.ts +40 -17
  212. package/src/server/cookie-store.ts +5 -0
  213. package/src/server/handle-store.ts +13 -0
  214. package/src/server/loader-registry.ts +6 -1
  215. package/src/server/request-context.ts +87 -1
  216. package/src/ssr/ssr-root.tsx +20 -13
  217. package/src/suspense-audit-react.tsx +98 -0
  218. package/src/suspense-audit.ts +1030 -0
  219. package/src/testing/action-form.ts +66 -0
  220. package/src/testing/flight-runtime.d.ts +27 -0
  221. package/src/testing/flight.entry.ts +3 -0
  222. package/src/testing/index.ts +19 -0
  223. package/src/testing/internal/server-modules.ts +32 -0
  224. package/src/testing/render-route.tsx +25 -12
  225. package/src/testing/serve-shell-request.ts +190 -34
  226. package/src/testing/shell-status.ts +17 -6
  227. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  228. package/src/thenable-status.ts +10 -0
  229. package/src/types/handler-context.ts +7 -0
  230. package/src/types/route-entry.ts +6 -0
  231. package/src/urls/path-helper.ts +69 -9
  232. package/src/use-loader.tsx +57 -30
  233. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  234. package/src/vite/discovery/discover-routers.ts +15 -0
  235. package/src/vite/discovery/shell-prerender-phase.ts +5 -5
  236. package/src/vite/discovery/state.ts +17 -1
  237. package/src/vite/inject-client-debug.ts +42 -20
  238. package/src/vite/plugins/virtual-entries.ts +8 -1
  239. package/src/vite/router-discovery.ts +14 -0
  240. package/src/vite/utils/bundle-analysis.ts +39 -7
  241. package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
  242. package/dist/types/segment-content-promise.d.ts +0 -13
  243. package/src/browser/react/subscribe-to-nothing.ts +0 -7
  244. package/src/segment-content-promise.ts +0 -67
@@ -416,13 +416,82 @@ default) or serves an open-ended param space. See `/prerender` for full API.
416
416
 
417
417
  Next.js route segment config maps onto Rango's explicit primitives:
418
418
 
419
- | Next.js segment config | Rango |
420
- | --------------------------------------------------- | ------------------------------------------------------------ |
421
- | `dynamic = "force-static"` + `generateStaticParams` | `Static()` / `Prerender()` (see `/prerender`) |
422
- | `revalidate = 60` (ISR) | `cache({ ttl: 60, swr: ... })` on the route (see `/caching`) |
423
- | `dynamic = "force-dynamic"` | the default — routes are dynamic unless you cache them |
424
- | `dynamicParams = true` | `Passthrough()` (above) |
425
- | `experimental_ppr = true` | the `ppr` path option (below, and `/ppr`) |
419
+ | Next.js segment config | Rango |
420
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
421
+ | `dynamic = "force-static"` + `generateStaticParams` | `Static()` / `Prerender()` (see `/prerender`) |
422
+ | `revalidate = 60` (ISR) | `cache({ ttl: 60, swr: ... })` on the route (see `/caching`), or on-demand prerender with `onDemand: { ttl: 60 }` (below) |
423
+ | `dynamic = "force-dynamic"` | the default — routes are dynamic unless you cache them |
424
+ | `dynamicParams = true` | `Passthrough()` (above) |
425
+ | `experimental_ppr = true` | the `ppr` path option (below, and `/ppr`) |
426
+
427
+ ### On-demand ISR → `Prerender(..., { onDemand })` + `router.prerender()`
428
+
429
+ A Next.js page with `generateStaticParams` refreshed by `revalidatePath()` or
430
+ `revalidateTag()` from a CMS webhook maps to an on-demand prerender route. The
431
+ route opts in, a store holds refreshed payloads, and the webhook re-renders the
432
+ page instead of invalidating it:
433
+
434
+ ```typescript
435
+ // Next.js:
436
+ export const revalidate = 3600;
437
+ export async function generateStaticParams() {
438
+ return (await getFeatured()).map((p) => ({ id: p.id }));
439
+ }
440
+ // app/api/revalidate/route.ts
441
+ revalidatePath(`/products/${id}`);
442
+ revalidateTag(`product:${id}`);
443
+
444
+ // Rango:
445
+ export const ProductPage = Prerender<{ id: string }>(
446
+ async () => (await getFeatured()).map((p) => ({ id: p.id })),
447
+ async (ctx) => <Product data={await getProduct(ctx.params.id)} />,
448
+ { onDemand: { ttl: 3600, tags: ({ params }) => [`product:${params.id}`] } },
449
+ );
450
+
451
+ // router: createRouter({ prerender: { store, ttl, onRevalidate? } })
452
+ // webhook (route handler, cron or queue):
453
+ const prerender = router.prerender({ env, ctx });
454
+ await prerender(`/products/${id}`); // revalidatePath
455
+ await prerender.markStale([`product:${id}`]); // revalidateTag
456
+ ```
457
+
458
+ | Next.js | Rango |
459
+ | ------------------------------------- | --------------------------------------------------------------------------------------------------- |
460
+ | `revalidatePath(path)` on an ISR page | `await router.prerender({ env, ctx })(path)`: renders now, serves the new payload next request |
461
+ | `revalidateTag(tag)` on ISR pages | `router.prerender({ env, ctx }).markStale([tag])`: marks only; a stale hit schedules `onRevalidate` |
462
+ | `revalidate = N` (time-based ISR) | `onDemand: { ttl: N }` + `onRevalidate`; `ttl` is soft, entries never expire |
463
+ | `dynamicParams = false` | plain on-demand `Prerender`: an unknown param 404s until something refreshes it |
464
+ | `dynamicParams = true` | wrap it in `Passthrough()`: unknown params render live until refreshed |
465
+
466
+ A page whose item was deleted. In the Pages Router, `getStaticProps` returns
467
+ `{ notFound: true }` on a revalidation: "With `notFound: true`, the page will
468
+ return a `404` even if there was a successfully generated page before. This is
469
+ meant to support use cases like user-generated content getting removed by its
470
+ author. Note, `notFound` follows the same `revalidate` behavior described
471
+ here." In Rango a `notFound()` anywhere in the on-demand render (the route
472
+ handler, a layout or slot handler, any server component) does the same on the
473
+ next `prerender(path)`: the result is `{ ok: true, status: "removed" }`, the
474
+ page answers 404 instead of the page or its build-time entry, and the removal
475
+ is rechecked per the route's `ttl`, as Next's is per `revalidate`. When the
476
+ data source may be failing, throw anything else: that keeps the page.
477
+ `prerender.remove(path)` removes without rendering, for a webhook that already
478
+ knows the item is gone, and is permanent until a `prerender(path)` renders the
479
+ page again. See `/prerender` → "Remove a page".
480
+
481
+ Three differences to plan for:
482
+
483
+ - Prerender tags are their own namespace. `updateTag()` / `revalidateTag()`
484
+ from `@rangojs/router` purge the runtime cache and never reach on-demand
485
+ entries; `markStale()` never reaches the runtime cache.
486
+ - A refresh is requestless: `cookies()` / `headers()` in the page make it
487
+ return `skipped-personalized`, and `ctx.env` is the live env you pass.
488
+ - `markStale()` does not re-render: without `onRevalidate` or a cron calling
489
+ `prerender(target, { onlyIfStale: true })` (the runner from
490
+ `router.prerender({ env, ctx })`), a marked page keeps
491
+ serving its old payload.
492
+
493
+ See `/prerender` → "On-demand refresh (ISR)" for the store options, the serve
494
+ table and the rules.
426
495
 
427
496
  ### Partial prerendering → the `ppr` path option
428
497
 
@@ -552,7 +621,9 @@ cookie (`rango-state-fresh`); other users, and other locations on
552
621
  propagates (`/caching` → "The fresh-reads cookie").
553
622
  Built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`, `VercelCacheStore`)
554
623
  index by tag. Next's
555
- `revalidatePath` has no path-based equivalent — tag the relevant entries instead.
624
+ `revalidatePath` has no path-based equivalent for runtime-cached data — tag the
625
+ relevant entries instead. For a prerendered ISR page it is
626
+ `router.prerender({ env, ctx })(path)` (see "On-demand ISR" above).
556
627
 
557
628
  **2. Partial-render selection (which segments re-run after an action).** This is
558
629
  NOT cache invalidation — it is `revalidate()`, controlling which segments
@@ -594,7 +665,9 @@ When migrating:
594
665
 
595
666
  - `revalidateTag(tag)` → `await updateTag(tag)` (in a server action) or
596
667
  `revalidateTag(tag)` (in a route handler / webhook). Effectively 1:1.
597
- - `revalidatePath(path)` → no path-based equivalent; tag the entries on that
668
+ - `revalidatePath(path)` on an ISR page → `router.prerender({ env, ctx })(path)`
669
+ (on-demand prerender, §3); on runtime-cached data, no path-based
670
+ equivalent; tag the entries on that
598
671
  route (`cache({ tags })` / `cacheTag(...)`) and invalidate by tag.
599
672
  - To also force specific segments to re-render after the action (independent of
600
673
  cache busting), attach a `revalidate()` rule at those segment boundaries.
@@ -170,7 +170,7 @@ The partition follows nested scopes (issue #970): a `ppr` route under a
170
170
  inner boundary has none, and by both composed (outermost first, joined by
171
171
  `|`) when it has its own. Each `key()` result is namespaced as `key:` plus
172
172
  its URI encoding (issue #975), exactly as in the record key, so the shell of
173
- `key: () => "tier:gold"` is `<host><path>:shell|key%3Atier%253Agold`. The
173
+ `key: () => "tier:gold"` is `<router>@<host><path>:shell|key%3Atier%253Agold`. The
174
174
  route's record adds the inner boundary's own default key when it has no
175
175
  `key`; the shell partition does not, since the shell key already carries the
176
176
  URL (a store `keyGenerator`'s result is kept when it adds anything, an
@@ -220,10 +220,10 @@ does not render from: otherwise the first variant captured is served for every
220
220
  value of it.
221
221
 
222
222
  In tests, `serveShellRequest` reports the key the serve path resolved
223
- (`result.key`, partition included); `shellCacheKey(url, searchParams,
223
+ (`result.key`, partition included); `shellCacheKey(router, url, searchParams,
224
224
  partition)` builds the same key from the `key()` result, namespacing it as
225
225
  production does. Under nested keyed scopes, pass the `key()` results as an
226
- array, outermost first (`shellCacheKey(url, undefined, ["tier:gold",
226
+ array, outermost first (`shellCacheKey(router, url, undefined, ["tier:gold",
227
227
  "v:a"])`); for a store `keyGenerator` partition pass
228
228
  `{ keys, generated }` (`/testing`, `cache-prerender.md`).
229
229
 
@@ -264,7 +264,7 @@ On a document GET to a ppr route the router runs:
264
264
  DSL `middleware()`; both are guards, and the COMMIT POINT is after all of
265
265
  them: any rejection/redirect/401 wins before a single shell byte, on MISS
266
266
  and on a warmed HIT alike;
267
- 3. **shell lookup** — `getShell(key)` on the app store (key =
267
+ 3. **shell lookup** — `getShell(key)` on the app store (key = router id +
268
268
  host+pathname+sorted search). `CFCacheStore` stores the entry prelude-first
269
269
  and the serve path reads only its head and prelude here, with the tag-marker
270
270
  read running alongside; the capture snapshot is read off the commit path.
@@ -561,6 +561,19 @@ curl -s -D - -o /dev/null https://app.example.com/products/1 | grep -i x-rango-s
561
561
  writes, and marker rejection, with `cf-ray`/colo and read timings. The flag is baked at
562
562
  build time; setting only a Worker runtime variable is too late.
563
563
 
564
+ ### Capturing a shell before the first visitor
565
+
566
+ A shell is captured by the first document MISS for its key, so the first
567
+ visitor after a deploy or an `updateTag()` renders without one. To capture it
568
+ ahead of them, warm the URL: `router.prerender({ env, ctx })(url)` renders it
569
+ through the handler, schedules the capture and waits for it, so the next
570
+ document request is a HIT. A warm replaces a stored shell in place (the old
571
+ one serves until the new one is written) and reports the capture's outcome
572
+ (`result.caches.shell`, with `refusal` when the capture refused). It is not
573
+ subject to the per-key in-flight dedup or the refused-capture backoff, which
574
+ exist to stop a herd of visitors repeating work; every correctness guard on
575
+ this page still decides. See `/prerender` → "Warm any route before traffic".
576
+
564
577
  ### Unit / integration testing (public primitives)
565
578
 
566
579
  Import from `@rangojs/router/testing` (Vitest) or `@rangojs/router/testing/e2e`
@@ -571,17 +584,18 @@ Import from `@rangojs/router/testing` (Vitest) or `@rangojs/router/testing/e2e`
571
584
  | `assertShellStatus(res, "HIT" \| "MISS")` | Document Response from a real RSC serve / e2e `page.request` |
572
585
  | `assertPprReplayStatus(res, expected)` | Partial response fresh/stale replay or bounded bypass |
573
586
  | `parsePprReplayStatus(res)` | Read structured replay/bypass status or null |
574
- | `shellCacheKey(url)` | Production store key for `store.getShell` / custom stores |
587
+ | `shellCacheKey(router, url)` | Production store key for `store.getShell` / custom stores |
575
588
  | `MemorySegmentCacheStore` + `getShell`/`putShell` | Custom store contract / tag eviction (no faked HIT) |
576
589
  | `serveShellRequest(router, url, opts)` | A real MISS → capture → HIT (react-server Vitest) |
577
590
 
578
591
  ```ts
579
592
  import { MemorySegmentCacheStore } from "@rangojs/router/cache";
580
593
  import { assertShellStatus, shellCacheKey } from "@rangojs/router/testing";
594
+ import { router } from "./router";
581
595
 
582
596
  // Unit: store + key (after a real putShell / capture flush in e2e)
583
597
  const store = new MemorySegmentCacheStore();
584
- const key = shellCacheKey("http://localhost/products/1");
598
+ const key = shellCacheKey(router, "http://localhost/products/1");
585
599
  // ... after production putShell ...
586
600
  expect(await store.getShell(key)).not.toBeNull();
587
601
 
@@ -788,6 +802,9 @@ identifiers — served to anonymous visitors.)
788
802
  - `ctx.use(H)(promise)` from a handler — a TOP-LEVEL pushed promise is awaited
789
803
  at capture and BAKED into the shell (bounded by `ppr.captureTimeout`, 15s by
790
804
  default).
805
+ - A promise pushed from a LOADER is never in the shell and the capture does not
806
+ wait for it: however slow it is (or if it never settles), the shell is still
807
+ stored, and the HIT's own loader run delivers the value after hydration.
791
808
  - `ctx.use(H)({ x: promise })` from a HANDLER — also baked. The capture waits
792
809
  for every promise nested in a handler's push, at any depth of plain objects,
793
810
  arrays and JSX props, before it records the handles every HIT replays. Keep
@@ -930,6 +947,11 @@ pin: a snapshot captured by a navigation alone carries none, and a shell over
930
947
  `ppr.maxSnapshotBytes` drops them. There the loader runs fresh, and its
931
948
  pushes are that run's, like its data.
932
949
 
950
+ A `Prerender` + `ppr` page follows the same table. Its handler layer comes
951
+ from the prerendered payload, which never holds a loader push (loaders do not
952
+ run at build), so the shell entry records the loader pushes its HTML rendered
953
+ on its own, and a HIT restores them as above (#1057).
954
+
933
955
  A dependency the route does not register, which an `ssr: false` loader
934
956
  awaited at capture outside any live loader, is on neither lane: its settled
935
957
  pushes stay as the prelude rendered them, even where a live loader also
@@ -1347,8 +1369,9 @@ shrink what the shell bakes or move large regions under a live loader's
1347
1369
  boundary.
1348
1370
 
1349
1371
  The shell store is always the app-level `createRouter({ cache })` store; the
1350
- default key is `${host}${pathname}${sortedSearch}:shell` (host-scoped so
1351
- multi-tenant shells never collide).
1372
+ default key is `${routerId}@${host}${pathname}${sortedSearch}:shell`: host-scoped
1373
+ so multi-tenant shells never collide, and router-scoped so two routers on one
1374
+ store keep their own (see `/host-router`, "Shared cache store").
1352
1375
 
1353
1376
  These options control the in-function shell entry only. They do not emit HTTP
1354
1377
  `Cache-Control`. Adding `s-maxage` separately allows a platform CDN to cache the