@rangojs/router 0.20.0 → 0.22.0
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.
- package/dist/bin/rango.js +39 -15
- package/dist/types/browser/event-controller.d.ts +49 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/navigation-transaction.d.ts +7 -0
- package/dist/types/browser/partial-update.d.ts +2 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
- package/dist/types/browser/rango-state.d.ts +3 -1
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +25 -0
- package/dist/types/browser/react/location-state-shared.d.ts +84 -10
- package/dist/types/browser/react/location-state.d.ts +7 -2
- package/dist/types/browser/rsc-router.d.ts +13 -2
- package/dist/types/browser/types.d.ts +10 -0
- package/dist/types/build/generate-manifest.d.ts +2 -0
- package/dist/types/cache/background-task.d.ts +22 -0
- package/dist/types/cache/cache-key-utils.d.ts +50 -5
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +5 -6
- package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +59 -50
- package/dist/types/cache/index.d.ts +3 -1
- package/dist/types/cache/invalidation-order.d.ts +14 -0
- package/dist/types/cache/memory-segment-store.d.ts +33 -1
- package/dist/types/cache/shell-snapshot.d.ts +29 -4
- package/dist/types/cache/store-scope.d.ts +22 -0
- package/dist/types/cache/tag-invalidation.d.ts +7 -6
- package/dist/types/cache/types.d.ts +84 -24
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +18 -1
- package/dist/types/internal-suspense-audit.d.ts +1 -0
- package/dist/types/loader-store.d.ts +4 -8
- package/dist/types/outlet-provider.d.ts +2 -1
- package/dist/types/prerender/build-shell-capture.d.ts +8 -6
- package/dist/types/prerender/cloudflare.d.ts +49 -0
- package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
- package/dist/types/prerender/index.d.ts +12 -0
- package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
- package/dist/types/prerender/on-demand.d.ts +272 -0
- package/dist/types/prerender/producer-guard.d.ts +33 -0
- package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
- package/dist/types/prerender/warm-request.d.ts +65 -0
- package/dist/types/prerender/warm.d.ts +65 -0
- package/dist/types/prerender/writable-store.d.ts +170 -0
- package/dist/types/prerender.d.ts +27 -0
- package/dist/types/reverse.d.ts +1 -1
- package/dist/types/route-content-wrapper.d.ts +3 -1
- package/dist/types/router/error-handling.d.ts +2 -0
- package/dist/types/router/handler-context.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- package/dist/types/router/pattern-matching.d.ts +2 -0
- package/dist/types/router/prefetch-default.d.ts +4 -4
- package/dist/types/router/prerender-match.d.ts +23 -1
- package/dist/types/router/route-trie-builder.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +1 -0
- package/dist/types/router/router-interfaces.d.ts +32 -1
- package/dist/types/router/router-options.d.ts +39 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
- package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
- package/dist/types/router/trie-matching.d.ts +2 -0
- package/dist/types/router-versions.d.ts +39 -0
- package/dist/types/rsc/handler-context.d.ts +5 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
- package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
- package/dist/types/rsc/shell-capture.d.ts +14 -1
- package/dist/types/rsc/shell-serve.d.ts +2 -2
- package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
- package/dist/types/rsc/types.d.ts +12 -6
- package/dist/types/segment-boundary-content.d.ts +17 -0
- package/dist/types/segment-loader-promise.d.ts +2 -6
- package/dist/types/server/build-version-table.d.ts +53 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +89 -46
- package/dist/types/server/request-context.d.ts +84 -9
- package/dist/types/suspense-audit-react.d.ts +26 -0
- package/dist/types/suspense-audit.d.ts +157 -0
- package/dist/types/testing/action-form.d.ts +34 -0
- package/dist/types/testing/build-versions.d.ts +32 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +3 -1
- package/dist/types/testing/index.d.ts +6 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/location-state-key.d.ts +5 -1
- package/dist/types/testing/render-route.d.ts +77 -6
- package/dist/types/testing/serve-shell-request.d.ts +55 -4
- package/dist/types/testing/shell-status.d.ts +14 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +15 -5
- package/dist/types/types/route-entry.d.ts +5 -0
- package/dist/types/urls/path-helper.d.ts +18 -0
- package/dist/types/vite/discovery/build-versions.d.ts +393 -0
- package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
- package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
- package/dist/types/vite/discovery/state.d.ts +25 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/plugin-types.d.ts +27 -0
- package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
- package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/types/vite/utils/client-chunks.d.ts +2 -0
- package/dist/types/vite/utils/compare-strings.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +8 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
- package/dist/vite/index.js +1707 -931
- package/package.json +12 -3
- package/skills/cache-guide/SKILL.md +102 -37
- package/skills/caching/SKILL.md +56 -9
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +133 -1
- package/skills/comparison/references/framework-comparison.md +17 -13
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +25 -0
- package/skills/hooks/handle-and-actions.md +5 -0
- package/skills/hooks/navigation.md +38 -0
- package/skills/hooks/state.md +213 -11
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +31 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +25 -5
- package/skills/middleware/SKILL.md +2 -0
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +150 -48
- package/skills/prerender/SKILL.md +651 -10
- package/skills/rango/SKILL.md +9 -7
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/router-setup/SKILL.md +15 -1
- package/skills/testing/SKILL.md +32 -27
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/client-components.md +193 -8
- package/skills/testing/handles.md +2 -0
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +18 -18
- package/skills/testing/serve-shell-request.md +167 -22
- package/skills/testing/server-actions.md +17 -17
- package/skills/testing/server-tree.md +1 -1
- package/skills/typesafety/generated-files-and-cli.md +1 -1
- package/skills/use-cache/SKILL.md +25 -10
- package/skills/vercel/SKILL.md +5 -3
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +119 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +104 -41
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +24 -12
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +58 -14
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +37 -43
- package/src/browser/react/NavigationProvider.tsx +85 -1
- package/src/browser/react/context.ts +30 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +28 -77
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +3 -0
- package/src/browser/react/use-link-status.ts +4 -3
- package/src/browser/react/use-navigation.ts +4 -1
- package/src/browser/react/use-params.ts +4 -1
- package/src/browser/react/use-pathname.ts +4 -1
- package/src/browser/react/use-search-params.ts +5 -3
- package/src/browser/react/use-segments.ts +36 -54
- package/src/browser/rsc-router.tsx +93 -31
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +43 -7
- package/src/browser/types.ts +11 -0
- package/src/build/generate-manifest.ts +17 -0
- package/src/build/merge-full-manifests.ts +5 -0
- package/src/build/route-trie.ts +1 -0
- package/src/cache/background-task.ts +61 -0
- package/src/cache/cache-key-utils.ts +91 -12
- package/src/cache/cache-policy.ts +11 -2
- package/src/cache/cache-runtime.ts +23 -23
- package/src/cache/cache-scope.ts +48 -36
- package/src/cache/cf/cf-cache-store.ts +91 -66
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +118 -86
- package/src/cache/index.ts +5 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +52 -5
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +90 -24
- package/src/cache/vercel/vercel-cache-store.ts +68 -26
- package/src/client-urls/client-root.tsx +1 -0
- package/src/client-urls/navigation.ts +10 -0
- package/src/client-urls/optimistic-location.ts +10 -4
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +36 -0
- package/src/internal-suspense-audit.ts +12 -0
- package/src/loader-store.ts +4 -9
- package/src/outlet-provider.tsx +25 -1
- package/src/prerender/build-shell-capture.ts +11 -9
- package/src/prerender/cloudflare.ts +130 -0
- package/src/prerender/create-prerender-trigger.ts +780 -0
- package/src/prerender/index.ts +39 -0
- package/src/prerender/memory-prerender-store.ts +115 -0
- package/src/prerender/on-demand.ts +339 -0
- package/src/prerender/producer-guard.ts +66 -0
- package/src/prerender/shell-manifest-key.ts +12 -17
- package/src/prerender/warm-request.ts +154 -0
- package/src/prerender/warm.ts +193 -0
- package/src/prerender/writable-store.ts +278 -0
- package/src/prerender.ts +29 -0
- package/src/reverse.ts +1 -1
- package/src/route-content-wrapper.tsx +107 -13
- package/src/router/error-handling.ts +12 -12
- package/src/router/find-match.ts +1 -0
- package/src/router/handler-context.ts +2 -0
- package/src/router/intercept-resolution.ts +15 -8
- package/src/router/loader-resolution.ts +2 -10
- package/src/router/manifest.ts +2 -0
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +239 -54
- package/src/router/navigation-snapshot.ts +25 -6
- package/src/router/pattern-matching.ts +14 -0
- package/src/router/prefetch-default.ts +4 -4
- package/src/router/prerender-match.ts +460 -352
- package/src/router/request-classification.ts +15 -12
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +35 -1
- package/src/router/router-options.ts +45 -5
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +110 -77
- package/src/router/segment-resolution/revalidation.ts +34 -24
- package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
- package/src/router/trie-matching.ts +3 -0
- package/src/router-versions.ts +60 -0
- package/src/router.ts +107 -1
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +64 -7
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +65 -12
- package/src/rsc/shell-build-manifest.ts +17 -12
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +144 -45
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +12 -6
- package/src/segment-boundary-content.ts +25 -0
- package/src/segment-loader-promise.ts +28 -38
- package/src/segment-system.tsx +117 -57
- package/src/server/build-version-table.ts +130 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +357 -127
- package/src/server/request-context.ts +149 -12
- package/src/ssr/ssr-root.tsx +32 -13
- package/src/suspense-audit-react.tsx +98 -0
- package/src/suspense-audit.ts +1030 -0
- package/src/testing/action-form.ts +66 -0
- package/src/testing/build-versions.ts +64 -0
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/dom.entry.ts +1 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +4 -0
- package/src/testing/index.ts +22 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +293 -46
- package/src/testing/serve-shell-request.ts +262 -37
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +15 -5
- package/src/types/route-entry.ts +6 -0
- package/src/urls/path-helper.ts +69 -9
- package/src/use-loader.tsx +57 -30
- package/src/vite/discovery/build-versions.ts +1281 -0
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/installed-packages.ts +159 -0
- package/src/vite/discovery/prerender-collection.ts +38 -1
- package/src/vite/discovery/router-versions-phase.ts +336 -0
- package/src/vite/discovery/shell-prerender-phase.ts +33 -43
- package/src/vite/discovery/state.ts +36 -1
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugin-types.ts +28 -0
- package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
- package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
- package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
- package/src/vite/plugins/expose-internal-ids.ts +12 -3
- package/src/vite/plugins/stable-rsc-output.ts +147 -0
- package/src/vite/plugins/version-plugin.ts +19 -24
- package/src/vite/plugins/version.d.ts +17 -2
- package/src/vite/plugins/virtual-entries.ts +39 -6
- package/src/vite/rango.ts +28 -3
- package/src/vite/router-discovery.ts +96 -13
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/src/vite/utils/client-chunks.ts +5 -1
- package/src/vite/utils/compare-strings.ts +9 -0
- package/src/vite/utils/package-resolution.ts +9 -0
- package/src/vite/utils/prerender-utils.ts +4 -1
- package/src/vite/utils/shared-utils.ts +1 -1
- package/dist/types/segment-content-promise.d.ts +0 -13
- package/src/segment-content-promise.ts +0 -67
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prerender
|
|
3
|
-
description: Pre-render route segments at build time with Prerender (with getParams), Passthrough live fallback, Static segments, and Skip. Use when a page's content is mostly static and shouldn't render on every request, speeding up cold responses, reading build-only data (files, build env), or deciding which routes to prerender vs render live.
|
|
3
|
+
description: Pre-render route segments at build time with Prerender (with getParams), Passthrough live fallback, Static segments, and Skip, refresh them from a running app with on-demand prerender (Prerender onDemand + router.prerender, ISR-style), and warm any other route's runtime caches (ppr shell, cache(), "use cache", document cache) before traffic with the same router.prerender. Use when a page's content is mostly static and shouldn't render on every request, speeding up cold responses, reading build-only data (files, build env), refreshing one page from a webhook, cron or queue without a redeploy, removing a deleted item's page (prerender.remove, or notFound() in a refresh), making routes ready after a deploy or an updateTag so no visitor pays the first render, or deciding which routes to prerender vs render live.
|
|
4
4
|
argument-hint: [passthrough]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -354,13 +354,27 @@ string (after `cache.searchParams` filtering). A URL with search params has its
|
|
|
354
354
|
own shell identity and is captured at runtime. The runtime shell store is read
|
|
355
355
|
first, so a runtime capture supersedes the baked entry once it exists.
|
|
356
356
|
|
|
357
|
+
Handle values behave as on any `ppr` shell (`/ppr` → "Handles on a shell
|
|
358
|
+
HIT"): a value an `ssr: false` loader pushes settled is in the shell's HTML and
|
|
359
|
+
in the data a HIT hydrates with, and one it pushes as a promise arrives after
|
|
360
|
+
hydration. The prerendered payload holds only the handler's pushes (loaders
|
|
361
|
+
never run at build), so the shell entry keeps the loader pushes its HTML
|
|
362
|
+
rendered, on a build-baked shell and on one captured at runtime alike. A shell
|
|
363
|
+
stored by router 0.21 or earlier lacks them, and a promise-free `ssr: false`
|
|
364
|
+
loader's settled push fails hydration on its HITs until the shell is replaced
|
|
365
|
+
(#1057).
|
|
366
|
+
|
|
357
367
|
### Freshness of a build shell
|
|
358
368
|
|
|
359
369
|
A build-baked shell is not on a wall clock the way a pure runtime `ppr` shell is.
|
|
360
370
|
It serves from the first request after a deploy and keeps serving until one of:
|
|
361
371
|
|
|
362
|
-
- **a redeploy** — the
|
|
363
|
-
|
|
372
|
+
- **a redeploy** — the new build ships its own build shell manifest and
|
|
373
|
+
re-bakes. A shell stored at runtime for the route is replaced when the
|
|
374
|
+
router's document version changes, and a change of the prerendered payload
|
|
375
|
+
changes it (a Prerender payload is part of its router's document version, not
|
|
376
|
+
its data version, so cached data is kept); a React-version bump also retires
|
|
377
|
+
it;
|
|
364
378
|
- **`updateTag`** on a tag the shell carries — drops it, so the next request
|
|
365
379
|
MISSes and a runtime capture takes over.
|
|
366
380
|
|
|
@@ -582,6 +596,10 @@ export const BlogPost = Passthrough(BlogPostDef, async (ctx) => {
|
|
|
582
596
|
- `ctx.passthrough()` on a route not wrapped with `Passthrough()` throws.
|
|
583
597
|
- `ctx.passthrough()` at runtime (`ctx.build === false`) also throws.
|
|
584
598
|
It is a build-time-only control flow.
|
|
599
|
+
- On an on-demand route, a refresh (`router.prerender()`) whose build handler
|
|
600
|
+
returns `ctx.passthrough()` reports `skipped-passthrough` and stores a
|
|
601
|
+
"removed" marker for that param, so the live handler answers there too, not
|
|
602
|
+
a page a refresh stored or the build baked earlier (see "Remove a page").
|
|
585
603
|
- `getParams()` still enumerates the param set; the build handler decides
|
|
586
604
|
per-param whether to produce an artifact or defer to the live handler.
|
|
587
605
|
|
|
@@ -664,7 +682,9 @@ and are revalidated by actions as usual.
|
|
|
664
682
|
|
|
665
683
|
- **Plain `Prerender`**: the handler was evicted, so the action re-render serves
|
|
666
684
|
the stored prerender entry again. The prerendered output stays frozen; the
|
|
667
|
-
action's return value reaches the client as usual.
|
|
685
|
+
action's return value reaches the client as usual. An `onDemand` route serves
|
|
686
|
+
its on-demand entry first, and re-sends its own segments from it, so an
|
|
687
|
+
action that calls `router.prerender()` for the page shows the new entry.
|
|
668
688
|
- **`Passthrough()`**: the action re-render runs the live handler, replacing the
|
|
669
689
|
prerendered output. To keep the frozen tree mounted after actions (for example
|
|
670
690
|
so a client component's form state survives), suppress action
|
|
@@ -695,18 +715,635 @@ emitted — wrap the route in `Passthrough()` when its segments must re-render.
|
|
|
695
715
|
### Tag invalidation does not reach prerendered segments
|
|
696
716
|
|
|
697
717
|
`updateTag()`/`revalidateTag()` evict runtime cache entries. They do not refresh
|
|
698
|
-
a prerendered route's build-time
|
|
699
|
-
system is consulted, and a `cacheTag()` inside a
|
|
700
|
-
during the build has no runtime effect. A
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
718
|
+
a prerendered route's payload, build-time or on-demand: the prerender store is
|
|
719
|
+
read before the tag system is consulted, and a `cacheTag()` inside a
|
|
720
|
+
`"use cache"` function that runs during the build has no runtime effect. A
|
|
721
|
+
redeploy replaces the build payload; because a Prerender payload is part of its
|
|
722
|
+
router's document version, a deploy whose only change is new prerendered content
|
|
723
|
+
keeps cached data and replaces stored HTML (shells, document-cache responses)
|
|
724
|
+
for that router.
|
|
725
|
+
|
|
726
|
+
To refresh a prerendered page without a redeploy, opt the route into
|
|
727
|
+
**on-demand prerender** and call `router.prerender()` (next section); its tags
|
|
728
|
+
are a separate namespace, invalidated with the runner's `markStale()`. To
|
|
729
|
+
make a route invalidatable by `updateTag()`, serve it from the runtime cache
|
|
730
|
+
instead (`cache()`, or a `Passthrough()` live handler). A build-baked **ppr
|
|
731
|
+
shell** is different: a tag it carries does drop it (see "Freshness of a build
|
|
732
|
+
shell").
|
|
733
|
+
|
|
734
|
+
## On-demand refresh (ISR)
|
|
735
|
+
|
|
736
|
+
Build-time prerender freezes a page until the next deploy. On-demand prerender
|
|
737
|
+
lets a running app re-render one page and serve that to everyone: a CMS
|
|
738
|
+
webhook, a cron sweep, a queue consumer, or a server action calls
|
|
739
|
+
`router.prerender()`, the router renders the route with route params and env
|
|
740
|
+
only (no request), and writes the payload to a durable store that the serve
|
|
741
|
+
path reads before the build manifest.
|
|
742
|
+
|
|
743
|
+
### Opt a route in
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
export const ProductPage = Prerender(
|
|
747
|
+
async () => [{ id: "featured" }],
|
|
748
|
+
async (ctx) => <Product data={await ctx.env.PRODUCTS.get(ctx.params.id)} />,
|
|
749
|
+
{
|
|
750
|
+
onDemand: {
|
|
751
|
+
ttl: 3600, // soft staleness, seconds; overrides the router `ttl`
|
|
752
|
+
tags: ({ params }) => [`product:${params.id}`], // or a string[]
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
);
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
`onDemand: true` uses the router defaults. Any truthy spelling works (a literal,
|
|
759
|
+
a spread, an imported const). The opt-in keeps the handler in the production
|
|
760
|
+
bundle, which plain `Prerender` evicts. `Passthrough()` alone is not an opt-in:
|
|
761
|
+
wrap an `onDemand` definition to get both a live fallback and refreshes.
|
|
762
|
+
|
|
763
|
+
`onDemand` and the `ppr` path option cannot be combined: the route definition
|
|
764
|
+
throws, because a refresh cannot replace a captured document shell atomically.
|
|
765
|
+
|
|
766
|
+
### Configure a store
|
|
767
|
+
|
|
768
|
+
```typescript
|
|
769
|
+
import { createKVPrerenderStore } from "@rangojs/router/prerender/cloudflare";
|
|
770
|
+
|
|
771
|
+
export const router = createRouter<Env>({
|
|
772
|
+
prerender: (env) => ({
|
|
773
|
+
store: createKVPrerenderStore(env.PRERENDER_KV),
|
|
774
|
+
ttl: 3600, // default soft TTL; absent = never stale
|
|
775
|
+
// Optional. Its presence turns on stale-while-revalidate scheduling.
|
|
776
|
+
onRevalidate: (target, liveEnv) => liveEnv.PRERENDER_QUEUE.send({ target }),
|
|
777
|
+
}),
|
|
778
|
+
});
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
The option takes an object or a factory resolved per request and per trigger
|
|
782
|
+
call, like `cache`. `createMemoryPrerenderStore()` from
|
|
783
|
+
`@rangojs/router/prerender` is a single-process store (Node, tests). In dev a
|
|
784
|
+
zero-config in-memory store is used when the option is absent.
|
|
785
|
+
|
|
786
|
+
### Trigger a refresh
|
|
787
|
+
|
|
788
|
+
```typescript
|
|
789
|
+
const prerender = router.prerender({ env, ctx });
|
|
790
|
+
await prerender("/products/42");
|
|
791
|
+
await prerender(
|
|
792
|
+
{ route: "products.detail", params: { id: "42" } },
|
|
793
|
+
{ throwOnError: true },
|
|
794
|
+
);
|
|
795
|
+
await prerender.many(targets, { concurrency: 4, onlyIfStale: true });
|
|
796
|
+
await prerender.markStale(["product:42"]);
|
|
797
|
+
await prerender.remove("/products/42"); // see "Remove a page"
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
`router.prerender({ env, ctx })` binds the live env (and the Cloudflare `ctx`,
|
|
801
|
+
absent on Node) once and returns the runner synchronously; binding does no
|
|
802
|
+
work. Per-call options (`onlyIfStale`, `throwOnError`, and `concurrency` on
|
|
803
|
+
`.many()`) go on each call.
|
|
804
|
+
|
|
805
|
+
`.many()` runs targets through a bounded pool and returns one result per target
|
|
806
|
+
in input order. `concurrency` defaults to 1 (any invalid value is 1). A large
|
|
807
|
+
list belongs in a queue, one message per batch: one invocation runs under the
|
|
808
|
+
platform's time and CPU limits, so a single `.many()` over thousands of targets
|
|
809
|
+
will not finish.
|
|
810
|
+
|
|
811
|
+
A refresh always renders and replaces; `{ onlyIfStale: true }` (cron sweeps)
|
|
812
|
+
skips a fresh entry and returns `already-fresh`. The result is inspectable
|
|
813
|
+
(`{ ok, path: "on-demand", status, key, tags, ttl }`); statuses are `rendered`,
|
|
814
|
+
`already-fresh`, `removed` (the render hit `notFound()`, see "Remove a
|
|
815
|
+
page"), `no-match`, `no-store`, `skipped-personalized`,
|
|
816
|
+
`skipped-unsupported-target` (a target with `?search` or `#hash`),
|
|
817
|
+
`skipped-passthrough`, `render-failed` and `store-failed`. Every failure keeps
|
|
818
|
+
the previous entry, except `skipped-passthrough`, which hands the page to the
|
|
819
|
+
live handler. A `markStale()` that lands while a refresh renders marks the entry that
|
|
820
|
+
refresh stores.
|
|
821
|
+
|
|
822
|
+
A route that is not on-demand is not refused: the same call warms its runtime
|
|
823
|
+
caches instead (see "Warm any route before traffic"). An on-demand route gets
|
|
824
|
+
both: after the store write, the runner sends one warm request so the route's
|
|
825
|
+
loaders' own `cache()` and the document cache are rebuilt on the new entry.
|
|
826
|
+
That request needs a shared app cache store and an origin; without them the
|
|
827
|
+
refresh is complete as it is, and the result has no `caches`.
|
|
828
|
+
|
|
829
|
+
### Remove a page
|
|
830
|
+
|
|
831
|
+
A deleted product's page has to stop serving, and for a param the build baked,
|
|
832
|
+
the older build-time page must not come back in its place. So a removal does
|
|
833
|
+
not delete the entry: it stores a "removed" marker in the prerender store in
|
|
834
|
+
place of the page. There are two ways to write it.
|
|
835
|
+
|
|
836
|
+
```typescript
|
|
837
|
+
// 1. Explicitly, for example from a "product deleted" webhook. Nothing is
|
|
838
|
+
// rendered, so it does not wait for the data source to catch up.
|
|
839
|
+
const prerender = router.prerender({ env, ctx });
|
|
840
|
+
await prerender.remove("/products/42");
|
|
841
|
+
await prerender.remove.many(paths, { concurrency: 4 });
|
|
842
|
+
|
|
843
|
+
// 2. By refreshing: a render that hits notFound() removes its own page.
|
|
844
|
+
export const ProductPage = Prerender(
|
|
845
|
+
async () => [{ id: "featured" }],
|
|
846
|
+
async (ctx) => {
|
|
847
|
+
const product = await ctx.env.PRODUCTS.get(ctx.params.id);
|
|
848
|
+
if (!product) notFound();
|
|
849
|
+
return <Product data={product} />;
|
|
850
|
+
},
|
|
851
|
+
{ onDemand: { ttl: 3600, tags: ({ params }) => [`product:${params.id}`] } },
|
|
852
|
+
);
|
|
853
|
+
await prerender("/products/42"); // { ok: true, status: "removed", ... }
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
Both return `{ ok: true, path: "on-demand", status: "removed", key, tags }`.
|
|
857
|
+
The result is `ok: true`: a removal worked, so count `removed` in what a sweep
|
|
858
|
+
reports, next to `rendered` and `already-fresh`. From the next request on, the
|
|
859
|
+
route answers 404 for that param, in dev and in production; a `Passthrough`
|
|
860
|
+
route runs its live handler. A page the build baked is covered too: its
|
|
861
|
+
build-time entry is not served.
|
|
862
|
+
|
|
863
|
+
The two differ in how long they last:
|
|
864
|
+
|
|
865
|
+
| | `prerender.remove(url)` | a refresh that hits `notFound()` |
|
|
866
|
+
| ------------------------ | ------------------------------------------ | ---------------------------------------------------------------------------------- |
|
|
867
|
+
| renders | no | yes |
|
|
868
|
+
| the marker | permanent: no `ttl`, no tags | has the route's `ttl` and `tags`, like the page it replaces |
|
|
869
|
+
| `onRevalidate` | never scheduled | scheduled by a request once the marker is stale; the 404 keeps answering meanwhile |
|
|
870
|
+
| `{ onlyIfStale: true }` | leaves it and returns `removed` | leaves it while fresh (`removed`); renders again once stale |
|
|
871
|
+
| `markStale(tags)` | never reaches it | marks it stale through the route's tags, so it is rechecked |
|
|
872
|
+
| the page comes back when | a refresh without `onlyIfStale` renders it | the next recheck renders it, or any refresh does |
|
|
873
|
+
|
|
874
|
+
- **A `notFound()` anywhere in the render removes the page**: in the route
|
|
875
|
+
handler, a layout handler, a parallel slot handler, or any server component
|
|
876
|
+
in the tree, sync or async. It removes it only when nothing else went wrong.
|
|
877
|
+
When the data source may be failing, throw anything else: that is
|
|
878
|
+
`render-failed` and keeps the page. A render that hits a `notFound()` and
|
|
879
|
+
another error is `render-failed` too, whichever came first.
|
|
880
|
+
- **A `notFound()` removal is rechecked; `remove()` is permanent.** A data
|
|
881
|
+
source that loses an item for a moment must not remove its page until the
|
|
882
|
+
next deploy, so the marker of a refresh goes stale by the route's `ttl`
|
|
883
|
+
(else the router's `prerender.ttl`; with neither it never does) and is
|
|
884
|
+
rendered again by `onRevalidate` or a sweep, like a stale page. `remove()` is
|
|
885
|
+
a decision, not an observation: its marker stays until a
|
|
886
|
+
`prerender(url)` without `onlyIfStale` renders the page.
|
|
887
|
+
- **`ctx.passthrough()` in a refresh stores the marker too.** A `Passthrough`
|
|
888
|
+
route whose build handler declines a param gets `skipped-passthrough`, and
|
|
889
|
+
the live handler answers from then on, not a page stored or baked earlier.
|
|
890
|
+
The marker is stamped and rechecked like a `notFound()` one.
|
|
891
|
+
- **`remove()` is for on-demand routes.** On any other route it returns
|
|
892
|
+
`skipped-not-on-demand` and never warms. `no-match`, `no-store`,
|
|
893
|
+
`skipped-unsupported-target` and `store-failed` are as for a refresh, and
|
|
894
|
+
`throwOnError` works the same. `remove.many()` takes `concurrency` and
|
|
895
|
+
`throwOnError`, and returns one result per target in input order.
|
|
896
|
+
- **Last write wins.** `remove()` and a refresh of the same page are two
|
|
897
|
+
writes to one key. A job queued before a removal (an `onRevalidate` for the
|
|
898
|
+
page when it was stale) that runs a plain refresh after it brings the page
|
|
899
|
+
back: pass `{ onlyIfStale: true }` in `onRevalidate` and queue consumers, so
|
|
900
|
+
they find the marker and render nothing. A marker that lands while a refresh
|
|
901
|
+
is still rendering is not overwritten by that render's page; that narrows
|
|
902
|
+
the race and cannot close it (the check and the write are two store calls,
|
|
903
|
+
and KV is eventually consistent).
|
|
904
|
+
- **The marker does not reach the runtime caches around the route**, and no
|
|
905
|
+
warm request follows a removal. A document cache
|
|
906
|
+
(`createDocumentCacheMiddleware`) answers ahead of the router, so it keeps
|
|
907
|
+
serving the document it stored until that entry's `s-maxage` and
|
|
908
|
+
`stale-while-revalidate` run out. A `Passthrough` route's live handler serves
|
|
909
|
+
through its own `cache()` as it does for a page that was never refreshed.
|
|
910
|
+
Remove first, then invalidate:
|
|
911
|
+
|
|
912
|
+
```typescript
|
|
913
|
+
path.json("/hooks/product-deleted", async (ctx) => {
|
|
914
|
+
const { id } = (await ctx.request.json()) as { id: string };
|
|
915
|
+
const result = await router
|
|
916
|
+
.prerender({ env: ctx.env, ctx: ctx.executionContext })
|
|
917
|
+
.remove(`/products/${id}`);
|
|
918
|
+
await updateTag(`product:${id}`); // then drop the cached document
|
|
919
|
+
return { status: result.status };
|
|
920
|
+
});
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
The order matters. With the invalidation first, a visitor between the two
|
|
924
|
+
calls misses the document cache, is served the page the store still holds,
|
|
925
|
+
and the document cache stores it again; the removal then cannot reach it.
|
|
926
|
+
With the removal first, that visitor gets the document that is still
|
|
927
|
+
cached, or, past the cache, the marker's 404, which is never stored: nothing
|
|
928
|
+
can put the page back. `updateTag()` needs a request context: a webhook
|
|
929
|
+
route has one, a
|
|
930
|
+
queue or cron handler does not, and there the stored document serves until
|
|
931
|
+
it expires. The cached document carries the tags its own request recorded
|
|
932
|
+
(for example a `cacheTag()` call in middleware), not the route's
|
|
933
|
+
`onDemand.tags`, which are prerender tags. A plain on-demand route's own
|
|
934
|
+
`cache()` and its loaders' `cache()` need nothing: the request is a 404
|
|
935
|
+
before any of them is shown.
|
|
936
|
+
|
|
937
|
+
- **A store that cannot be read is a miss**, for a marker as for a page: while
|
|
938
|
+
the prerender store is failing, the build-time entry of a removed page can
|
|
939
|
+
serve again.
|
|
940
|
+
- **The marker is per data version**, like every entry: a deploy that leaves
|
|
941
|
+
the router's server code unchanged keeps it. After one that changes it the
|
|
942
|
+
build decides again: a param `getParams()` still lists is baked and served.
|
|
943
|
+
In dev every edit to a server module is such a change, so a marker (like a
|
|
944
|
+
refreshed page) is gone after one.
|
|
945
|
+
- **Intercepted navigations do not see it.** They do not read the prerender
|
|
946
|
+
store (see the rules below), so the build's intercept variant of a removed
|
|
947
|
+
page still serves.
|
|
948
|
+
|
|
949
|
+
The `notFound()` half is the Next.js Pages Router's rule: "With
|
|
950
|
+
`notFound: true`, the page will return a `404` even if there was a
|
|
951
|
+
successfully generated page before. This is meant to support use cases like
|
|
952
|
+
user-generated content getting removed by its author. Note, `notFound`
|
|
953
|
+
follows the same `revalidate` behavior described here."
|
|
954
|
+
([getStaticProps](https://nextjs.org/docs/pages/api-reference/functions/get-static-props))
|
|
955
|
+
|
|
956
|
+
### What is served
|
|
957
|
+
|
|
958
|
+
| Request finds | `Prerender(..., { onDemand })` | `Passthrough(def, live)` |
|
|
959
|
+
| --------------------------------- | ----------------------------------------------------------- | ------------------------------------------ |
|
|
960
|
+
| fresh overlay entry | the overlay entry, loaders fresh (dev and production) | same |
|
|
961
|
+
| stale overlay entry | the overlay entry; `onRevalidate` scheduled if configured | same |
|
|
962
|
+
| "removed" marker | **404** (dev and production); the build entry is not served | the live handler (dev and production) |
|
|
963
|
+
| stale "removed" marker | the same 404; `onRevalidate` scheduled if configured | the live handler; `onRevalidate` likewise |
|
|
964
|
+
| no overlay entry, param baked | production: the build entry; dev: the dev prerender render | same |
|
|
965
|
+
| no overlay entry, param not baked | production: **404**; dev: rendered through the dev endpoint | the live handler (dev and production) |
|
|
966
|
+
| server action re-render | overlay, then build entry, then 404, as above | the live handler (the overlay is not read) |
|
|
967
|
+
|
|
968
|
+
The retained handler never renders inside a production request: a plain
|
|
969
|
+
on-demand route 404s for a param until something refreshes it. Dev renders any
|
|
970
|
+
param through the dev prerender endpoint, so a production 404 does not show up
|
|
971
|
+
in dev. A removed page is the exception: its marker answers 404 in dev too.
|
|
972
|
+
|
|
973
|
+
### Rules worth knowing
|
|
974
|
+
|
|
975
|
+
- **`ttl` is soft staleness.** Entries never expire: a stale entry still serves
|
|
976
|
+
(it is newer than the build entry below it) and only decides whether
|
|
977
|
+
`onRevalidate` is scheduled. The KV store writes no `expirationTtl`.
|
|
978
|
+
- **`onRevalidate` runs once per stale key per isolate** while one is in
|
|
979
|
+
flight, so calling `router.prerender()` from it renders once per stale key
|
|
980
|
+
on a single Node process. Across isolates, dedup belongs to the queue. Its
|
|
981
|
+
`target` is a `PrerenderTargetObject`, which the runner and its `.many()`
|
|
982
|
+
accept as is, so
|
|
983
|
+
`onRevalidate: (target, env, ctx) => router.prerender({ env, ctx })(target, { onlyIfStale: true })`
|
|
984
|
+
typechecks without a cast. `onRevalidate` receives `(target, env, ctx)`;
|
|
985
|
+
`ctx` is the stale request's execution context, absent where none exists.
|
|
986
|
+
Pass `{ onlyIfStale: true }` wherever the refresh runs later than it was
|
|
987
|
+
scheduled (here, and in a queue consumer): the entry is stale, so it
|
|
988
|
+
renders, unless another job already refreshed it (`already-fresh`) or
|
|
989
|
+
`prerender.remove()` removed the page in between (`removed`, nothing
|
|
990
|
+
rendered). A refresh without it would bring a removed page back.
|
|
991
|
+
- **Prerender tags are their own namespace.** `cacheTag()`, `updateTag()` and
|
|
992
|
+
`revalidateTag()` never reach the prerender store; the runner's `markStale()`
|
|
993
|
+
never reaches the runtime cache. (`updateTag()` needs a request context;
|
|
994
|
+
refreshes run from queues and crons.)
|
|
995
|
+
- **`markStale()` only marks.** The entry keeps serving; nothing re-renders
|
|
996
|
+
unless `onRevalidate` is configured or a sweep calls
|
|
997
|
+
`prerender(target, { onlyIfStale: true })`. Dev warns when you mark
|
|
998
|
+
with no `onRevalidate`.
|
|
999
|
+
- **The producer is requestless.** `cookies()`, `headers()` and the client-cache
|
|
1000
|
+
directives make a refresh return `skipped-personalized`. `ctx.env` is the
|
|
1001
|
+
live env bound by `router.prerender({ env })`, not `buildEnv`, and `ctx.onDemand` is `true`.
|
|
1002
|
+
- **A trigger may render params `getParams()` never returned.** Validate ids
|
|
1003
|
+
from webhooks before refreshing them.
|
|
1004
|
+
- **A throw in a refresh keeps the old entry** (`render-failed`). A
|
|
1005
|
+
`notFound()` on its own is not a failure: it removes the page (see "Remove
|
|
1006
|
+
a page").
|
|
1007
|
+
- **Intercepted navigations are not refreshed.** A refresh writes the main
|
|
1008
|
+
variant only; an intercept navigation still serves the build's intercept
|
|
1009
|
+
variant, and an on-demand route used as an intercept target 404s on a param
|
|
1010
|
+
that only a refresh produced.
|
|
1011
|
+
- **Connected clients can lag.** A refresh has no response to reach open tabs:
|
|
1012
|
+
a client that prefetched the page keeps its copy for `prefetchCacheTTL`
|
|
1013
|
+
(default 300s) and the browser HTTP cache (`prefetchCacheControl`). Lower
|
|
1014
|
+
them if the window matters.
|
|
1015
|
+
- **Entries are per deploy.** Keys carry the router's data version, so a deploy
|
|
1016
|
+
that changes server code starts from the build entries again; a client-only
|
|
1017
|
+
deploy keeps refreshed pages.
|
|
1018
|
+
|
|
1019
|
+
### Custom stores
|
|
1020
|
+
|
|
1021
|
+
A store is plain get/set; the router composes the envelope on write and
|
|
1022
|
+
verifies it on read (version and params collision guard):
|
|
1023
|
+
|
|
1024
|
+
```typescript
|
|
1025
|
+
import {
|
|
1026
|
+
serializePrerenderKey,
|
|
1027
|
+
type WritablePrerenderStore,
|
|
1028
|
+
} from "@rangojs/router/prerender";
|
|
1029
|
+
|
|
1030
|
+
const store: WritablePrerenderStore = {
|
|
1031
|
+
async get(key) {
|
|
1032
|
+
const raw = await blob.get(serializePrerenderKey(key));
|
|
1033
|
+
return raw ? JSON.parse(raw) : null;
|
|
1034
|
+
},
|
|
1035
|
+
async set(key, stored) {
|
|
1036
|
+
await blob.put(serializePrerenderKey(key), JSON.stringify(stored));
|
|
1037
|
+
},
|
|
1038
|
+
// optional: delete(key), markStale(routerId, tags)
|
|
1039
|
+
};
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
`markStale(routerId, tags)` receives the id of the router whose runner was
|
|
1043
|
+
called (`key.routerId` on that router's entries). Mark only entries whose
|
|
1044
|
+
`key.routerId` equals it: routers behind a host router share one store, and a
|
|
1045
|
+
tag means something only inside the router whose routes declared it.
|
|
1046
|
+
|
|
1047
|
+
Key everything off the `key` you are given (`serializePrerenderKey` includes
|
|
1048
|
+
`key.version`). Do not call `getCacheVersions()` in a prerender store: `set()`
|
|
1049
|
+
runs outside the producer's request context.
|
|
1050
|
+
|
|
1051
|
+
`stored` is a page (`{ v, entry, meta }`) or the "removed" marker
|
|
1052
|
+
(`{ v, removed: true, meta }`, no `entry`). A store that persists the JSON it
|
|
1053
|
+
is given, like the one above, handles both and needs no change. `meta` has the
|
|
1054
|
+
same fields on both, and means the same: a store that indexes by `meta.tags`
|
|
1055
|
+
or lowers `meta.staleAt` for `markStale()` treats a marker as it treats a
|
|
1056
|
+
page. That is how a `notFound()` marker is rechecked. A `remove()` marker has
|
|
1057
|
+
no tags and no `staleAt`. Never drop or rewrite a value you do not
|
|
1058
|
+
recognize: the router decides what it means. The router does not call
|
|
1059
|
+
`delete(key)`; removing a page is a `set()` of the marker.
|
|
1060
|
+
|
|
1061
|
+
### Several routers (host router)
|
|
1062
|
+
|
|
1063
|
+
A runner belongs to the router `router.prerender()` was called on, so an app
|
|
1064
|
+
with several routers behind a host router calls it on each router:
|
|
1065
|
+
|
|
1066
|
+
- It matches only that router's routes. A URL only another router has is
|
|
1067
|
+
`no-match`.
|
|
1068
|
+
- Entries are keyed by router id plus that router's data version, so two
|
|
1069
|
+
routers can share one `prerender.store` (one KV namespace) without touching
|
|
1070
|
+
each other's entries. A deploy that changes one router's server code leaves
|
|
1071
|
+
the others' refreshed entries served when their code is separate (for
|
|
1072
|
+
example lazy mounts); routers that share a statically imported module move
|
|
1073
|
+
together.
|
|
1074
|
+
- `markStale(tags)` marks only that router's entries, in every shipped store:
|
|
1075
|
+
`markStale(["product:1"])` on one router does not make another router's
|
|
1076
|
+
`product:1` entry stale.
|
|
1077
|
+
- A warm of a non-on-demand route goes through that router's own handler, and
|
|
1078
|
+
its cache keys carry the host: pass the host the visitors use as `origin`
|
|
1079
|
+
(`router.prerender({ env, ctx, origin: "https://a.example" })`), or a path
|
|
1080
|
+
target resolves against the calling request's origin. A multi-router app
|
|
1081
|
+
passes `origin` or a full URL on every warm, so each router warms the host
|
|
1082
|
+
its visitors use. A warm that resolves another router's host stores the
|
|
1083
|
+
warming router's entries under that host; the router that owns the host
|
|
1084
|
+
never reads them (cache keys start with the router's id: `/host-router`,
|
|
1085
|
+
"Shared cache store").
|
|
1086
|
+
|
|
1087
|
+
### KV tag markers
|
|
1088
|
+
|
|
1089
|
+
`createKVPrerenderStore(kv)` writes a marker per tag at
|
|
1090
|
+
`__rango_pr_tag__/{encodeURIComponent(routerId)}/{tag}` (the encoded router id)
|
|
1091
|
+
and markers have no expiry: an entry without a `ttl` never goes stale by
|
|
1092
|
+
itself, so an expired marker would silently drop an invalidation.
|
|
1093
|
+
|
|
1094
|
+
### Test it
|
|
1095
|
+
|
|
1096
|
+
`router.prerender()` and the serve path run under the public testing
|
|
1097
|
+
primitives: render with `router.prerender()`, then `serveShellRequest(router,
|
|
1098
|
+
url)` from `@rangojs/router/testing/flight` serves the route through the
|
|
1099
|
+
production handler and returns the Flight payload. After
|
|
1100
|
+
`prerender.remove(url)` the same request is a `404` (`result.response.status`).
|
|
1101
|
+
`createMemoryPrerenderStore` is exported from `@rangojs/router/testing` too.
|
|
1102
|
+
See `/testing`.
|
|
704
1103
|
|
|
705
1104
|
### loading() is ignored without Passthrough
|
|
706
1105
|
|
|
707
1106
|
Pre-rendered segments are fully resolved at build time and never suspend.
|
|
708
1107
|
With `Passthrough()`, `loading()` works for live fallback renders.
|
|
709
1108
|
|
|
1109
|
+
## Warm any route before traffic
|
|
1110
|
+
|
|
1111
|
+
`router.prerender()` is one verb for "make this URL ready before a visitor
|
|
1112
|
+
asks for it". For a `Prerender(..., { onDemand })` route that is the
|
|
1113
|
+
requestless render above. For **every other route** it warms: the runner
|
|
1114
|
+
sends the URL through the router's own request handler as an anonymous
|
|
1115
|
+
visitor would, in a mode where every runtime cache read misses and every
|
|
1116
|
+
write replaces the entry under the visitor's key. The old entries keep
|
|
1117
|
+
serving until the new ones are written, so there is no cold window.
|
|
1118
|
+
|
|
1119
|
+
After a deploy that changes server code, and after an `updateTag()`, every
|
|
1120
|
+
cache key starts cold and the first visitor per key pays the render. A warm
|
|
1121
|
+
pays it for them.
|
|
1122
|
+
|
|
1123
|
+
```typescript
|
|
1124
|
+
const prerender = router.prerender({
|
|
1125
|
+
env,
|
|
1126
|
+
ctx,
|
|
1127
|
+
origin: "https://shop.example",
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
await prerender("/products/1"); // any route
|
|
1131
|
+
await prerender("/search?q=wine"); // search params are part of the key
|
|
1132
|
+
await prerender.many(urls, { concurrency: 4 }); // default concurrency 1
|
|
1133
|
+
await prerender.many(urls, { onlyIfStale: true }); // top up what is cold or stale
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
### What one call does
|
|
1137
|
+
|
|
1138
|
+
| Route | `prerender(url)` | `result.path` |
|
|
1139
|
+
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
|
1140
|
+
| `Prerender(..., { onDemand })` | requestless render into the prerender store; then a warm request, when the app cache store is shared and an origin resolves | `on-demand` |
|
|
1141
|
+
| any other route, shared app cache store | a warm request through the router's handler | `warm` |
|
|
1142
|
+
| any other route, store not shared | refused before anything renders: `skipped-store-not-shared` (dev warns once) | `warm` |
|
|
1143
|
+
| any other route, no `createRouter({ cache })` | `no-store` | `warm` |
|
|
1144
|
+
|
|
1145
|
+
A warm replaces what a cookie-free visitor's document request for that URL
|
|
1146
|
+
would fill:
|
|
1147
|
+
|
|
1148
|
+
- a `ppr` route's shell (the next document request is `x-rango-shell: HIT`);
|
|
1149
|
+
- the route's `cache()` records for a document request;
|
|
1150
|
+
- the `"use cache"` results the render reaches (a function keyed by its
|
|
1151
|
+
arguments only is refreshed for every URL that calls it);
|
|
1152
|
+
- its loaders' own `cache()`;
|
|
1153
|
+
- the document cache entry (`createDocumentCacheMiddleware`), and a response
|
|
1154
|
+
route's cache.
|
|
1155
|
+
|
|
1156
|
+
It does not write the records a client navigation reads (`partial:`) or an
|
|
1157
|
+
intercept's: those fill on the first navigation. A `ppr` route's navigations
|
|
1158
|
+
replay the document shell, so they are covered.
|
|
1159
|
+
|
|
1160
|
+
A `clientUrls()` group cannot use `Prerender()`; with `ppr` on a group route,
|
|
1161
|
+
warming is how that route is ready before its first visitor.
|
|
1162
|
+
|
|
1163
|
+
### The store must be shared
|
|
1164
|
+
|
|
1165
|
+
A warm fills the cache where the call runs. That helps other traffic only when
|
|
1166
|
+
the store is shared beyond that place, so the router asks the store
|
|
1167
|
+
(`SegmentCacheStore.scope`):
|
|
1168
|
+
|
|
1169
|
+
| Store | `scope` | Warm |
|
|
1170
|
+
| --------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
1171
|
+
| `CFCacheStore` with `kv` | `"global"` | yes |
|
|
1172
|
+
| `CFCacheStore` without `kv` | `"local"` | refused: the Cache API is per colo |
|
|
1173
|
+
| `VercelCacheStore` | `"regional"` | yes; fills the region the call runs in (all traffic only on a single-region project) |
|
|
1174
|
+
| `MemorySegmentCacheStore` | `"local"` | refused in production; allowed under the dev server (one process serves every request); `{ scope: "global" }` admits it |
|
|
1175
|
+
| a custom store | its own | only when it declares `scope: "global"` or `"regional"`; a store that declares none is refused |
|
|
1176
|
+
|
|
1177
|
+
`VercelCacheStore` is not checked for the number of regions: Vercel's Runtime
|
|
1178
|
+
Cache is per region, and a warm fills the one it runs in. New projects and
|
|
1179
|
+
every Hobby project run functions in one region (`iad1`); on a multi-region
|
|
1180
|
+
project warm from each region, or accept that the others fill on first use.
|
|
1181
|
+
|
|
1182
|
+
The shared-store check covers the app store (`createRouter({ cache })`) only.
|
|
1183
|
+
A warm also overwrites entries in a route's or loader's own `cache({ store })`,
|
|
1184
|
+
and that store is not checked: one that is `local` is warmed for the calling
|
|
1185
|
+
process only, so no other process or location sees the new entry.
|
|
1186
|
+
|
|
1187
|
+
The dev exception exists so you can try warming locally on the Node preset. It
|
|
1188
|
+
follows the dev server, not `NODE_ENV`: a production build, `vite preview` and
|
|
1189
|
+
a test runner all refuse the memory store. A single-process deployment whose
|
|
1190
|
+
memory store really is the only copy says so with the `scope` option:
|
|
1191
|
+
|
|
1192
|
+
```typescript
|
|
1193
|
+
const store = new MemorySegmentCacheStore({ scope: "global" });
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
It is not the default because the store is per process and the router cannot
|
|
1197
|
+
tell one long-running server from several replicas or serverless instances. A
|
|
1198
|
+
default of `"global"` would report `warmed` while most visitors hit a cold
|
|
1199
|
+
instance, so you opt in only when exactly one process serves the app. Two
|
|
1200
|
+
instances with the same `name` share their maps but each declares its own
|
|
1201
|
+
`scope`: keep them identical.
|
|
1202
|
+
|
|
1203
|
+
### The origin
|
|
1204
|
+
|
|
1205
|
+
Cache keys carry the host, so a warm must request the host visitors use. In
|
|
1206
|
+
order:
|
|
1207
|
+
|
|
1208
|
+
1. the target's own origin (`prerender("https://shop.example/p/1")`, or a
|
|
1209
|
+
`URL`);
|
|
1210
|
+
2. `origin` on the binding (`router.prerender({ env, ctx, origin })`), for
|
|
1211
|
+
path and `{ route, params }` targets;
|
|
1212
|
+
3. the origin of the request the runner is called from, when there is one: a
|
|
1213
|
+
server action, a route handler, `onRevalidate`;
|
|
1214
|
+
4. none: `skipped-no-origin`.
|
|
1215
|
+
|
|
1216
|
+
A cron or queue handler has no request, so give it `origin`. An on-demand
|
|
1217
|
+
render never needs one (prerender keys carry no host); without one it only
|
|
1218
|
+
skips the warm request that follows.
|
|
1219
|
+
|
|
1220
|
+
A hash, or a parameter the router reserves (`_rsc*`, `__no_cache`), is
|
|
1221
|
+
`skipped-unsupported-target`.
|
|
1222
|
+
|
|
1223
|
+
### The request is anonymous
|
|
1224
|
+
|
|
1225
|
+
The warm is a `GET` with `accept: text/html` and nothing else: no cookies, no
|
|
1226
|
+
authorization, no user agent, no `accept-language`, and on Cloudflare no
|
|
1227
|
+
`request.cf`. It lands in the partition a header-less visitor lands in (a
|
|
1228
|
+
route partitioned by `cache({ key })` or the store's `keyGenerator` gets that
|
|
1229
|
+
one partition warmed).
|
|
1230
|
+
|
|
1231
|
+
- Middleware runs and sees an anonymous request. An auth middleware that
|
|
1232
|
+
redirects makes the result `render-failed` with `responseStatus: 302`: an
|
|
1233
|
+
anonymous visitor is not served a cached page there either.
|
|
1234
|
+
- `cookies()` or `headers()` read outside any cached scope see nothing, and
|
|
1235
|
+
the anonymous view is rendered and stored wherever a cookie-free visitor's
|
|
1236
|
+
would be.
|
|
1237
|
+
- A read inside `cache()`, `"use cache"` or a `ppr` shell capture trips the
|
|
1238
|
+
same guard it trips for a visitor; the result is `skipped-personalized`.
|
|
1239
|
+
- Analytics that count document GETs count warms, and a rate limiter keyed by
|
|
1240
|
+
IP sees an empty key.
|
|
1241
|
+
|
|
1242
|
+
Nothing a client sends makes a request a warm: the mark is in process.
|
|
1243
|
+
|
|
1244
|
+
### Results
|
|
1245
|
+
|
|
1246
|
+
```typescript
|
|
1247
|
+
const result = await prerender("/products/1");
|
|
1248
|
+
if (!result.ok) console.warn(result.status, result.caches);
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
| `status` | `ok` | Meaning |
|
|
1252
|
+
| ---------------------------- | ------- | ----------------------------------------------------------------------------------------------- |
|
|
1253
|
+
| `warmed` | `true` | the request rendered and at least one cache wrote |
|
|
1254
|
+
| `already-fresh` | `true` | `onlyIfStale`: nothing was cold or stale |
|
|
1255
|
+
| `skipped-store-not-shared` | `false` | the app store's `scope` is `"local"` or undeclared; nothing rendered |
|
|
1256
|
+
| `skipped-no-origin` | `false` | a path target with no origin to request it on |
|
|
1257
|
+
| `no-store` | `false` | no `createRouter({ cache })`, or `enabled: false` |
|
|
1258
|
+
| `skipped-personalized` | `false` | an identity guard refused a read |
|
|
1259
|
+
| `shell-not-stored` | `false` | a `ppr` route whose shell was not stored; `caches.shell` and `caches.refusal` say why |
|
|
1260
|
+
| `skipped-uncached` | `false` | the request rendered and no cache wrote (the route caches nothing, or every write was refused) |
|
|
1261
|
+
| `render-failed` | `false` | the handler threw, the response was not 200 (`responseStatus`), or the render reported an error |
|
|
1262
|
+
| `skipped-unsupported-target` | `false` | a hash or a reserved parameter |
|
|
1263
|
+
| `no-match` | `false` | no route matches the path |
|
|
1264
|
+
|
|
1265
|
+
`result.target` is the absolute URL that was requested. `result.caches` has
|
|
1266
|
+
the detail: `writes` by store family (`record`, `item`, `response`, `shell`),
|
|
1267
|
+
`shell` (`stored`, `fresh`, `refused`, `no-shell`, `not-eligible`,
|
|
1268
|
+
`skipped-capacity`, `skipped-queue-timeout`, `error`), `refusal` when the
|
|
1269
|
+
capture refused, and `document` (`stored` or `not-cacheable`) when the document
|
|
1270
|
+
cache middleware ran. `throwOnError: true` throws a `PrerenderError` on any
|
|
1271
|
+
`ok: false`, refusals included.
|
|
1272
|
+
|
|
1273
|
+
`.many()` dispatches each target on its own, so a batch can mix on-demand and
|
|
1274
|
+
plain routes: one result per target, in input order.
|
|
1275
|
+
|
|
1276
|
+
`onlyIfStale` on a plain route makes the request read its caches normally
|
|
1277
|
+
instead of forcing misses: a fresh entry is left alone, a stale one refreshes,
|
|
1278
|
+
a missing one is written. It costs a visitor's request when everything is
|
|
1279
|
+
fresh.
|
|
1280
|
+
|
|
1281
|
+
### Refreshing changed content: `updateTag`, then `prerender`
|
|
1282
|
+
|
|
1283
|
+
A warm replaces the entry where it runs, and on a shared store that is the
|
|
1284
|
+
entry other locations read on their next miss. It does not reach a copy
|
|
1285
|
+
another location already holds: on `CFCacheStore`, each colo keeps serving its
|
|
1286
|
+
own Cache API copy until it expires, and an entry whose `ttl + swr` is under
|
|
1287
|
+
60 seconds never reaches KV at all.
|
|
1288
|
+
|
|
1289
|
+
So after a deploy (new keys: no location holds a copy) a warm alone makes
|
|
1290
|
+
the route ready everywhere. For a content change, invalidate first, then
|
|
1291
|
+
warm:
|
|
1292
|
+
|
|
1293
|
+
```typescript
|
|
1294
|
+
// A CMS webhook route. A webhook is a request, so updateTag() works here and
|
|
1295
|
+
// the warm takes the request's origin.
|
|
1296
|
+
path.json("/hooks/product-changed", async (ctx) => {
|
|
1297
|
+
const { id } = (await ctx.request.json()) as { id: string };
|
|
1298
|
+
await updateTag(`product:${id}`); // drops every location's copy
|
|
1299
|
+
const result = await router.prerender({
|
|
1300
|
+
env: ctx.env,
|
|
1301
|
+
ctx: ctx.executionContext,
|
|
1302
|
+
})(`/products/${id}`); // writes the new one
|
|
1303
|
+
return { status: result.status };
|
|
1304
|
+
});
|
|
1305
|
+
```
|
|
1306
|
+
|
|
1307
|
+
The invalidation makes every location's copy unservable, so no visitor is
|
|
1308
|
+
served the old content, and the warm writes the new entry where it runs and
|
|
1309
|
+
to the shared store. A location with no copy of its own reads that entry on
|
|
1310
|
+
its next request and renders nothing. On `CFCacheStore`, a colo that was
|
|
1311
|
+
still holding its own (now invalidated) copy renders once for itself instead
|
|
1312
|
+
of reading the warmed entry, unless the store uses `tagPurge`, which evicts
|
|
1313
|
+
those copies. Await `updateTag()` before the warm: a write that started
|
|
1314
|
+
before an invalidation of one of its tags is refused.
|
|
1315
|
+
|
|
1316
|
+
### Limits worth knowing
|
|
1317
|
+
|
|
1318
|
+
- **Only the router's own handler.** A warm goes through `router.fetch`, the
|
|
1319
|
+
handler `createRouter`'s `cache`, `nonce` and `version` configure. An app
|
|
1320
|
+
entry that builds its own `createRSCHandler({ cache, version })` is not what
|
|
1321
|
+
a warm runs: put those options on `createRouter`.
|
|
1322
|
+
- **Document records only.** Navigation (`partial:`) and intercept records
|
|
1323
|
+
fill on the first navigation.
|
|
1324
|
+
- **One location at a time.** A warm does not reach a copy another location
|
|
1325
|
+
already holds; see the refresh pattern above.
|
|
1326
|
+
- **A warm costs a visitor's MISS:** one render, plus a shell capture on a
|
|
1327
|
+
`ppr` route. `concurrency` defaults to 1 so a sweep never fans out against
|
|
1328
|
+
your data source unless you ask. One call waits for the request's
|
|
1329
|
+
background work (the cache writes, the capture), up to about 40 seconds in
|
|
1330
|
+
the worst case; a large list belongs in a queue, one message per batch.
|
|
1331
|
+
- **Gradual deployments:** a warm fills the version that received the call.
|
|
1332
|
+
- **The correctness guards still decide.** A warm never stores what a
|
|
1333
|
+
visitor's request would refuse to store: `cache(false)`, a false
|
|
1334
|
+
`condition()`, `private` / `no-store` / `Set-Cookie` on a document, a tag
|
|
1335
|
+
invalidated mid-render, an entry over the store's size limit.
|
|
1336
|
+
|
|
1337
|
+
### Test it
|
|
1338
|
+
|
|
1339
|
+
`router.prerender()` warms under the public testing primitives too: warm, then
|
|
1340
|
+
`serveShellRequest(router, url)` from `@rangojs/router/testing/flight` serves
|
|
1341
|
+
the next visitor's request through the production handler (`shellStatus:
|
|
1342
|
+
"HIT"`). Configure the store on the router (`createRouter({ cache: { store }
|
|
1343
|
+
})`), not through `serveShellRequest`'s `cacheStore` option, and give it a
|
|
1344
|
+
shared scope: the shipped `MemorySegmentCacheStore` is refused, so pass
|
|
1345
|
+
`{ scope: "global" }` as above. See `/testing`.
|
|
1346
|
+
|
|
710
1347
|
## Complete Example
|
|
711
1348
|
|
|
712
1349
|
Known guides are pre-rendered; any other slug is rendered live by the
|
|
@@ -835,9 +1472,12 @@ Pre-rendered routes set flags on the route trie leaf at build time:
|
|
|
835
1472
|
|
|
836
1473
|
- `pr: true` -- route has pre-rendered segment data
|
|
837
1474
|
- `pt: true` -- route wrapped with `Passthrough()` (live handler available)
|
|
1475
|
+
- `od: true` -- route opted into on-demand refresh (`onDemand`); the writable
|
|
1476
|
+
store is read before the build manifest
|
|
838
1477
|
|
|
839
1478
|
At runtime, the cache-lookup middleware uses these flags:
|
|
840
1479
|
|
|
1480
|
+
- `od + overlay hit` -- serve the on-demand entry
|
|
841
1481
|
- `pr + hit` -- serve pre-rendered Flight payload
|
|
842
1482
|
- `pr + pt + miss` -- fall through to Passthrough live handler
|
|
843
1483
|
- `pr + miss` (no pt) -- fall through to the stubbed handler, which throws a
|
|
@@ -850,4 +1490,5 @@ At runtime, the cache-lookup middleware uses these flags:
|
|
|
850
1490
|
- `/ppr` — the `ppr` path option; combine with `Prerender` for build-time shells
|
|
851
1491
|
- `/shell-manifest` — prerendered shell feeding ids to a live loader
|
|
852
1492
|
- `/deployment-caching` — why prerender output is not a CDN static file
|
|
1493
|
+
- `/cloudflare` — the KV-backed on-demand store and a queue consumer
|
|
853
1494
|
- `/loader` — loaders stay live on prerendered routes
|