@rangojs/router 0.0.0-experimental.2

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 (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. package/src/vite/virtual-entries.ts +109 -0
package/package.json ADDED
@@ -0,0 +1,140 @@
1
+ {
2
+ "name": "@rangojs/router",
3
+ "version": "0.0.0-experimental.2",
4
+ "type": "module",
5
+ "description": "Django-inspired RSC router with composable URL patterns",
6
+ "author": "Ivo Todorov",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ivogt/vite-rsc.git",
11
+ "directory": "packages/rangojs-router"
12
+ },
13
+ "homepage": "https://github.com/ivogt/vite-rsc#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/ivogt/vite-rsc/issues"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "tag": "experimental"
20
+ },
21
+ "keywords": [
22
+ "react",
23
+ "rsc",
24
+ "react-server-components",
25
+ "router",
26
+ "vite"
27
+ ],
28
+ "exports": {
29
+ ".": {
30
+ "react-server": "./src/index.rsc.ts",
31
+ "types": "./src/index.ts",
32
+ "default": "./src/index.ts"
33
+ },
34
+ "./server": {
35
+ "types": "./src/server.ts",
36
+ "import": "./src/server.ts"
37
+ },
38
+ "./client": {
39
+ "react-server": "./src/client.rsc.tsx",
40
+ "types": "./src/client.tsx",
41
+ "default": "./src/client.tsx"
42
+ },
43
+ "./browser": {
44
+ "types": "./src/browser/index.ts",
45
+ "default": "./src/browser/index.ts"
46
+ },
47
+ "./ssr": {
48
+ "types": "./src/ssr/index.tsx",
49
+ "default": "./src/ssr/index.tsx"
50
+ },
51
+ "./rsc": {
52
+ "react-server": "./src/rsc/index.ts",
53
+ "types": "./src/rsc/index.ts",
54
+ "default": "./src/rsc/index.ts"
55
+ },
56
+ "./vite": {
57
+ "types": "./src/vite/index.ts",
58
+ "import": "./dist/vite/index.js"
59
+ },
60
+ "./types": {
61
+ "types": "./src/vite/version.d.ts"
62
+ },
63
+ "./internal/deps/browser": {
64
+ "types": "./src/deps/browser.ts",
65
+ "default": "./src/deps/browser.ts"
66
+ },
67
+ "./internal/deps/ssr": {
68
+ "types": "./src/deps/ssr.ts",
69
+ "default": "./src/deps/ssr.ts"
70
+ },
71
+ "./internal/deps/rsc": {
72
+ "react-server": "./src/deps/rsc.ts",
73
+ "types": "./src/deps/rsc.ts",
74
+ "default": "./src/deps/rsc.ts"
75
+ },
76
+ "./internal/deps/html-stream-client": {
77
+ "types": "./src/deps/html-stream-client.ts",
78
+ "default": "./src/deps/html-stream-client.ts"
79
+ },
80
+ "./internal/deps/html-stream-server": {
81
+ "types": "./src/deps/html-stream-server.ts",
82
+ "default": "./src/deps/html-stream-server.ts"
83
+ },
84
+ "./cache": {
85
+ "react-server": "./src/cache/index.ts",
86
+ "types": "./src/cache/index.ts",
87
+ "default": "./src/cache/index.ts"
88
+ },
89
+ "./theme": {
90
+ "types": "./src/theme/index.ts",
91
+ "default": "./src/theme/index.ts"
92
+ }
93
+ },
94
+ "files": [
95
+ "src",
96
+ "dist",
97
+ "skills",
98
+ "CLAUDE.md",
99
+ "README.md"
100
+ ],
101
+ "peerDependencies": {
102
+ "@cloudflare/vite-plugin": "^1.21.0",
103
+ "@vitejs/plugin-rsc": "^0.5.14",
104
+ "react": "^18.0.0 || ^19.0.0",
105
+ "vite": "^7.3.0"
106
+ },
107
+ "peerDependenciesMeta": {
108
+ "@cloudflare/vite-plugin": {
109
+ "optional": true
110
+ },
111
+ "vite": {
112
+ "optional": true
113
+ }
114
+ },
115
+ "dependencies": {
116
+ "@vitejs/plugin-rsc": "^0.5.14",
117
+ "magic-string": "^0.30.17",
118
+ "rsc-html-stream": "^0.0.7"
119
+ },
120
+ "devDependencies": {
121
+ "@playwright/test": "^1.49.1",
122
+ "@types/node": "^24.10.1",
123
+ "@types/react": "^19.2.7",
124
+ "@types/react-dom": "^19.2.3",
125
+ "react": "^19.2.1",
126
+ "react-dom": "^19.2.1",
127
+ "esbuild": "^0.27.0",
128
+ "tinyexec": "^0.3.2",
129
+ "typescript": "^5.3.0",
130
+ "vitest": "^2.1.8"
131
+ },
132
+ "scripts": {
133
+ "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external",
134
+ "typecheck": "tsc --noEmit",
135
+ "test": "playwright test",
136
+ "test:ui": "playwright test --ui",
137
+ "test:unit": "vitest run",
138
+ "test:unit:watch": "vitest"
139
+ }
140
+ }
@@ -0,0 +1,319 @@
1
+ ---
2
+ name: caching
3
+ description: Configure segment caching with memory or Cloudflare KV stores in rsc-router
4
+ argument-hint: [setup]
5
+ ---
6
+
7
+ # Caching
8
+
9
+ rsc-router supports segment-level caching with stale-while-revalidate (SWR) for optimal performance.
10
+
11
+ ## Router Cache Configuration
12
+
13
+ Configure caching in `createRSCRouter`:
14
+
15
+ ```typescript
16
+ import { createRSCRouter } from "rsc-router/server";
17
+ import { MemorySegmentCacheStore } from "rsc-router/cache";
18
+
19
+ const store = new MemorySegmentCacheStore({
20
+ defaults: { ttl: 60, swr: 300 }
21
+ });
22
+
23
+ const router = createRSCRouter<AppEnv>({
24
+ document: Document,
25
+ cache: {
26
+ store,
27
+ enabled: true,
28
+ },
29
+ });
30
+ ```
31
+
32
+ ### Dynamic Cache Configuration
33
+
34
+ Use a factory function for environment-based config:
35
+
36
+ ```typescript
37
+ const router = createRSCRouter<AppEnv>({
38
+ document: Document,
39
+ cache: (env) => ({
40
+ store: new CFCacheStore({
41
+ baseUrl: env.Bindings.CACHE_URL,
42
+ waitUntil: (fn) => env.ctx.waitUntil(fn),
43
+ }),
44
+ enabled: env.Bindings.CACHE_ENABLED === "true",
45
+ }),
46
+ });
47
+ ```
48
+
49
+ ## Cache Stores
50
+
51
+ ### Memory Store (Development)
52
+
53
+ ```typescript
54
+ import { MemorySegmentCacheStore } from "rsc-router/cache";
55
+
56
+ const store = new MemorySegmentCacheStore({
57
+ defaults: { ttl: 60, swr: 300 }
58
+ });
59
+
60
+ // Debug cache stats
61
+ console.log(store.getStats());
62
+ ```
63
+
64
+ Features:
65
+ - Survives HMR in development
66
+ - No true SWR (entries expire at TTL)
67
+ - Good for development and single-instance deployments
68
+
69
+ ### Cloudflare Cache Store (Production)
70
+
71
+ ```typescript
72
+ import { CFCacheStore } from "rsc-router/cache/cf";
73
+
74
+ const store = new CFCacheStore({
75
+ namespace: "rsc-cache", // Optional, uses caches.default
76
+ baseUrl: "https://cache.example.com/",
77
+ defaults: { ttl: 3600, swr: 7200 },
78
+ waitUntil: (fn) => ctx.waitUntil(fn),
79
+ version: "1.0.0", // For cache invalidation on deploy
80
+ });
81
+ ```
82
+
83
+ Features:
84
+ - Cloudflare Cache API integration
85
+ - True SWR with atomic revalidation (prevents thundering herd)
86
+ - Non-blocking writes via `waitUntil`
87
+ - Version-based cache invalidation
88
+
89
+ ## Cache Options
90
+
91
+ ```typescript
92
+ interface CacheOptions {
93
+ // Time-to-live in seconds
94
+ ttl: number;
95
+
96
+ // Stale-while-revalidate window (seconds after TTL)
97
+ swr?: number;
98
+
99
+ // Override cache store for this boundary
100
+ store?: SegmentCacheStore;
101
+
102
+ // Conditional caching
103
+ condition?: (ctx) => boolean;
104
+
105
+ // Custom cache key
106
+ key?: (ctx) => string | Promise<string>;
107
+
108
+ // Tags for invalidation
109
+ tags?: string[] | ((ctx) => string[]);
110
+ }
111
+ ```
112
+
113
+ ## Route-Level Caching
114
+
115
+ Use `cache()` in handlers to set cache boundaries:
116
+
117
+ ```typescript
118
+ import { map } from "rsc-router/server";
119
+
120
+ export default map<typeof routes>(({ route, layout, cache }) => [
121
+ // Cache entire layout and children
122
+ cache({ ttl: 3600, swr: 7200 }, () => [
123
+ layout(<StaticLayout />, () => [
124
+ route("about", AboutPage),
125
+ route("contact", ContactPage),
126
+ ]),
127
+ ]),
128
+
129
+ // Different cache settings per route
130
+ cache({ ttl: 60 }, () => [
131
+ route("dashboard", DashboardPage),
132
+ ]),
133
+
134
+ // No caching (default)
135
+ route("checkout", CheckoutPage),
136
+ ]);
137
+ ```
138
+
139
+ ## Conditional Caching
140
+
141
+ Skip cache based on request context:
142
+
143
+ ```typescript
144
+ cache({
145
+ ttl: 3600,
146
+ condition: (ctx) => {
147
+ // Don't cache for authenticated users
148
+ const hasAuth = ctx.request.headers.has("Authorization");
149
+ return !hasAuth;
150
+ },
151
+ }, () => [
152
+ route("products", ProductList),
153
+ ])
154
+ ```
155
+
156
+ ## Custom Cache Keys
157
+
158
+ Override the default cache key:
159
+
160
+ ```typescript
161
+ cache({
162
+ ttl: 3600,
163
+ key: (ctx) => {
164
+ // Include user segment in cache key
165
+ const segment = ctx.request.headers.get("x-user-segment") || "default";
166
+ return `${segment}:products:${ctx.params.category}`;
167
+ },
168
+ }, () => [
169
+ route("products.category", CategoryPage),
170
+ ])
171
+ ```
172
+
173
+ Store-level key modification:
174
+
175
+ ```typescript
176
+ const store = new CFCacheStore({
177
+ keyGenerator: (ctx, defaultKey) => {
178
+ const region = ctx.request.cf?.colo || "unknown";
179
+ return `${region}:${defaultKey}`;
180
+ },
181
+ });
182
+ ```
183
+
184
+ ## Stale-While-Revalidate (SWR)
185
+
186
+ SWR serves stale content while fetching fresh data in the background:
187
+
188
+ ```
189
+ Timeline:
190
+ ├─ 0 to TTL ─────────┤ Fresh content served
191
+ ├─ TTL to TTL+SWR ───┤ Stale content served, revalidation in background
192
+ ├─ After TTL+SWR ────┤ Cache expired, new fetch required
193
+ ```
194
+
195
+ ```typescript
196
+ cache({
197
+ ttl: 60, // Fresh for 60 seconds
198
+ swr: 300, // Stale-but-usable for 5 more minutes
199
+ }, () => [
200
+ route("feed", FeedPage),
201
+ ])
202
+ ```
203
+
204
+ ## Multi-Store Setup
205
+
206
+ Use different stores for different data patterns:
207
+
208
+ ```typescript
209
+ const hotStore = new MemorySegmentCacheStore({
210
+ defaults: { ttl: 10 }
211
+ });
212
+
213
+ const coldStore = new CFCacheStore({
214
+ defaults: { ttl: 3600, swr: 7200 }
215
+ });
216
+
217
+ export default map<typeof routes>(({ route, cache }) => [
218
+ // Frequently changing data - short TTL, memory
219
+ cache({ store: hotStore, ttl: 10 }, () => [
220
+ route("dashboard", DashboardPage),
221
+ route("notifications", NotificationsPage),
222
+ ]),
223
+
224
+ // Rarely changing data - long TTL, edge cache
225
+ cache({ store: coldStore, ttl: 3600 }, () => [
226
+ route("archive", ArchivePage),
227
+ route("docs", DocsPage),
228
+ ]),
229
+ ]);
230
+ ```
231
+
232
+ ## Cache Invalidation
233
+
234
+ ### Version-Based (Cloudflare)
235
+
236
+ ```typescript
237
+ import packageJson from "./package.json";
238
+
239
+ const store = new CFCacheStore({
240
+ version: packageJson.version, // All cache invalidated on deploy
241
+ });
242
+ ```
243
+
244
+ ### Tag-Based (Future)
245
+
246
+ ```typescript
247
+ cache({
248
+ tags: (ctx) => [`product:${ctx.params.id}`, "products"],
249
+ }, () => [
250
+ route("products.detail", ProductDetail),
251
+ ])
252
+
253
+ // Invalidate by tag
254
+ await store.invalidateTag("products");
255
+ ```
256
+
257
+ ## Complete Production Example
258
+
259
+ ```typescript
260
+ // router.tsx
261
+ import { createRSCRouter } from "rsc-router/server";
262
+ import { CFCacheStore } from "rsc-router/cache/cf";
263
+ import packageJson from "./package.json";
264
+
265
+ const router = createRSCRouter<AppEnv>({
266
+ document: Document,
267
+ cache: (env) => ({
268
+ store: new CFCacheStore({
269
+ baseUrl: "https://rsc-cache.internal/",
270
+ defaults: { ttl: 300, swr: 3600 },
271
+ waitUntil: (fn) => env.ctx.waitUntil(fn),
272
+ version: packageJson.version,
273
+ }),
274
+ enabled: env.Bindings.NODE_ENV === "production",
275
+ }),
276
+ });
277
+
278
+ // handlers/shop.tsx
279
+ export default map<typeof shopRoutes>(({ route, layout, cache }) => [
280
+ // Static pages - aggressive caching
281
+ cache({ ttl: 86400, swr: 604800 }, () => [
282
+ route("shop.about", AboutPage),
283
+ route("shop.terms", TermsPage),
284
+ ]),
285
+
286
+ // Product listing - moderate caching
287
+ cache({ ttl: 300, swr: 3600 }, () => [
288
+ route("shop.products", ProductList),
289
+ ]),
290
+
291
+ // Product detail - cache with product-specific key
292
+ cache({
293
+ ttl: 600,
294
+ swr: 3600,
295
+ key: (ctx) => `product:${ctx.params.slug}`,
296
+ }, () => [
297
+ route("shop.product", ProductDetail),
298
+ ]),
299
+
300
+ // User-specific pages - no caching
301
+ route("shop.cart", CartPage),
302
+ route("shop.checkout", CheckoutPage),
303
+ ]);
304
+ ```
305
+
306
+ ## Cache Headers (Cloudflare)
307
+
308
+ CFCacheStore sets these headers for debugging:
309
+
310
+ - `x-edge-cache-status`: `HIT` or `REVALIDATING`
311
+ - `x-edge-cache-stale-at`: Timestamp when entry becomes stale
312
+
313
+ ## What Gets Cached
314
+
315
+ - Route segments (components, layouts, loading states)
316
+ - Handle data (breadcrumbs, metadata)
317
+ - **Not cached by default**: Loader data (fetched fresh each request)
318
+
319
+ To cache loader results, use loader-level caching or external caching strategies.
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: document-cache
3
+ description: Cache full HTTP responses at the edge with Cache-Control headers
4
+ argument-hint: [setup]
5
+ ---
6
+
7
+ # Document Cache
8
+
9
+ Caches complete HTTP responses (HTML/RSC) at the edge based on Cache-Control headers. Routes opt-in by setting `s-maxage`.
10
+
11
+ ## Setup
12
+
13
+ Add middleware to router:
14
+
15
+ ```typescript
16
+ import { createRSCRouter, createDocumentCacheMiddleware } from "@rangojs/router/server";
17
+ import { CFCacheStore } from "@rangojs/router/cache/cf";
18
+
19
+ const router = createRSCRouter<AppEnv>({
20
+ document: Document,
21
+ cache: (env) => ({
22
+ store: new CFCacheStore({ ctx: env.ctx }),
23
+ }),
24
+ })
25
+ .use(createDocumentCacheMiddleware())
26
+ .routes(routes);
27
+ ```
28
+
29
+ ## Route Opt-In
30
+
31
+ Routes opt-in by setting `Cache-Control` with `s-maxage`:
32
+
33
+ ```typescript
34
+ route("home", (ctx) => {
35
+ ctx.headers.set("Cache-Control", "s-maxage=60, stale-while-revalidate=300");
36
+ return <HomePage />;
37
+ });
38
+ ```
39
+
40
+ ## Middleware Options
41
+
42
+ ```typescript
43
+ createDocumentCacheMiddleware({
44
+ // Skip specific paths
45
+ skipPaths: ["/api", "/admin"],
46
+
47
+ // Custom cache key
48
+ keyGenerator: (url) => url.pathname,
49
+
50
+ // Conditional caching
51
+ isEnabled: (ctx) => !ctx.request.headers.has("x-preview"),
52
+
53
+ // Debug logging
54
+ debug: true,
55
+ });
56
+ ```
57
+
58
+ ## How It Works
59
+
60
+ ```
61
+ Request → Check Cache
62
+
63
+ ┌──────┴──────┐
64
+ │ │
65
+ HIT MISS
66
+ │ │
67
+ ↓ ↓
68
+ Fresh? Run handler
69
+ │ │
70
+ Yes → Return Has s-maxage?
71
+ │ │
72
+ No (stale) Yes → Cache + Return
73
+ │ │
74
+ ↓ No → Return (no cache)
75
+ Return stale,
76
+ revalidate in
77
+ background (SWR)
78
+ ```
79
+
80
+ ## Cache Status Header
81
+
82
+ Response includes `x-document-cache-status`:
83
+ - `HIT` - Fresh cache hit
84
+ - `STALE` - Served stale, revalidating in background
85
+ - `MISS` - Cache miss, response was generated fresh
86
+
87
+ ## Cache Key Generation
88
+
89
+ Default keys differentiate:
90
+ - HTML requests: `{pathname}:html`
91
+ - RSC partials: `{pathname}:{segmentHash}:rsc`
92
+
93
+ Segment hash ensures different cached responses for navigations from different source pages (with different layouts).
94
+
95
+ ## What Gets Cached
96
+
97
+ - Full HTML responses (document requests)
98
+ - RSC payloads (client navigation)
99
+ - Only 200 OK responses with `s-maxage`
100
+
101
+ ## What's NOT Cached
102
+
103
+ - Server actions (`_rsc_action`)
104
+ - Loader requests (`_rsc_loader`)
105
+ - Responses without `s-maxage`
106
+ - Non-200 responses
107
+
108
+ ## Complete Example
109
+
110
+ ```typescript
111
+ // router.tsx
112
+ const router = createRSCRouter<AppEnv>({
113
+ document: Document,
114
+ cache: (env) => ({
115
+ store: new CFCacheStore({ ctx: env.ctx }),
116
+ }),
117
+ })
118
+ .use(createDocumentCacheMiddleware({
119
+ skipPaths: ["/api"],
120
+ debug: process.env.NODE_ENV === "development",
121
+ }))
122
+ .routes(routes);
123
+
124
+ // handlers.tsx
125
+ route("blog", (ctx) => {
126
+ // Cache for 5 min, serve stale for 1 hour while revalidating
127
+ ctx.headers.set("Cache-Control", "s-maxage=300, stale-while-revalidate=3600");
128
+ return <BlogIndex />;
129
+ });
130
+
131
+ route("blog.post", (ctx) => {
132
+ // Long cache for individual posts
133
+ ctx.headers.set("Cache-Control", "s-maxage=3600, stale-while-revalidate=86400");
134
+ return <BlogPost slug={ctx.params.slug} />;
135
+ });
136
+
137
+ route("dashboard", (ctx) => {
138
+ // No cache header = not cached
139
+ return <Dashboard />;
140
+ });
141
+ ```
142
+
143
+ ## Document Cache vs Segment Cache
144
+
145
+ | Feature | Document Cache | Segment Cache |
146
+ |---------|---------------|---------------|
147
+ | Granularity | Full response | Individual segments |
148
+ | Opt-in | `s-maxage` header | `cache()` DSL |
149
+ | Use case | Static pages | Dynamic compositions |
150
+ | Key includes | URL + segment hash | Route params |
151
+
152
+ Use document cache for mostly-static pages. Use segment cache when different parts of a page have different cache requirements.