@rangojs/router 0.24.4 → 0.26.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.
Files changed (301) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +0 -2
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
  22. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  23. package/dist/types/cache/handle-snapshot.d.ts +2 -2
  24. package/dist/types/cache/types.d.ts +16 -6
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
  26. package/dist/types/client-urls/client-urls.d.ts +6 -2
  27. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  28. package/dist/types/client-urls/server-projection.d.ts +9 -5
  29. package/dist/types/client-urls/types.d.ts +29 -16
  30. package/dist/types/client.d.ts +2 -2
  31. package/dist/types/client.rsc.d.ts +2 -2
  32. package/dist/types/decode-loader-results.d.ts +28 -7
  33. package/dist/types/handles/deferred-resolution.d.ts +3 -2
  34. package/dist/types/handles/meta.d.ts +5 -0
  35. package/dist/types/href-client.d.ts +3 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/loader-redirect.d.ts +2 -3
  38. package/dist/types/outlet-context.d.ts +7 -6
  39. package/dist/types/prefetch-markers.d.ts +14 -5
  40. package/dist/types/prerender/build-shell-capture.d.ts +1 -2
  41. package/dist/types/prerender/writable-store.d.ts +2 -3
  42. package/dist/types/route-content-wrapper.d.ts +17 -14
  43. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  44. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  45. package/dist/types/route-definition/rewrite.d.ts +48 -5
  46. package/dist/types/route-types.d.ts +2 -2
  47. package/dist/types/router/handler-context.d.ts +14 -2
  48. package/dist/types/router/intercept-resolution.d.ts +19 -46
  49. package/dist/types/router/lazy-includes.d.ts +7 -0
  50. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  51. package/dist/types/router/middleware.d.ts +0 -16
  52. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  53. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  54. package/dist/types/router/revalidation.d.ts +8 -1
  55. package/dist/types/router/router-context.d.ts +4 -16
  56. package/dist/types/router/router-interfaces.d.ts +0 -11
  57. package/dist/types/router/router-options.d.ts +4 -27
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
  59. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  60. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  61. package/dist/types/router/segment-resolution.d.ts +1 -1
  62. package/dist/types/router/segment-wrappers.d.ts +3 -5
  63. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  64. package/dist/types/router/tracing.d.ts +2 -2
  65. package/dist/types/router-versions.d.ts +8 -6
  66. package/dist/types/rsc/full-payload.d.ts +14 -2
  67. package/dist/types/rsc/helpers.d.ts +8 -0
  68. package/dist/types/rsc/manifest-init.d.ts +20 -0
  69. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  70. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  71. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  72. package/dist/types/rsc/server-action.d.ts +6 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +14 -4
  74. package/dist/types/rsc/shell-capture.d.ts +23 -2
  75. package/dist/types/rsc/types.d.ts +5 -8
  76. package/dist/types/segment-system.d.ts +15 -0
  77. package/dist/types/server/build-version-table.d.ts +10 -13
  78. package/dist/types/server/context.d.ts +90 -6
  79. package/dist/types/server/handle-store.d.ts +17 -6
  80. package/dist/types/server/request-context.d.ts +45 -7
  81. package/dist/types/server/request-headers.d.ts +12 -0
  82. package/dist/types/server/rewrite-trail.d.ts +8 -1
  83. package/dist/types/testing/build-versions.d.ts +0 -3
  84. package/dist/types/testing/e2e/index.d.ts +1 -1
  85. package/dist/types/testing/flight.entry.d.ts +1 -1
  86. package/dist/types/testing/index.d.ts +1 -1
  87. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  88. package/dist/types/testing/internal/seed-vars.d.ts +4 -4
  89. package/dist/types/testing/render-route.d.ts +21 -11
  90. package/dist/types/testing/run-loader.d.ts +7 -0
  91. package/dist/types/testing/run-middleware.d.ts +7 -0
  92. package/dist/types/testing/run-revalidate.d.ts +5 -4
  93. package/dist/types/testing/serve-shell-request.d.ts +42 -5
  94. package/dist/types/testing/shell-status.d.ts +17 -0
  95. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  96. package/dist/types/testing/vitest.d.ts +10 -1
  97. package/dist/types/thenable-status.d.ts +0 -6
  98. package/dist/types/types/handler-context.d.ts +12 -1
  99. package/dist/types/types/loader-types.d.ts +10 -4
  100. package/dist/types/types/route-entry.d.ts +2 -1
  101. package/dist/types/urls/path-helper-types.d.ts +4 -2
  102. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  103. package/dist/types/vite/discovery/state.d.ts +20 -0
  104. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  105. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  106. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  107. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  108. package/dist/vite/index.js +6819 -6555
  109. package/package.json +1 -1
  110. package/skills/app-structure/SKILL.md +255 -0
  111. package/skills/app-structure/content-site.md +333 -0
  112. package/skills/app-structure/e-commerce.md +435 -0
  113. package/skills/app-structure/large-app.md +885 -0
  114. package/skills/app-structure/multi-app.md +280 -0
  115. package/skills/breadcrumbs/SKILL.md +6 -1
  116. package/skills/cache-guide/SKILL.md +4 -8
  117. package/skills/caching/SKILL.md +6 -6
  118. package/skills/catalog.json +12 -0
  119. package/skills/client-urls/SKILL.md +185 -32
  120. package/skills/comparison/references/framework-comparison.md +1 -1
  121. package/skills/composability/SKILL.md +10 -8
  122. package/skills/handler-use/SKILL.md +48 -9
  123. package/skills/hooks/handle-and-actions.md +104 -0
  124. package/skills/hooks/navigation.md +24 -10
  125. package/skills/hooks/urls.md +1 -1
  126. package/skills/host-router/SKILL.md +1 -1
  127. package/skills/i18n/SKILL.md +2 -1
  128. package/skills/intercept/SKILL.md +60 -29
  129. package/skills/layout/SKILL.md +6 -3
  130. package/skills/links/SKILL.md +151 -17
  131. package/skills/loader/SKILL.md +42 -19
  132. package/skills/middleware/SKILL.md +111 -15
  133. package/skills/observability/SKILL.md +11 -0
  134. package/skills/parallel/SKILL.md +5 -5
  135. package/skills/ppr/SKILL.md +103 -52
  136. package/skills/prerender/SKILL.md +4 -5
  137. package/skills/rango/SKILL.md +3 -0
  138. package/skills/response-routes/SKILL.md +4 -1
  139. package/skills/router-setup/SKILL.md +0 -2
  140. package/skills/scripts/SKILL.md +12 -1
  141. package/skills/server-actions/SKILL.md +60 -20
  142. package/skills/testing/SKILL.md +1 -0
  143. package/skills/testing/client-components.md +13 -10
  144. package/skills/testing/loader.md +25 -24
  145. package/skills/testing/middleware.md +11 -11
  146. package/skills/testing/render-handler.md +17 -17
  147. package/skills/testing/response-routes.md +4 -1
  148. package/skills/testing/serve-shell-request.md +86 -22
  149. package/skills/testing/server-actions.md +16 -16
  150. package/skills/ux-architecture/SKILL.md +980 -0
  151. package/skills/vercel/SKILL.md +1 -1
  152. package/skills/view-transitions/SKILL.md +43 -10
  153. package/src/__augment-tests__/augmented.check.ts +28 -0
  154. package/src/browser/event-controller.ts +44 -18
  155. package/src/browser/intercept-utils.ts +3 -2
  156. package/src/browser/merge-segment-loaders.ts +0 -85
  157. package/src/browser/navigation-bridge.ts +112 -84
  158. package/src/browser/navigation-client.ts +20 -15
  159. package/src/browser/navigation-store.ts +4 -2
  160. package/src/browser/navigation-transaction.ts +9 -8
  161. package/src/browser/partial-update.ts +90 -32
  162. package/src/browser/prefetch/cache.ts +8 -0
  163. package/src/browser/prefetch/fetch.ts +13 -3
  164. package/src/browser/rango-marker.ts +13 -0
  165. package/src/browser/react/NavigationProvider.tsx +25 -7
  166. package/src/browser/react/filter-segment-order.ts +13 -0
  167. package/src/browser/react/use-action.ts +9 -15
  168. package/src/browser/react/use-handle.ts +63 -71
  169. package/src/browser/react/use-href.tsx +24 -5
  170. package/src/browser/react/use-link-status.ts +1 -1
  171. package/src/browser/react/use-navigation.ts +30 -40
  172. package/src/browser/react/use-router.ts +37 -6
  173. package/src/browser/react/use-segments.ts +8 -12
  174. package/src/browser/response-adapter.ts +11 -5
  175. package/src/browser/rsc-router.tsx +3 -1
  176. package/src/browser/scroll-restoration.ts +34 -20
  177. package/src/browser/segment-reconciler.ts +11 -29
  178. package/src/browser/server-action-bridge.ts +52 -16
  179. package/src/browser/types.ts +40 -8
  180. package/src/build/generate-manifest.ts +12 -1
  181. package/src/build/route-types/codegen.ts +6 -2
  182. package/src/build/route-types/per-module-writer.ts +94 -61
  183. package/src/cache/cache-runtime.ts +9 -0
  184. package/src/cache/cache-scope.ts +2 -1
  185. package/src/cache/cf/cf-cache-store.ts +6 -7
  186. package/src/cache/cf/cf-cache-types.ts +0 -17
  187. package/src/cache/cf/cf-shell-frame.ts +3 -0
  188. package/src/cache/handle-snapshot.ts +2 -2
  189. package/src/cache/types.ts +16 -6
  190. package/src/cache/vercel/vercel-cache-store.ts +8 -19
  191. package/src/client-urls/client-urls.ts +202 -153
  192. package/src/client-urls/revalidate-registry.ts +12 -3
  193. package/src/client-urls/server-projection.ts +137 -66
  194. package/src/client-urls/types.ts +30 -15
  195. package/src/client.rsc.tsx +11 -1
  196. package/src/client.tsx +9 -1
  197. package/src/decode-loader-results.ts +64 -31
  198. package/src/handles/deferred-resolution.ts +3 -2
  199. package/src/handles/meta.ts +5 -0
  200. package/src/href-client.ts +3 -1
  201. package/src/index.rsc.ts +1 -0
  202. package/src/index.ts +12 -4
  203. package/src/loader-redirect.tsx +2 -3
  204. package/src/outlet-context.ts +7 -6
  205. package/src/prefetch-markers.ts +15 -5
  206. package/src/prerender/build-shell-capture.ts +1 -2
  207. package/src/prerender/writable-store.ts +2 -3
  208. package/src/reverse.ts +3 -3
  209. package/src/route-content-wrapper.tsx +19 -17
  210. package/src/route-definition/dsl-helpers.ts +41 -21
  211. package/src/route-definition/helpers-types.ts +7 -2
  212. package/src/route-definition/resolve-handler-use.ts +23 -10
  213. package/src/route-definition/rewrite.ts +86 -19
  214. package/src/route-types.ts +1 -2
  215. package/src/router/find-match.ts +37 -1
  216. package/src/router/handler-context.ts +85 -40
  217. package/src/router/intercept-resolution.ts +161 -307
  218. package/src/router/lazy-includes.ts +43 -27
  219. package/src/router/loader-resolution.ts +21 -1
  220. package/src/router/manifest.ts +15 -10
  221. package/src/router/match-api.ts +11 -1
  222. package/src/router/match-middleware/background-revalidation.ts +2 -4
  223. package/src/router/match-middleware/cache-lookup.ts +16 -20
  224. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  225. package/src/router/match-result.ts +31 -2
  226. package/src/router/middleware.ts +6 -147
  227. package/src/router/navigation-snapshot.ts +38 -2
  228. package/src/router/prefetch-cache-ttl.ts +10 -18
  229. package/src/router/prerender-match.ts +2 -1
  230. package/src/router/revalidation.ts +24 -3
  231. package/src/router/router-context.ts +5 -18
  232. package/src/router/router-interfaces.ts +0 -13
  233. package/src/router/router-options.ts +4 -28
  234. package/src/router/segment-resolution/fresh.ts +6 -2
  235. package/src/router/segment-resolution/helpers.ts +7 -1
  236. package/src/router/segment-resolution/loader-cache.ts +4 -3
  237. package/src/router/segment-resolution/mask-nested.ts +5 -3
  238. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  239. package/src/router/segment-resolution/revalidation.ts +75 -35
  240. package/src/router/segment-resolution.ts +0 -1
  241. package/src/router/segment-wrappers.ts +8 -13
  242. package/src/router/substitute-pattern-params.ts +3 -2
  243. package/src/router/tracing.ts +2 -2
  244. package/src/router-versions.ts +7 -7
  245. package/src/router.ts +5 -17
  246. package/src/rsc/full-payload.ts +26 -4
  247. package/src/rsc/handler.ts +86 -48
  248. package/src/rsc/helpers.ts +13 -0
  249. package/src/rsc/manifest-init.ts +56 -3
  250. package/src/rsc/prefetch-render-error.ts +38 -0
  251. package/src/rsc/progressive-enhancement.ts +161 -211
  252. package/src/rsc/rewrite-passes.ts +56 -9
  253. package/src/rsc/rsc-rendering.ts +79 -23
  254. package/src/rsc/server-action.ts +25 -2
  255. package/src/rsc/shell-capture-constants.ts +26 -3
  256. package/src/rsc/shell-capture.ts +137 -51
  257. package/src/rsc/types.ts +5 -9
  258. package/src/segment-system.tsx +125 -119
  259. package/src/server/build-version-table.ts +12 -24
  260. package/src/server/context.ts +160 -7
  261. package/src/server/handle-store.ts +80 -12
  262. package/src/server/request-context.ts +46 -6
  263. package/src/server/request-headers.ts +32 -0
  264. package/src/server/rewrite-trail.ts +55 -9
  265. package/src/ssr/ssr-root.tsx +6 -10
  266. package/src/testing/build-versions.ts +0 -3
  267. package/src/testing/dispatch.ts +5 -8
  268. package/src/testing/e2e/index.ts +1 -0
  269. package/src/testing/flight.entry.ts +1 -0
  270. package/src/testing/index.ts +1 -0
  271. package/src/testing/internal/client-urls-projection.ts +31 -11
  272. package/src/testing/internal/seed-vars.ts +4 -4
  273. package/src/testing/render-route.tsx +83 -40
  274. package/src/testing/run-loader.ts +26 -2
  275. package/src/testing/run-middleware.ts +10 -0
  276. package/src/testing/run-revalidate.ts +13 -6
  277. package/src/testing/serve-shell-request.ts +79 -9
  278. package/src/testing/shell-status.ts +40 -0
  279. package/src/testing/vitest-stubs/version.ts +1 -0
  280. package/src/testing/vitest.ts +74 -7
  281. package/src/thenable-status.ts +0 -11
  282. package/src/types/handler-context.ts +12 -1
  283. package/src/types/loader-types.ts +10 -4
  284. package/src/types/route-entry.ts +2 -1
  285. package/src/urls/include-helper.ts +38 -11
  286. package/src/urls/path-helper-types.ts +4 -2
  287. package/src/vite/discovery/client-urls-projection.ts +205 -16
  288. package/src/vite/discovery/discover-routers.ts +2 -2
  289. package/src/vite/discovery/prerender-collection.ts +2 -6
  290. package/src/vite/discovery/shell-prerender-phase.ts +1 -5
  291. package/src/vite/discovery/state.ts +26 -0
  292. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  293. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  294. package/src/vite/plugins/hoist/source.ts +8 -7
  295. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  296. package/src/vite/plugins/version-plugin.ts +7 -0
  297. package/src/vite/plugins/version.d.ts +7 -0
  298. package/src/vite/plugins/virtual-entries.ts +20 -0
  299. package/src/vite/rango.ts +4 -1
  300. package/src/vite/router-discovery.ts +39 -7
  301. package/src/vite/utils/prerender-utils.ts +17 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.24.4",
3
+ "version": "0.26.0",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -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 |