@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
@@ -0,0 +1,435 @@
1
+ # A shop
2
+
3
+ A storefront: category pages with filters, product pages, a cart badge on
4
+ every page, a cart page and a checkout. Most of each page is the same for
5
+ every visitor (the product, the category grid); a few regions are per
6
+ visitor or change by the minute (the cart, the price, the stock).
7
+
8
+ - **Category and product pages are server routes with `ppr`.** The handler
9
+ renders the shared part from a tagged `"use cache"` read, and it bakes into
10
+ the shell; the per-visitor parts are live loaders read under their own
11
+ `<Suspense>`, the holes.
12
+ - **The cart is a loader on the store layout**, re-run after cart actions
13
+ only.
14
+ - **Checkout is server-rendered with actions**: no shell, no cache, a guard
15
+ that needs a cart.
16
+ - **Filters are search params** with a typed schema; each distinct query is
17
+ its own shell.
18
+
19
+ ## File tree
20
+
21
+ ```
22
+ src/
23
+ router.tsx cache store; tracking params out of the keys; loadCart
24
+ router.named-routes.gen.ts generated, committed
25
+ urls.tsx root: store layout, catalog, cart, checkout
26
+ urls/
27
+ catalog.tsx urls(): category and product pages (ppr)
28
+ checkout.tsx urls(): checkout steps (no ppr)
29
+ pages/store.tsx StoreLayout, CategoryPage, ProductPage
30
+ pages/checkout.tsx "use client": CartPage, CheckoutPage
31
+ components/
32
+ cart-badge.tsx "use client": useLoader(CartLoader)
33
+ buy-box.tsx "use client": price, stock, reviews, add to cart
34
+ loaders/shop.ts CartLoader, PriceLoader, StockLoader, ReviewsLoader
35
+ lib/data.ts getCategory, getProduct: "use cache" + cacheTag
36
+ lib/catalog.ts lib/carts.ts lib/payments.ts your data layer
37
+ middleware/cart.ts loadCart (global), requireCart
38
+ actions/cart.ts "use server": addToCart
39
+ actions/checkout.ts "use server": placeOrder
40
+ ```
41
+
42
+ ## router.tsx
43
+
44
+ ```tsx
45
+ // src/router.tsx
46
+ import { createRouter, TRACKING_SEARCH_PARAMS } from "@rangojs/router";
47
+ import { MemorySegmentCacheStore } from "@rangojs/router/cache";
48
+ import { loadCart } from "./middleware/cart";
49
+ import { urlpatterns } from "./urls";
50
+
51
+ export const router = createRouter({
52
+ cache: {
53
+ store: new MemorySegmentCacheStore(), // CFCacheStore / VercelCacheStore when deployed
54
+ // utm_* and click ids mint no new shells or cache entries.
55
+ searchParams: { exclude: TRACKING_SEARCH_PARAMS },
56
+ },
57
+ })
58
+ .use(loadCart)
59
+ .routes(urlpatterns);
60
+ ```
61
+
62
+ ```ts
63
+ // src/middleware/cart.ts
64
+ import { cookies, createVar, redirect } from "@rangojs/router";
65
+ import type { Middleware } from "@rangojs/router";
66
+ import { carts } from "../lib/carts";
67
+
68
+ export const CartId = createVar<string>({ cache: false });
69
+
70
+ export const loadCart: Middleware = async (ctx, next) => {
71
+ const id = cookies().get("cart")?.value;
72
+ if (id) ctx.set(CartId, id);
73
+ return next();
74
+ };
75
+
76
+ export const requireCart: Middleware = async (ctx, next) => {
77
+ const id = ctx.get(CartId);
78
+ if (!id || (await carts.get(id)).length === 0)
79
+ return redirect(ctx.reverse("cart"));
80
+ return next();
81
+ };
82
+ ```
83
+
84
+ ## urls.tsx
85
+
86
+ ```tsx
87
+ // src/urls.tsx
88
+ import { urls } from "@rangojs/router";
89
+ import { catalogPatterns } from "./urls/catalog";
90
+ import { checkoutPatterns } from "./urls/checkout";
91
+ import { StoreLayout } from "./pages/store";
92
+ import { CartPage } from "./pages/checkout";
93
+ import { CartLoader } from "./loaders/shop";
94
+ import * as CartActions from "./actions/cart";
95
+ import { requireCart } from "./middleware/cart";
96
+
97
+ export const urlpatterns = urls(
98
+ ({ path, layout, include, loader, revalidate, middleware }) => [
99
+ layout(StoreLayout, () => [
100
+ // The cart badge on every page. A layout loader re-runs after every
101
+ // action by default: narrow it to cart actions. Navigations hold it.
102
+ loader(CartLoader, () => [
103
+ revalidate((ctx) =>
104
+ ctx.isAction() ? ctx.isAction(CartActions) : undefined,
105
+ ),
106
+ ]),
107
+ include("/", catalogPatterns, { name: "catalog" }),
108
+ path("/cart", CartPage, { name: "cart" }),
109
+ middleware(requireCart, () => [
110
+ include("/checkout", checkoutPatterns, { name: "checkout" }),
111
+ ]),
112
+ ]),
113
+ ],
114
+ );
115
+ ```
116
+
117
+ ## The catalog
118
+
119
+ ```ts
120
+ // src/lib/data.ts
121
+ import { cacheTag } from "@rangojs/router";
122
+ import { catalog } from "./catalog";
123
+
124
+ // Shared by every visitor and tagged: a catalog edit drops the entry and
125
+ // every shell that baked it.
126
+ export async function getCategory(
127
+ category: string,
128
+ sort?: string,
129
+ page?: number,
130
+ ) {
131
+ "use cache";
132
+ cacheTag(`category:${category}`);
133
+ return catalog.list(category, { sort, page });
134
+ }
135
+
136
+ export async function getProduct(slug: string) {
137
+ "use cache";
138
+ cacheTag(`product:${slug}`);
139
+ return catalog.get(slug);
140
+ }
141
+ ```
142
+
143
+ ```tsx
144
+ // src/pages/store.tsx
145
+ import { Suspense } from "react";
146
+ import { notFound, type Handler } from "@rangojs/router";
147
+ import { Link, Outlet } from "@rangojs/router/client";
148
+ import { CartBadge } from "../components/cart-badge";
149
+ import { BuyBox } from "../components/buy-box";
150
+ import { getCategory, getProduct } from "../lib/data";
151
+
152
+ export function StoreLayout() {
153
+ return (
154
+ <>
155
+ <header>
156
+ {/* A badge-sized hole: the header itself bakes into every shell. */}
157
+ <Suspense fallback={<span>Cart</span>}>
158
+ <CartBadge />
159
+ </Suspense>
160
+ </header>
161
+ <Outlet />
162
+ </>
163
+ );
164
+ }
165
+
166
+ // Typed from the generated map: ctx.params.category, ctx.search.sort.
167
+ export const CategoryPage: Handler<"catalog.category"> = async (ctx) => {
168
+ const products = await getCategory(
169
+ ctx.params.category,
170
+ ctx.search.sort,
171
+ ctx.search.page,
172
+ );
173
+ return (
174
+ <ul>
175
+ {products.map((p) => (
176
+ <li key={p.slug}>
177
+ <Link to={ctx.reverse("catalog.product", { slug: p.slug })}>
178
+ {p.name}
179
+ </Link>
180
+ </li>
181
+ ))}
182
+ </ul>
183
+ );
184
+ };
185
+
186
+ export const ProductPage: Handler<"catalog.product"> = async (ctx) => {
187
+ const product = await getProduct(ctx.params.slug);
188
+ if (!product) notFound();
189
+ return (
190
+ <article>
191
+ <h1>{product.name}</h1>
192
+ <BuyBox slug={ctx.params.slug} />
193
+ </article>
194
+ );
195
+ };
196
+ ```
197
+
198
+ ```tsx
199
+ // src/urls/catalog.tsx
200
+ import { urls, type Revalidate } from "@rangojs/router";
201
+ import { CategoryPage, ProductPage } from "../pages/store";
202
+ import { PriceLoader, ReviewsLoader, StockLoader } from "../loaders/shop";
203
+
204
+ // A route and its loaders re-run after every action by default. The product
205
+ // itself, its price and its reviews do not change with the cart; stock may.
206
+ const notOnActions: Revalidate = (ctx) => (ctx.isAction() ? false : undefined);
207
+
208
+ export const catalogPatterns = urls(({ path, loader, revalidate }) => [
209
+ path("/c/:category", CategoryPage, {
210
+ name: "category",
211
+ search: { sort: "string?", page: "number?" },
212
+ ppr: true,
213
+ }),
214
+ path("/p/:slug", ProductPage, { name: "product", ppr: { ttl: 3600 } }, () => [
215
+ revalidate(notOnActions), // the handler
216
+ loader(PriceLoader, () => [revalidate(notOnActions)]),
217
+ loader(StockLoader),
218
+ // Below the fold and costly: kept out of link prefetches.
219
+ loader(ReviewsLoader, { prefetch: false }, () => [
220
+ revalidate(notOnActions),
221
+ ]),
222
+ ]),
223
+ ]);
224
+ ```
225
+
226
+ ```tsx
227
+ // src/components/buy-box.tsx
228
+ "use client";
229
+
230
+ import { Suspense } from "react";
231
+ import { useLoader } from "@rangojs/router/client";
232
+ import { PriceLoader, ReviewsLoader, StockLoader } from "../loaders/shop";
233
+ import { addToCart } from "../actions/cart";
234
+
235
+ function Price() {
236
+ const { data } = useLoader(PriceLoader);
237
+ return <p>{(data.cents / 100).toFixed(2)}</p>;
238
+ }
239
+ function Stock() {
240
+ const { data } = useLoader(StockLoader);
241
+ return <p>{data.count} left</p>;
242
+ }
243
+ function Reviews() {
244
+ const { data } = useLoader(ReviewsLoader);
245
+ return (
246
+ <ul>
247
+ {data.map((r) => (
248
+ <li key={r.id}>{r.text}</li>
249
+ ))}
250
+ </ul>
251
+ );
252
+ }
253
+
254
+ // Each read under its own boundary: these are the shell's live holes.
255
+ export function BuyBox({ slug }: { slug: string }) {
256
+ return (
257
+ <>
258
+ <Suspense fallback={<p>…</p>}>
259
+ <Price />
260
+ <Stock />
261
+ </Suspense>
262
+ <button onClick={() => addToCart(slug)}>Add to cart</button>
263
+ <Suspense fallback={<p>Loading reviews…</p>}>
264
+ <Reviews />
265
+ </Suspense>
266
+ </>
267
+ );
268
+ }
269
+ ```
270
+
271
+ What a product page request does:
272
+
273
+ | Part | Where it comes from | On a shell HIT |
274
+ | ------------------------- | ------------------------------------------------------ | ---------------------------------------------------- |
275
+ | Name, description, images | the handler, from `getProduct` (`"use cache"`, tagged) | from the shell; the handler does not run |
276
+ | Price, stock | live loaders under `<Suspense>` | run for this request, stream into the holes |
277
+ | Reviews | a live loader, kept out of prefetches | run for this request; a prefetch defers it to a fill |
278
+ | Cart badge | the layout's `CartLoader` | run for this request |
279
+
280
+ - Filters link with the typed search:
281
+ `ctx.reverse("catalog.category", { category }, { sort: "price" })`. Every
282
+ distinct query is its own shell, captured on its first request; keep the
283
+ filter set small or move the grid into a live loader when combinations
284
+ explode.
285
+ - A catalog edit is a tag: `updateTag()` with `product:<slug>` drops the
286
+ `"use cache"` entry and every shell that baked it; the next request
287
+ recaptures. A webhook route has the request context `updateTag()` needs:
288
+
289
+ ```ts
290
+ path.json(
291
+ "/hooks/catalog",
292
+ async (ctx) => {
293
+ // verify the sender's signature first
294
+ const { slug } = (await ctx.request.json()) as { slug: string };
295
+ await updateTag(`product:${slug}`);
296
+ return { ok: true };
297
+ },
298
+ { name: "catalogHook" },
299
+ );
300
+ ```
301
+
302
+ - Every per-visitor read lives in a loader the client reads. A `cookies()`
303
+ read in a handler or in a loader a handler awaits refuses the shell
304
+ capture, and the page never serves a shell (`/ppr` → "The
305
+ layout-with-loaders playbook").
306
+
307
+ ### Variant: product pages prerendered
308
+
309
+ For a catalog known at build time, the product page can be
310
+ `Prerender(getParams, handler, { onDemand })` wrapped in `Passthrough()`: the
311
+ build bakes the listed products, a CMS webhook refreshes one with
312
+ `router.prerender()`, and the live handler renders the long tail. `onDemand`
313
+ cannot be combined with `ppr`, so this variant stores the Flight payload but
314
+ no HTML shell; a plain `Prerender` (no `onDemand`) can take `ppr`, but then a
315
+ content change needs a deploy. The pattern, webhook included, is in
316
+ [`./content-site.md`](./content-site.md#the-blog-section).
317
+
318
+ ## The cart
319
+
320
+ ```ts
321
+ // src/loaders/shop.ts
322
+ import { createLoader } from "@rangojs/router";
323
+ import { carts } from "../lib/carts";
324
+ import { catalog } from "../lib/catalog";
325
+ import { CartId } from "../middleware/cart";
326
+
327
+ // Per visitor: live, never cached.
328
+ export const CartLoader = createLoader(async (ctx) => {
329
+ const id = ctx.get(CartId);
330
+ const items = id ? await carts.get(id) : [];
331
+ return { count: items.length, items };
332
+ });
333
+
334
+ // Change by the minute: live on every request, shell HITs included.
335
+ export const PriceLoader = createLoader(async (ctx) =>
336
+ catalog.price(ctx.params.slug!),
337
+ );
338
+ export const StockLoader = createLoader(async (ctx) =>
339
+ catalog.stock(ctx.params.slug!),
340
+ );
341
+ export const ReviewsLoader = createLoader(async (ctx) =>
342
+ catalog.reviews(ctx.params.slug!),
343
+ );
344
+ ```
345
+
346
+ ```ts
347
+ // src/actions/cart.ts
348
+ "use server";
349
+
350
+ import { cookies, getRequestContext } from "@rangojs/router";
351
+ import { carts } from "../lib/carts";
352
+ import { CartId } from "../middleware/cart";
353
+
354
+ export async function addToCart(slug: string) {
355
+ const ctx = getRequestContext();
356
+ let id = ctx.get(CartId);
357
+ if (!id) {
358
+ id = crypto.randomUUID();
359
+ cookies().set("cart", id, { httpOnly: true, path: "/" });
360
+ // loadCart ran before the action; the revalidation render reads this.
361
+ ctx.set(CartId, id);
362
+ }
363
+ await carts.add(id, slug);
364
+ return { ok: true as const };
365
+ }
366
+ ```
367
+
368
+ One add-to-cart from a product page re-runs `CartLoader` (its
369
+ `revalidate()` names cart actions) and `StockLoader` (the default), and holds
370
+ `PriceLoader`, `ReviewsLoader` and the product handler (`notOnActions`); the
371
+ store layout is skipped after actions by default. Each decision is per
372
+ segment, in the action's own response. Without the route's own
373
+ `revalidate()`, the handler would re-run on every add-to-cart.
374
+
375
+ ### Variant: the shop as a `clientUrls()` group
376
+
377
+ When shoppers mostly move inside one layout (filters, tabs, product to
378
+ product) and the pages render entirely from loaders, mount the shop as a
379
+ `clientUrls()` group, `include("/shop", shopUrls, { name: "shop" })`:
380
+ navigations inside it render at the click, and each loader's `revalidate()`
381
+ holds its data across them. A group has no handlers, so a `ppr` group route
382
+ holds its client components' HTML and its `ssr: false` loaders' data, and
383
+ every other loader is a hole; handler output baked from a tagged
384
+ `"use cache"` read, as in the catalog above, needs `urls()` routes. The router
385
+ repository's `tests/vite-rsc-demo/src/urls/client-shop.client.tsx` is a full
386
+ example (index, product, a cart badge, param-sensitive predicates). A cart
387
+ loader on the group's `layout()` is held across the group's routes, as on a
388
+ server layout. Register it on a server layout above the include instead when
389
+ the badge must also survive entering and leaving the group.
390
+
391
+ ## Checkout
392
+
393
+ ```tsx
394
+ // src/urls/checkout.tsx
395
+ import { urls } from "@rangojs/router";
396
+ import { CheckoutPage } from "../pages/checkout";
397
+
398
+ // Per visitor and mutating: no ppr, no cache().
399
+ export const checkoutPatterns = urls(({ path }) => [
400
+ path("/", CheckoutPage, { name: "start" }),
401
+ ]);
402
+ ```
403
+
404
+ ```ts
405
+ // src/actions/checkout.ts
406
+ "use server";
407
+
408
+ import { redirect } from "@rangojs/router";
409
+ import { payments } from "../lib/payments";
410
+
411
+ export async function placeOrder(_prev: unknown, formData: FormData) {
412
+ const email = String(formData.get("email") ?? "");
413
+ if (!email.includes("@")) return { error: "Enter an email address" }; // shown by useActionState
414
+ const session = await payments.createSession();
415
+ // Off-site redirects are refused unless you opt in.
416
+ throw redirect(session.url, { external: true });
417
+ }
418
+ ```
419
+
420
+ `requireCart` wraps the checkout's renders, not `placeOrder`: an action
421
+ checks what it needs itself (`/server-actions` → "Authorization in actions").
422
+ A form whose `action` is the dispatcher `useActionState` returns posts
423
+ without JavaScript too (`/server-actions` → "Progressive Enhancement").
424
+
425
+ ## Why
426
+
427
+ | Choice | Why |
428
+ | --------------------------------------------- | -------------------------------------------------------------------------- |
429
+ | `ppr` on category and product pages | The shared part is HTML from the shell; only the holes render per request |
430
+ | `"use cache"` + `cacheTag()` for product data | One read for every visitor, and the tag reaches the shells that baked it |
431
+ | Live loaders for price, stock, cart | Fresh on every request, shell HITs included |
432
+ | `CartLoader` on the store layout | One badge on every page; held on navigations; narrowed to cart actions |
433
+ | `TRACKING_SEARCH_PARAMS` excluded | Campaign links do not multiply shells |
434
+ | `prefetch: false` on reviews | Every product card prefetches its page; few shoppers scroll to the reviews |
435
+ | Checkout without `ppr` or `cache()` | Nothing on it is shared |