@rangojs/router 0.24.3 → 0.25.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/README.md +1 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/types.d.ts +8 -0
- package/dist/types/client-urls/client-root.d.ts +4 -1
- package/dist/types/client-urls/client-urls.d.ts +28 -2
- package/dist/types/client-urls/navigation.d.ts +6 -8
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +20 -5
- package/dist/types/client-urls/types.d.ts +35 -17
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -6
- package/dist/types/router/router-options.d.ts +4 -7
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +19 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
- package/dist/types/rsc/shell-capture.d.ts +7 -0
- package/dist/types/rsc/types.d.ts +5 -0
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/context.d.ts +64 -4
- package/dist/types/server/request-context.d.ts +41 -4
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +41 -4
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +4 -3
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/urls/path-helper.d.ts +5 -0
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6816 -6547
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/caching/SKILL.md +3 -1
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +195 -32
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +32 -10
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +43 -8
- package/skills/prerender/SKILL.md +1 -2
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +85 -21
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +119 -98
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-reverse.ts +4 -17
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +2 -0
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/types.ts +8 -0
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/client-urls/client-root.tsx +10 -5
- package/src/client-urls/client-urls.ts +246 -166
- package/src/client-urls/navigation.ts +45 -38
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +286 -75
- package/src/client-urls/types.ts +40 -16
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +27 -7
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +6 -8
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -7
- package/src/router/router-options.ts +4 -7
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +37 -2
- package/src/router/tracing.ts +2 -2
- package/src/router.ts +2 -9
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +83 -41
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +22 -0
- package/src/rsc/shell-capture.ts +11 -0
- package/src/rsc/types.ts +5 -0
- package/src/segment-system.tsx +125 -119
- package/src/server/context.ts +96 -5
- package/src/server/handle-store.ts +3 -2
- package/src/server/request-context.ts +42 -3
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/dispatch.ts +4 -5
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +77 -4
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +4 -3
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/urls/path-helper.ts +16 -3
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
package/package.json
CHANGED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-structure
|
|
3
|
+
description: Concrete urls() layouts for whole Rango apps, with the file tree, router.tsx, the root urls.tsx with sync include() of section modules, and where middleware, server layouts, clientUrls() groups, response routes, caching, prerender, PPR and boundaries sit. Recipes for a large signed-in app with a protected admin area (auth, onboarding, app, settings and admin as clientUrls() groups under one server shell), several apps under createHostRouter(), a docs site or blog, and a shop. Use when starting or restructuring a Rango app, laying out urls() files, splitting an app into sections or separate apps, or setting up a dashboard, SaaS, multi-app, static or content site, or e-commerce app.
|
|
4
|
+
argument-hint: "[large-app|multi-app|content-site|e-commerce]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# App structure recipes
|
|
8
|
+
|
|
9
|
+
The other skills cover one primitive each. This one puts them together into
|
|
10
|
+
whole apps: for each common shape, the file tree, `router.tsx`, the root
|
|
11
|
+
`urls.tsx` with a sync `include()` per section, each section module, and
|
|
12
|
+
where middleware, layouts, `clientUrls()` groups, response routes, caches,
|
|
13
|
+
prerender and PPR, and error boundaries go, with the reason for each
|
|
14
|
+
placement.
|
|
15
|
+
|
|
16
|
+
## Not this skill if…
|
|
17
|
+
|
|
18
|
+
- You want the mechanics of one primitive: `include()` and helper factories
|
|
19
|
+
are `/composability`, router options are `/router-setup`, route groups of
|
|
20
|
+
client components are `/client-urls`, hostname routing is `/host-router`.
|
|
21
|
+
- You want navigation UX inside one of these shapes: where loaders,
|
|
22
|
+
`loading()`, inline `<Suspense>` and `revalidate()` go so that navigations
|
|
23
|
+
show the next page early and never flash a fallback. That is the
|
|
24
|
+
`ux-architecture` skill.
|
|
25
|
+
|
|
26
|
+
## Pick a recipe
|
|
27
|
+
|
|
28
|
+
| You are building | Shape | Read |
|
|
29
|
+
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
|
|
30
|
+
| A large signed-in app (SaaS, dashboard) with a protected admin area | One server shell; `auth`, `onboarding`, `app`, `settings` and `admin` as `clientUrls()` groups; guards as middleware | [`./large-app.md`](./large-app.md) |
|
|
31
|
+
| Several apps on one deployment: marketing on the apex, `app.`, `admin.`, `docs.` | `createHostRouter()` with one `createRouter()` per app | [`./multi-app.md`](./multi-app.md) |
|
|
32
|
+
| A docs site or a blog | A module per section; `Prerender`, `Static`, `ppr` shells, on-demand refresh | [`./content-site.md`](./content-site.md) |
|
|
33
|
+
| A shop | `ppr` catalog and product pages, a live cart, a server-rendered checkout | [`./e-commerce.md`](./e-commerce.md) |
|
|
34
|
+
| A small tool, or the first version of anything above | One `urls()` module | [A small app](#a-small-app-one-module) |
|
|
35
|
+
|
|
36
|
+
Each recipe file stands alone. Start with the one that matches, and come back
|
|
37
|
+
here for the rules they share.
|
|
38
|
+
|
|
39
|
+
## The rules every recipe follows
|
|
40
|
+
|
|
41
|
+
1. **The route tree has its own module.** `router.tsx` imports `urlpatterns`
|
|
42
|
+
from `urls.tsx` and calls `createRouter()`. Nothing the route tree imports
|
|
43
|
+
may import the router: a module that does is an import cycle, and when it
|
|
44
|
+
loads first (a test, a script) `createRouter().routes()` receives
|
|
45
|
+
`undefined` and throws `Cannot read properties of undefined (reading
|
|
46
|
+
'handler')`. Code that needs the router at request time, such as a webhook
|
|
47
|
+
that calls `router.prerender()`, lives next to the entry
|
|
48
|
+
([`./content-site.md`](./content-site.md#the-publish-webhook)).
|
|
49
|
+
2. **One module per section, mounted with a sync `include()` and a name.**
|
|
50
|
+
`include("/settings", settingsUrls, { name: "settings" })`. The root module
|
|
51
|
+
then reads as a table of contents: every URL prefix and who owns it.
|
|
52
|
+
3. **Identity in global middleware, gates in route middleware.**
|
|
53
|
+
`router.use()` wraps every request: renders, server actions, and
|
|
54
|
+
fetchable-loader calls (`load()`, `refetch`, `useRefreshLoaders()`). Route
|
|
55
|
+
`middleware()` wraps renders only, on document loads and on every client
|
|
56
|
+
navigation. So the session is resolved in `router.use()`, the redirect to
|
|
57
|
+
the login page is a `middleware()` around the includes it guards, and every
|
|
58
|
+
server action and fetchable loader authorizes itself.
|
|
59
|
+
4. **Chrome that spans sections is a server `layout()` above their
|
|
60
|
+
includes.** It renders on the document load and is kept on every
|
|
61
|
+
navigation below it. Chrome of one area is that area's own layout.
|
|
62
|
+
5. **Data comes from loaders,** read with `useLoader()` in the component that
|
|
63
|
+
shows it. Public pages get `ppr` or `Prerender`, an expensive read shared
|
|
64
|
+
by many users gets `"use cache"` and a tag, and per-user data stays in live
|
|
65
|
+
loaders.
|
|
66
|
+
6. **Commit the generated files.** `router.named-routes.gen.ts` sits next to
|
|
67
|
+
the router; the Vite plugin rewrites it on `vite dev` and `vite build`, and
|
|
68
|
+
`rango generate src/router.tsx` writes it without Vite (CI, a fresh clone).
|
|
69
|
+
`rango generate src` also writes a `*.gen.ts` beside every `urls()` and
|
|
70
|
+
`clientUrls()` module, the local `routes` map that `useReverse(routes)` and
|
|
71
|
+
`Handler<".name", routes>` read. Regenerate and commit both whenever a
|
|
72
|
+
`path()` or `include()` changes.
|
|
73
|
+
7. **Link by name, never by string. Inside a module, by local name, never by
|
|
74
|
+
its mount.** Moving an `include()` mount or renaming a path then updates
|
|
75
|
+
every link; a hardcoded `"/app/projects"` breaks silently. A local name
|
|
76
|
+
(`ctx.reverse(".x")`, `useReverse(routes)`) resolves against wherever the
|
|
77
|
+
module is mounted and fills the mount's params (`:locale`, `:accountId`,
|
|
78
|
+
`:projectId`) from the current URL, so the module never knows its prefix
|
|
79
|
+
and can be mounted twice. The maps exist only if generated and committed
|
|
80
|
+
(rule 6).
|
|
81
|
+
- Server handlers, layouts, parallel slots, loaders: `ctx.reverse(".local")`
|
|
82
|
+
or `ctx.reverse("global.name")`; both fill mount params, and explicit
|
|
83
|
+
params override (`{ locale: "de" }` switches; `{ locale: undefined }`
|
|
84
|
+
drops an optional `/:locale?` and throws for a required `/:locale`).
|
|
85
|
+
`scopedReverse` types the local names.
|
|
86
|
+
- Middleware `ctx.reverse` takes global names only and fills nothing, so
|
|
87
|
+
pass every param; a missing one throws `Missing param`.
|
|
88
|
+
`getRequestContext().reverse` in route middleware does fill; in global
|
|
89
|
+
middleware only after `next()`.
|
|
90
|
+
- An action's `getRequestContext().reverse("global.name")` fills from the
|
|
91
|
+
page URL; use global names.
|
|
92
|
+
- A client component linking within one module or `clientUrls()` group:
|
|
93
|
+
`useReverse(routes)`, `routes` imported from that module's `.gen.ts`
|
|
94
|
+
(`import { routes } from "./app.client.gen"`). Mount-aware, fills params
|
|
95
|
+
from `useParams()`, and only that module's names reach the client bundle.
|
|
96
|
+
Wrap each map once in `src/lib/links.ts`
|
|
97
|
+
(`export const useAppReverse = () => useReverse(appRoutes)`) and call
|
|
98
|
+
that hook in components under that module's own mount (`/links`).
|
|
99
|
+
- A URL from another module: generate it on the server and pass it as a
|
|
100
|
+
prop or loader data. Another module's map, or `router.named-routes.gen.ts`
|
|
101
|
+
under a mount, gives a wrong URL with no error (the latter is correct
|
|
102
|
+
only at the root mount and pulls every route name into the client
|
|
103
|
+
bundle).
|
|
104
|
+
- Not these: `useParams().accountId` in a template string, `ctx.params`
|
|
105
|
+
passed down as props, a "current account" context, `href("/path")` for
|
|
106
|
+
anything but static paths. `useHref()` and a relative `router.push()`
|
|
107
|
+
fill the mount's params but take unchecked paths: prefer `useReverse`.
|
|
108
|
+
8. **Rewrite is middleware only.** A maintenance page, a flag or an alias
|
|
109
|
+
URL is a `router.use()` middleware that does `throw rewrite("name")`; a
|
|
110
|
+
lock answers mutations with a `Response` or adds `.disallowMutations()`,
|
|
111
|
+
since a rewrite moves a server action to the target. The address bar
|
|
112
|
+
keeps the visitor's URL. A loader, handler or server action
|
|
113
|
+
cannot rewrite, and a loop answers 508. Redirect when the new URL should
|
|
114
|
+
show (`/middleware`, "Rewrite").
|
|
115
|
+
|
|
116
|
+
## A small app: one module
|
|
117
|
+
|
|
118
|
+
The minimal recommended shape, and the starting point of every recipe:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
src/
|
|
122
|
+
router.tsx createRouter(), the Document, router.use()
|
|
123
|
+
router.named-routes.gen.ts generated, committed
|
|
124
|
+
urls.tsx the whole route tree
|
|
125
|
+
document.tsx "use client" Document (optional)
|
|
126
|
+
pages/ loaders/ actions/
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
// src/router.tsx
|
|
131
|
+
import { createRouter } from "@rangojs/router";
|
|
132
|
+
import { Document } from "./document";
|
|
133
|
+
import { urlpatterns } from "./urls";
|
|
134
|
+
|
|
135
|
+
export const router = createRouter({
|
|
136
|
+
document: Document,
|
|
137
|
+
urls: urlpatterns,
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
// src/urls.tsx
|
|
143
|
+
import { urls } from "@rangojs/router";
|
|
144
|
+
import { RootLayout } from "./pages/root-layout"; // renders <Outlet />
|
|
145
|
+
import { HomePage, ReportsPage, ReportPage } from "./pages";
|
|
146
|
+
import { ReportLoader } from "./loaders/report";
|
|
147
|
+
|
|
148
|
+
export const urlpatterns = urls(({ path, layout, loader }) => [
|
|
149
|
+
layout(RootLayout, () => [
|
|
150
|
+
path("/", HomePage, { name: "home" }),
|
|
151
|
+
path("/reports", ReportsPage, { name: "reports" }),
|
|
152
|
+
path("/reports/:id", ReportPage, { name: "report" }, () => [
|
|
153
|
+
loader(ReportLoader),
|
|
154
|
+
]),
|
|
155
|
+
]),
|
|
156
|
+
]);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
On the Node preset that is the whole app: `rango()` finds the router and
|
|
160
|
+
generates the entries (`/router-setup`). Keep it in one module until a
|
|
161
|
+
section has its own layout, its own guard, or more than a handful of routes,
|
|
162
|
+
then follow [Growing an app](#growing-an-app).
|
|
163
|
+
|
|
164
|
+
## Sync or async include()
|
|
165
|
+
|
|
166
|
+
Default to the sync form, `include("/x", xPatterns)`. Both forms match the
|
|
167
|
+
same URLs, build the same names and types, and expand the module's routes
|
|
168
|
+
lazily on the first request under the prefix
|
|
169
|
+
(`src/urls/include-helper.ts`). They differ in when the module itself is
|
|
170
|
+
loaded:
|
|
171
|
+
|
|
172
|
+
| | `include("/x", xPatterns)` | `include("/x", () => import("./x"))` |
|
|
173
|
+
| -------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
174
|
+
| Module load | at startup, with the entry | its own chunk, imported on the first request under the prefix, then cached |
|
|
175
|
+
| First request under the prefix | no extra cost | pays the chunk import: about 75 ms for a 240-route group, 188 ms for 9,000 routes, measured cold on Workers (`/composability`) |
|
|
176
|
+
| Nested async includes | n/a | import one after another: a 3-level chain measured 464 ms on its first hit |
|
|
177
|
+
| Route types, `href()`, prerender | complete | complete: build discovery and `rango generate` await the provider |
|
|
178
|
+
| `rango generate` (static parser) | resolves it | resolves `() => import("./x")` when `./x` has `export default urls(...)`; a `.then((m) => m.x)` provider needs `rango generate --runtime` |
|
|
179
|
+
|
|
180
|
+
Switch a group to async when it pays: the group has hundreds of routes, or
|
|
181
|
+
pulls in heavy server-only modules the rest of the app does not share, and
|
|
182
|
+
your platform starts cold often (Workers isolates, serverless functions).
|
|
183
|
+
Admin areas and large API surfaces are the usual candidates. A
|
|
184
|
+
`clientUrls()` group gains no startup time from it (`/client-urls`), so keep
|
|
185
|
+
group includes sync. Measure before and after: the sizing rules and numbers
|
|
186
|
+
are in `/composability` → "Code-splitting a route group with async include()".
|
|
187
|
+
|
|
188
|
+
## Naming includes
|
|
189
|
+
|
|
190
|
+
| Include | Names in the app-wide map | Use for |
|
|
191
|
+
| ---------------------------------------------- | --------------------------------------- | --------------------------------------------------------------- |
|
|
192
|
+
| `include("/shop", shopUrls, { name: "shop" })` | `shop.index`, `shop.product`, … | every section of an app |
|
|
193
|
+
| `include("/", machineUrls, { name: "" })` | the child names as they are (`sitemap`) | a module whose names are unique app-wide |
|
|
194
|
+
| `include("/x", widgetUrls)` | none: a hidden scope (`$prefix_0`) | a reusable module linked only from inside, with `.name` reverse |
|
|
195
|
+
|
|
196
|
+
Name every section include. A name is what `ctx.reverse("settings.profile")`,
|
|
197
|
+
`href()`, `Handler<"settings.profile">` and the redirects in your middleware
|
|
198
|
+
type-check against, and an unnamed include keeps its routes out of
|
|
199
|
+
`router.named-routes.gen.ts` altogether.
|
|
200
|
+
|
|
201
|
+
In 0.24.x, give a name to every include that sits inside an included module.
|
|
202
|
+
With the route trie live (every request in `vite dev` and in production), an
|
|
203
|
+
unnamed include inside another unnamed include answers 404 for all of its
|
|
204
|
+
routes, with no warning from the build (#1104), and an unnamed include inside
|
|
205
|
+
a `{ name: "" }` include can render another module's handler when two modules
|
|
206
|
+
name a route alike. One unnamed level in the root module is fine.
|
|
207
|
+
|
|
208
|
+
## Where shared chrome lives
|
|
209
|
+
|
|
210
|
+
| Chrome | Put it in | Why |
|
|
211
|
+
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
212
|
+
| Spans several sections (top nav, user menu, footer) | a server `layout()` in `urls()`, above their includes | Rendered once on the document load and kept on every navigation below it; navigations do not re-render it, actions skip it (`/layout`) |
|
|
213
|
+
| Data in that chrome (the user, a workspace list, a cart count) | a `loader()` on that layout, read with `useLoader()` | Loaders are the live layer; a loader above the route re-runs after actions by default and not on navigations (`/loader`, segment defaults) |
|
|
214
|
+
| One area's chrome (tabs, a sub-nav, a pending indicator) | the area's own `layout()`: in `urls()`, or the group's `layout()` in `clientUrls()` | In a group, `useOutlet().pending` drives the chrome at the click, and the layout keeps its instance across its routes (`/client-urls`) |
|
|
215
|
+
| The document (`<html>`, stylesheets, theme script) | the `document` option | One per router; a cross-app navigation loads the other app's (`/host-router`) |
|
|
216
|
+
|
|
217
|
+
For where boundaries and `revalidate()` rules go inside that chrome so
|
|
218
|
+
navigations stay smooth, see the `ux-architecture` skill.
|
|
219
|
+
|
|
220
|
+
## Growing an app
|
|
221
|
+
|
|
222
|
+
1. **Start with one `urls()` module** ([A small app](#a-small-app-one-module)).
|
|
223
|
+
2. **Extract a section into its own module** when it gets its own layout, its
|
|
224
|
+
own guard or a dozen routes: move its `path()`s into `urls/<section>.tsx`
|
|
225
|
+
with paths relative to the section, and mount it with
|
|
226
|
+
`include("/<section>", sectionUrls, { name: "<section>" })`. Names change
|
|
227
|
+
from `report` to `reports.detail`: regenerate, and the type checker lists
|
|
228
|
+
every `ctx.reverse()` and `href()` to update.
|
|
229
|
+
3. **Lift shared chrome and guards** out of the sections: a server `layout()`
|
|
230
|
+
around the includes that share a shell, a `middleware(guard, () => [...])`
|
|
231
|
+
around the includes that share a gate.
|
|
232
|
+
4. **Move a section into a `clientUrls()` group** when most of its
|
|
233
|
+
navigations are tab, param and filter switches inside one layout, and its
|
|
234
|
+
handlers' data can become loaders read with `useLoader()` (a group has no
|
|
235
|
+
handlers). Middleware, `errorBoundary()`, `notFoundBoundary()`, `cache()`
|
|
236
|
+
and `parallel()` stay in the server tree around the include; the group
|
|
237
|
+
rejects them. Its `revalidate()` rules move into the group with its
|
|
238
|
+
loaders and keep running on the server. See `/client-urls`.
|
|
239
|
+
5. **Split code** with an async `include()` only for a group that measures
|
|
240
|
+
large ([Sync or async](#sync-or-async-include)).
|
|
241
|
+
6. **Split into separate apps** under `createHostRouter()` when parts of the
|
|
242
|
+
product have different documents, deploy cadences or hostnames
|
|
243
|
+
([`./multi-app.md`](./multi-app.md)). Navigations between them are
|
|
244
|
+
document loads by design.
|
|
245
|
+
|
|
246
|
+
## Related
|
|
247
|
+
|
|
248
|
+
- `/router-setup`: `createRouter()` options, the Document, presets.
|
|
249
|
+
- `/composability`: `include()`, factories, async-include sizing.
|
|
250
|
+
- `/client-urls`: route groups of client components.
|
|
251
|
+
- `/middleware`: execution model, redirects, `throw rewrite()`.
|
|
252
|
+
- `/host-router`: hostname patterns, the host-override cookie, shared stores.
|
|
253
|
+
- `/ppr`, `/prerender`, `/cache-guide`: which cache layer a page needs.
|
|
254
|
+
- `/links`, `/typesafety`: names, `ctx.reverse()`, `href()`, generated files.
|
|
255
|
+
- `ux-architecture`: navigation UX inside these shapes.
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# A docs site and a blog
|
|
2
|
+
|
|
3
|
+
A content site: docs written as files in the repository, a blog fed by a
|
|
4
|
+
CMS, a home page, and the machine routes (sitemap, robots, feeds). Almost
|
|
5
|
+
every page is the same for every visitor, so almost every page is built
|
|
6
|
+
ahead of the request.
|
|
7
|
+
|
|
8
|
+
- **Docs** change only when the repository does: `Prerender` renders every
|
|
9
|
+
page at build time and `ppr` bakes each page's HTML shell too. The nav is a
|
|
10
|
+
`Static` segment rendered once.
|
|
11
|
+
- **The blog** changes when an editor publishes: `Prerender` with `onDemand`
|
|
12
|
+
bakes the newest posts at build, a publish webhook re-renders one post with
|
|
13
|
+
`router.prerender()`, and `Passthrough()` renders anything else live.
|
|
14
|
+
- **Each section is one module** mounted with a sync `include()`.
|
|
15
|
+
|
|
16
|
+
Prerendering is caching at build time: the worker still handles every
|
|
17
|
+
request and reads the stored payload the way it reads a cache hit, so
|
|
18
|
+
middleware and loaders still run (`/prerender`). The repository's own docs
|
|
19
|
+
site, `apps/docs`, is this shape in one module: `Prerender` pages with `ppr`,
|
|
20
|
+
a `Static` nav in a parallel slot, and `path.text()` / `path.xml()` machine
|
|
21
|
+
routes.
|
|
22
|
+
|
|
23
|
+
## File tree
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
src/
|
|
27
|
+
worker.rsc.tsx Cloudflare entry, plus the publish webhook
|
|
28
|
+
publish.ts the webhook: router.prerender() for the changed pages
|
|
29
|
+
router.tsx cache store (shells) and prerender store (refreshes)
|
|
30
|
+
router.named-routes.gen.ts generated, committed
|
|
31
|
+
env.ts
|
|
32
|
+
urls.tsx root: site layout, docs, blog, machine routes
|
|
33
|
+
urls/
|
|
34
|
+
docs.tsx urls(): DocsLayout, nav slot, pages
|
|
35
|
+
blog.tsx urls(): index, posts
|
|
36
|
+
machine.tsx urls(): sitemap.xml, robots.txt
|
|
37
|
+
pages/
|
|
38
|
+
docs.tsx DocsNav (Static), DocsLayout, DocPage and DocsIndex (Prerender)
|
|
39
|
+
blog.tsx BlogPostDef (Prerender, onDemand), BlogPost (Passthrough), BlogIndex
|
|
40
|
+
site.tsx SiteLayout, HomePage
|
|
41
|
+
lib/content.ts docs from files, posts from the CMS
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`Prerender` and `Static` definitions are exported `const`s in their own
|
|
45
|
+
modules, never written inline in `path()`: the build injects their ids from
|
|
46
|
+
the export and evicts their handlers from the production bundle.
|
|
47
|
+
|
|
48
|
+
## router.tsx
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// src/router.tsx
|
|
52
|
+
import { createRouter } from "@rangojs/router";
|
|
53
|
+
import { CFCacheStore } from "@rangojs/router/cache";
|
|
54
|
+
import { createKVPrerenderStore } from "@rangojs/router/prerender/cloudflare";
|
|
55
|
+
import type { AppBindings } from "./env";
|
|
56
|
+
import { urlpatterns } from "./urls";
|
|
57
|
+
|
|
58
|
+
export const router = createRouter<AppBindings>({
|
|
59
|
+
// ppr shells and runtime caches.
|
|
60
|
+
cache: (env, ctx) => ({
|
|
61
|
+
store: new CFCacheStore({ kv: env.CACHE_KV, ctx: ctx! }),
|
|
62
|
+
}),
|
|
63
|
+
// Where on-demand refreshes are written, and read before the build's payloads.
|
|
64
|
+
prerender: (env) => ({ store: createKVPrerenderStore(env.PRERENDER_KV) }),
|
|
65
|
+
}).routes(urlpatterns);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
On Node, `MemorySegmentCacheStore` and `createMemoryPrerenderStore()` (from
|
|
69
|
+
`@rangojs/router/prerender`) are the single-process stores. In dev, an
|
|
70
|
+
in-memory prerender store is used when the option is absent.
|
|
71
|
+
|
|
72
|
+
## urls.tsx
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
// src/urls.tsx
|
|
76
|
+
import { urls } from "@rangojs/router";
|
|
77
|
+
import { docsPatterns } from "./urls/docs";
|
|
78
|
+
import { blogPatterns } from "./urls/blog";
|
|
79
|
+
import { machinePatterns } from "./urls/machine";
|
|
80
|
+
import { HomePage, SiteLayout } from "./pages/site";
|
|
81
|
+
|
|
82
|
+
export const urlpatterns = urls(({ path, layout, include }) => [
|
|
83
|
+
layout(SiteLayout, () => [
|
|
84
|
+
path("/", HomePage, { name: "home", ppr: true }),
|
|
85
|
+
include("/docs", docsPatterns, { name: "docs" }),
|
|
86
|
+
include("/blog", blogPatterns, { name: "blog" }),
|
|
87
|
+
]),
|
|
88
|
+
// Outside the layout: no chrome. Flattened names: sitemap, robots.
|
|
89
|
+
include("/", machinePatterns, { name: "" }),
|
|
90
|
+
]);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## The docs section
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// src/pages/docs.tsx
|
|
97
|
+
import { notFound, Prerender, Static } from "@rangojs/router";
|
|
98
|
+
import { Link, Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
99
|
+
import { docs } from "../lib/content";
|
|
100
|
+
|
|
101
|
+
// Rendered once at build time and shared by every docs page.
|
|
102
|
+
export const DocsNav = Static(async (ctx) => {
|
|
103
|
+
const all = await docs.all();
|
|
104
|
+
return (
|
|
105
|
+
<ul>
|
|
106
|
+
{all.map((d) => (
|
|
107
|
+
<li key={d.slug}>
|
|
108
|
+
<Link to={ctx.reverse("docs.page", { slug: d.slug })}>{d.title}</Link>
|
|
109
|
+
</li>
|
|
110
|
+
))}
|
|
111
|
+
</ul>
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export function DocsLayout() {
|
|
116
|
+
return (
|
|
117
|
+
<div className="docs">
|
|
118
|
+
<aside>
|
|
119
|
+
<ParallelOutlet name="@nav" />
|
|
120
|
+
</aside>
|
|
121
|
+
<Outlet />
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Every page is known at build time: one payload per slug.
|
|
127
|
+
export const DocPage = Prerender<{ slug: string }>(
|
|
128
|
+
async () => (await docs.all()).map((d) => ({ slug: d.slug })),
|
|
129
|
+
async (ctx) => {
|
|
130
|
+
const doc = await docs.get(ctx.params.slug);
|
|
131
|
+
if (!doc) notFound();
|
|
132
|
+
return <article>{doc.body}</article>;
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
export const DocsIndex = Prerender(async () => <h1>Docs</h1>);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// src/urls/docs.tsx
|
|
141
|
+
import { urls } from "@rangojs/router";
|
|
142
|
+
import { DocPage, DocsIndex, DocsLayout, DocsNav } from "../pages/docs";
|
|
143
|
+
|
|
144
|
+
export const docsPatterns = urls(({ path, layout, parallel }) => [
|
|
145
|
+
layout(DocsLayout, () => [
|
|
146
|
+
parallel({ "@nav": DocsNav }),
|
|
147
|
+
// Prerender + ppr: the build also bakes each page's HTML shell.
|
|
148
|
+
path("/", DocsIndex, { name: "index", ppr: true }),
|
|
149
|
+
path("/:slug", DocPage, { name: "page", ppr: true }),
|
|
150
|
+
]),
|
|
151
|
+
]);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- A slug that `getParams` did not list answers 404 in production: a plain
|
|
155
|
+
`Prerender` has no live handler left to render it.
|
|
156
|
+
- A content edit is a deploy: the new build bakes new payloads and shells,
|
|
157
|
+
and replaces the stored HTML of that router while keeping its cached data
|
|
158
|
+
(a Prerender payload is part of its router's document version).
|
|
159
|
+
- A build shell is keyed by pathname and serves requests without a query
|
|
160
|
+
string; a URL with search params is captured at runtime.
|
|
161
|
+
|
|
162
|
+
## The blog section
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
// src/pages/blog.tsx
|
|
166
|
+
import { notFound, Passthrough, Prerender } from "@rangojs/router";
|
|
167
|
+
import { cms, type Post } from "../lib/content";
|
|
168
|
+
|
|
169
|
+
function Article({ post }: { post: Post }) {
|
|
170
|
+
return <article>{post.body}</article>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The newest published posts bake at build. A refresh re-renders one post
|
|
174
|
+
// and serves that to everyone; ttl only decides when it counts as stale.
|
|
175
|
+
export const BlogPostDef = Prerender<{ slug: string }>(
|
|
176
|
+
async () => (await cms.posts({ limit: 50 })).map((p) => ({ slug: p.slug })),
|
|
177
|
+
async (ctx) => {
|
|
178
|
+
const post = await cms.post(ctx.params.slug);
|
|
179
|
+
// A refresh that hits notFound() removes the page: 404 from then on.
|
|
180
|
+
if (!post || post.draft) notFound();
|
|
181
|
+
return <Article post={post} />;
|
|
182
|
+
},
|
|
183
|
+
{ onDemand: { ttl: 3600, tags: ({ params }) => [`post:${params.slug}`] } },
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Older posts, and new ones nothing has refreshed yet, render live.
|
|
187
|
+
export const BlogPost = Passthrough(BlogPostDef, async (ctx) => {
|
|
188
|
+
const post = await cms.post(ctx.params.slug);
|
|
189
|
+
if (!post || post.draft) notFound();
|
|
190
|
+
return <Article post={post} />;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
export const BlogIndex = Prerender(
|
|
194
|
+
async () => {
|
|
195
|
+
const posts = await cms.posts({ limit: 20 });
|
|
196
|
+
return (
|
|
197
|
+
<ul>
|
|
198
|
+
{posts.map((p) => (
|
|
199
|
+
<li key={p.slug}>{p.title}</li>
|
|
200
|
+
))}
|
|
201
|
+
</ul>
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
{ onDemand: { tags: ["blog-index"] } },
|
|
205
|
+
);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
// src/urls/blog.tsx
|
|
210
|
+
import { urls } from "@rangojs/router";
|
|
211
|
+
import { BlogIndex, BlogPost } from "../pages/blog";
|
|
212
|
+
|
|
213
|
+
// onDemand and ppr cannot be combined (the route definition throws):
|
|
214
|
+
// these routes take no ppr option.
|
|
215
|
+
export const blogPatterns = urls(({ path }) => [
|
|
216
|
+
path("/", BlogIndex, { name: "index" }),
|
|
217
|
+
path("/:slug", BlogPost, { name: "post" }),
|
|
218
|
+
]);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
| Request for | Served from |
|
|
222
|
+
| -------------------------- | -------------------------------------------------------------------------------- |
|
|
223
|
+
| A post the build baked | its build payload, until a refresh stores a newer one |
|
|
224
|
+
| A refreshed post | the refreshed entry, read before the build payload |
|
|
225
|
+
| Any other post | the `Passthrough` live handler |
|
|
226
|
+
| A draft | not listed by `getParams`; the live handler answers 404 |
|
|
227
|
+
| A post moved back to draft | its refresh hits `notFound()` and stores a removal; the live handler answers 404 |
|
|
228
|
+
|
|
229
|
+
`throw new Skip()` leaves one entry out of a build without failing it, but
|
|
230
|
+
in a refresh it reports `render-failed` and keeps the previous entry: a
|
|
231
|
+
published post that went back to draft would keep serving. Use `notFound()`
|
|
232
|
+
for content that should disappear, or `prerender.remove(url)`.
|
|
233
|
+
`updateTag()` does not reach prerendered payloads; a refresh does
|
|
234
|
+
(`/prerender` → "On-demand refresh (ISR)").
|
|
235
|
+
|
|
236
|
+
## The publish webhook
|
|
237
|
+
|
|
238
|
+
The webhook needs the router, so it lives beside the entry and not in the
|
|
239
|
+
route tree: a route module that imports `router.tsx` is an import cycle
|
|
240
|
+
([`./SKILL.md`](./SKILL.md#the-rules-every-recipe-follows)).
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// src/publish.ts
|
|
244
|
+
import type { AppBindings } from "./env";
|
|
245
|
+
import { router } from "./router";
|
|
246
|
+
|
|
247
|
+
export async function cmsPublish(
|
|
248
|
+
request: Request,
|
|
249
|
+
env: AppBindings,
|
|
250
|
+
ctx: ExecutionContext,
|
|
251
|
+
): Promise<Response> {
|
|
252
|
+
if (request.headers.get("x-cms-secret") !== env.CMS_WEBHOOK_SECRET) {
|
|
253
|
+
return new Response("Forbidden", { status: 403 });
|
|
254
|
+
}
|
|
255
|
+
const { slug } = (await request.json()) as { slug: string };
|
|
256
|
+
const prerender = router.prerender({ env, ctx });
|
|
257
|
+
const [post, index] = await prerender.many([`/blog/${slug}`, "/blog"]);
|
|
258
|
+
return Response.json({ post: post?.status, index: index?.status });
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
// src/worker.rsc.tsx
|
|
264
|
+
import type { AppBindings } from "./env";
|
|
265
|
+
import { router } from "./router";
|
|
266
|
+
import { cmsPublish } from "./publish";
|
|
267
|
+
|
|
268
|
+
export default {
|
|
269
|
+
fetch(request, env, ctx) {
|
|
270
|
+
const { pathname } = new URL(request.url);
|
|
271
|
+
if (request.method === "POST" && pathname === "/hooks/cms-publish") {
|
|
272
|
+
return cmsPublish(request, env, ctx);
|
|
273
|
+
}
|
|
274
|
+
return router.fetch(request, { env, ctx });
|
|
275
|
+
},
|
|
276
|
+
} satisfies ExportedHandler<AppBindings>;
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
A refresh renders with route params and env only, no request, and returns a
|
|
280
|
+
status (`rendered`, `removed` when the render hit `notFound()`,
|
|
281
|
+
`render-failed`, …); every failure keeps the previous entry. A deleted post
|
|
282
|
+
is `prerender.remove("/blog/<slug>")`. Thousands of pages belong in a queue,
|
|
283
|
+
one batch per message, and `onRevalidate` in the `prerender` option can
|
|
284
|
+
schedule a refresh when a request finds a stale entry.
|
|
285
|
+
|
|
286
|
+
## Machine routes
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
// src/urls/machine.tsx
|
|
290
|
+
import { urls } from "@rangojs/router";
|
|
291
|
+
import { docs } from "../lib/content";
|
|
292
|
+
|
|
293
|
+
export const machinePatterns = urls(({ path }) => [
|
|
294
|
+
path.xml(
|
|
295
|
+
"/sitemap.xml",
|
|
296
|
+
async (ctx) => {
|
|
297
|
+
const pages = await docs.all();
|
|
298
|
+
const locs = pages.map(
|
|
299
|
+
(d) =>
|
|
300
|
+
`<url><loc>${ctx.url.origin}${ctx.reverse("docs.page", { slug: d.slug })}</loc></url>`,
|
|
301
|
+
);
|
|
302
|
+
return `<?xml version="1.0"?><urlset>${locs.join("")}</urlset>`;
|
|
303
|
+
},
|
|
304
|
+
{ name: "sitemap" },
|
|
305
|
+
),
|
|
306
|
+
path.text("/robots.txt", () => "User-agent: *\nAllow: /\n", {
|
|
307
|
+
name: "robots",
|
|
308
|
+
}),
|
|
309
|
+
]);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Response routes skip the RSC pipeline; add `cache({ ttl })` as a use item to
|
|
313
|
+
store the whole response (`/response-routes`).
|
|
314
|
+
|
|
315
|
+
## Locales
|
|
316
|
+
|
|
317
|
+
Put the locale in the URL, as an optional, constrained mount param:
|
|
318
|
+
`include("/:locale(en|de)?", docsPatterns, { name: "docs" })`. A
|
|
319
|
+
prerendered page has no request to read a cookie or `Accept-Language` from,
|
|
320
|
+
and a `ppr` capture refuses a handler that reads them, so on a content site
|
|
321
|
+
the URL is the only locale signal those pages can use. Resolution chains and
|
|
322
|
+
message loading are in `/i18n`.
|
|
323
|
+
|
|
324
|
+
## Why
|
|
325
|
+
|
|
326
|
+
| Choice | Why |
|
|
327
|
+
| --------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
328
|
+
| `Prerender` for docs | Content known at build; handler and its file reads leave the bundle |
|
|
329
|
+
| `ppr` on docs pages | The build bakes the HTML too: the first document request is a shell HIT |
|
|
330
|
+
| `Static` nav in a parallel slot | Rendered once for every page instead of once per page |
|
|
331
|
+
| `onDemand` + `Passthrough` for the blog | Published posts serve from stored payloads; a refresh needs no deploy; the long tail renders live |
|
|
332
|
+
| The webhook beside the entry | It needs the router; the route tree must not import it |
|
|
333
|
+
| `{ name: "" }` for machine routes | Short, unique names (`sitemap`), no chrome |
|