@rangojs/router 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/vite/index.js +1381 -1346
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +39 -33
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +8 -5
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +1 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +117 -30
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: middleware
|
|
3
|
-
description: Define middleware
|
|
3
|
+
description: Define global (router.use) and route (middleware()) middleware in @rangojs/router — auth gates, logging, response headers, context variables, redirects. Use when gating routes behind auth checks, logging requests, setting headers or ctx variables before a handler runs, or deciding whether middleware wraps server actions.
|
|
4
4
|
argument-hint: [middleware-name]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Middleware
|
|
8
8
|
|
|
9
|
-
Middleware runs before
|
|
9
|
+
Middleware runs before and after route handlers using the onion model: code
|
|
10
|
+
before `await next()` runs on the way in, code after it runs on the way out
|
|
11
|
+
with the downstream `Response` available. This skill covers the two
|
|
12
|
+
registration levels (global `router.use()` and route `middleware()`), what each
|
|
13
|
+
wraps (actions, renders, fetchable loaders), the middleware context, and
|
|
14
|
+
common patterns.
|
|
10
15
|
|
|
11
16
|
## Execution Model
|
|
12
17
|
|
|
@@ -23,13 +28,15 @@ const router = createRouter<AppEnv>({})
|
|
|
23
28
|
.routes(urlpatterns);
|
|
24
29
|
```
|
|
25
30
|
|
|
26
|
-
When the router has a `basename`, pattern-scoped `.use()` patterns are automatically prefixed. For example, with `basename: "/app"`, `.use("/admin/*", mw)` matches `/app/admin/*`.
|
|
31
|
+
Scope patterns use the route pattern syntax: `:param` (typed on `ctx.params`), optional `:param?`, constrained `:locale(en|gb)`, and a trailing `*` wildcard. When the router has a `basename`, pattern-scoped `.use()` patterns are automatically prefixed. For example, with `basename: "/app"`, `.use("/admin/*", mw)` matches `/app/admin/*`.
|
|
32
|
+
|
|
33
|
+
A pattern matches the request URL. Server actions are posted to the current page's URL, but an action can be invoked through any URL that matches a route, so a pattern-scoped guard is not an authorization boundary for actions. Authorize sensitive actions inside the action body (see `/server-actions` → "Authorization in actions").
|
|
27
34
|
|
|
28
35
|
### Route middleware (`middleware()` in `urls()`)
|
|
29
36
|
|
|
30
37
|
Registered inside `urls()` callback. Wraps **rendering only** -- it does NOT wrap server action execution. Actions run before route middleware, so when route middleware executes during post-action revalidation, it can observe state that the action set (cookies, context variables, headers).
|
|
31
38
|
|
|
32
|
-
> **Implication for auth:** route middleware cannot guard server actions.
|
|
39
|
+
> **Implication for auth:** route middleware cannot guard server actions. Load identity in global `router.use()` middleware (it wraps actions) and check authorization inside the action body. See `/server-actions` for action-side auth patterns.
|
|
33
40
|
|
|
34
41
|
```
|
|
35
42
|
Request flow (with action):
|
|
@@ -74,9 +81,10 @@ For shared segment data, use named revalidation contracts on both the producer
|
|
|
74
81
|
and consumer segments, even when middleware is present in the chain.
|
|
75
82
|
|
|
76
83
|
```typescript
|
|
84
|
+
import type { Revalidate } from "@rangojs/router";
|
|
77
85
|
import * as CartActions from "./actions/cart";
|
|
78
86
|
|
|
79
|
-
export const revalidateCartData = (ctx) =>
|
|
87
|
+
export const revalidateCartData: Revalidate = (ctx) =>
|
|
80
88
|
ctx.isAction(CartActions) || undefined;
|
|
81
89
|
|
|
82
90
|
layout(CartLayout, () => [
|
|
@@ -104,7 +112,7 @@ layout(CartLayout, () => [
|
|
|
104
112
|
]);
|
|
105
113
|
```
|
|
106
114
|
|
|
107
|
-
Route middleware is the right place for per-route concerns that affect rendering (setting context variables for handlers, adding response headers, reading cookies set by actions). It is NOT the right place for action guards -- use global middleware
|
|
115
|
+
Route middleware is the right place for per-route concerns that affect rendering (setting context variables for handlers, adding response headers, reading cookies set by actions). It is NOT the right place for action guards -- use global middleware to load identity and check authorization in the action body.
|
|
108
116
|
|
|
109
117
|
## Basic Middleware
|
|
110
118
|
|
|
@@ -125,6 +133,13 @@ export const authMiddleware: Middleware = async (ctx, next) => {
|
|
|
125
133
|
};
|
|
126
134
|
```
|
|
127
135
|
|
|
136
|
+
Every middleware must either call `next()` or return (or throw) a `Response`.
|
|
137
|
+
Returning or throwing a `Response` short-circuits the chain; downstream
|
|
138
|
+
middleware and the handler do not run, and headers/cookies already set on the
|
|
139
|
+
context are merged into it. A middleware that does neither throws an error; any
|
|
140
|
+
other return value is ignored with a warning. `await next()` resolves to the
|
|
141
|
+
downstream `Response`, so `return next()` and `await next()` are both fine.
|
|
142
|
+
|
|
128
143
|
## Using Middleware in Routes
|
|
129
144
|
|
|
130
145
|
```typescript
|
|
@@ -132,7 +147,7 @@ import { urls } from "@rangojs/router";
|
|
|
132
147
|
import { authMiddleware, loggerMiddleware } from "./middleware";
|
|
133
148
|
|
|
134
149
|
export const urlpatterns = urls(({ path, layout, middleware }) => [
|
|
135
|
-
//
|
|
150
|
+
// Route middleware for every route in this urls() tree (wraps renders, not actions)
|
|
136
151
|
middleware(loggerMiddleware),
|
|
137
152
|
|
|
138
153
|
// Layout with scoped middleware
|
|
@@ -192,31 +207,90 @@ middleware([authMw, loggingMw], () => [
|
|
|
192
207
|
This creates a transparent layout (`<Outlet />`) that carries the middleware.
|
|
193
208
|
The middleware does not affect sibling routes outside the callback.
|
|
194
209
|
|
|
210
|
+
That scope comes from the routes inside the callback. With no routes inside,
|
|
211
|
+
the wrapper scopes nothing: its middleware runs for every route of the
|
|
212
|
+
enclosing path or layout. Such a wrapper cannot contain `layout()`;
|
|
213
|
+
`router.routes()` throws and points to the flat form:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// throws at definition time
|
|
217
|
+
path("/account", AccountPage, { name: "account" }, () => [
|
|
218
|
+
middleware(requireAuth, () => [layout(<AccountNav />)]),
|
|
219
|
+
]);
|
|
220
|
+
// same behavior, written flat
|
|
221
|
+
path("/account", AccountPage, { name: "account" }, () => [
|
|
222
|
+
middleware(requireAuth),
|
|
223
|
+
layout(<AccountNav />),
|
|
224
|
+
]);
|
|
225
|
+
```
|
|
226
|
+
|
|
195
227
|
## Middleware Context
|
|
196
228
|
|
|
197
229
|
```typescript
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
ctx.url; // Parsed URL
|
|
202
|
-
ctx.params; // Route parameters
|
|
203
|
-
ctx.build; // in middleware: true only during Prerender + ppr build-shell capture (plain Prerender does not run middleware)
|
|
230
|
+
import { cookies, headers } from "@rangojs/router";
|
|
231
|
+
import type { Middleware } from "@rangojs/router";
|
|
232
|
+
import { FlashMessage } from "./location-states";
|
|
204
233
|
|
|
205
|
-
|
|
234
|
+
export const myMiddleware: Middleware = async (ctx, next) => {
|
|
235
|
+
// Request
|
|
236
|
+
ctx.request; // incoming Request (raw URL, method, body)
|
|
237
|
+
ctx.url; // URL with internal _rsc* params stripped (also ctx.pathname, ctx.searchParams)
|
|
238
|
+
ctx.params; // params from the router.use() pattern or the matched route
|
|
239
|
+
ctx.routeName; // matched route name (reliable after await next() in global middleware)
|
|
240
|
+
ctx.build; // true only during Prerender + ppr build-shell capture (plain Prerender does not run middleware)
|
|
241
|
+
headers().get("accept-language"); // read-only request headers (free function)
|
|
242
|
+
cookies().get("session")?.value; // request cookies (free function; also .set/.delete)
|
|
243
|
+
|
|
244
|
+
// Platform bindings (plain bindings from createRouter<TEnv>())
|
|
206
245
|
ctx.env.DB; // D1Database
|
|
207
|
-
ctx.
|
|
246
|
+
ctx.waitUntil(async () => {}); // work after the response is sent
|
|
208
247
|
|
|
209
|
-
//
|
|
248
|
+
// Variables for downstream middleware, handlers, and loaders (typed via Rango.Vars)
|
|
210
249
|
ctx.set("user", { id: "123", name: "John" });
|
|
250
|
+
ctx.get("user");
|
|
251
|
+
|
|
252
|
+
// Response shaping
|
|
253
|
+
ctx.header("X-Frame-Options", "DENY"); // set one response header
|
|
254
|
+
ctx.headers; // response Headers (stub before next(), the real response after)
|
|
255
|
+
ctx.setLocationState(FlashMessage({ text: "Saved" })); // history state for the client
|
|
256
|
+
ctx.reverse("home"); // URL for a global route name (no ".name", no param auto-fill)
|
|
211
257
|
|
|
212
258
|
// Opt the current request out of PPR shell lookup/capture.
|
|
213
259
|
ctx.dynamic();
|
|
260
|
+
// Print the per-request performance timeline (see /loader → "debugPerformance").
|
|
261
|
+
ctx.debugPerformance();
|
|
262
|
+
|
|
263
|
+
// Continue to the next middleware / the handler
|
|
264
|
+
const response = await next();
|
|
265
|
+
|
|
266
|
+
// After the handler: the real Response is available
|
|
267
|
+
console.log(response.status);
|
|
268
|
+
};
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Cookies and request headers are not `ctx` members; use the free functions
|
|
272
|
+
`cookies()` and `headers()` from `@rangojs/router`.
|
|
273
|
+
|
|
274
|
+
### Changing the response after `next()`
|
|
214
275
|
|
|
215
|
-
|
|
276
|
+
After `await next()`, `ctx.header()` and `ctx.headers` write to the real
|
|
277
|
+
downstream response. Returning a different `Response` replaces it.
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
export const securityHeaders: Middleware = async (ctx, next) => {
|
|
216
281
|
await next();
|
|
282
|
+
ctx.header("Strict-Transport-Security", "max-age=63072000");
|
|
283
|
+
ctx.headers.set("X-Content-Type-Options", "nosniff");
|
|
284
|
+
};
|
|
217
285
|
|
|
218
|
-
|
|
219
|
-
|
|
286
|
+
export const timing: Middleware = async (ctx, next) => {
|
|
287
|
+
const start = performance.now();
|
|
288
|
+
try {
|
|
289
|
+
await next();
|
|
290
|
+
} finally {
|
|
291
|
+
// try/catch around next() also sees downstream errors
|
|
292
|
+
console.log(`${ctx.pathname} ${(performance.now() - start).toFixed(1)}ms`);
|
|
293
|
+
}
|
|
220
294
|
};
|
|
221
295
|
```
|
|
222
296
|
|
|
@@ -229,7 +303,8 @@ import { createVar } from "@rangojs/router";
|
|
|
229
303
|
import type { Middleware } from "@rangojs/router";
|
|
230
304
|
|
|
231
305
|
interface AuthUser { id: string; email: string; role: string }
|
|
232
|
-
|
|
306
|
+
// cache: false — per-user data must never bake into a cache() segment
|
|
307
|
+
export const CurrentUser = createVar<AuthUser>({ cache: false });
|
|
233
308
|
|
|
234
309
|
export const authMiddleware: Middleware = async (ctx, next) => {
|
|
235
310
|
const token = ctx.request.headers.get("Authorization");
|
|
@@ -241,6 +316,7 @@ export const authMiddleware: Middleware = async (ctx, next) => {
|
|
|
241
316
|
};
|
|
242
317
|
|
|
243
318
|
// In a handler -- typed read
|
|
319
|
+
import type { Handler } from "@rangojs/router";
|
|
244
320
|
import { CurrentUser } from "./middleware";
|
|
245
321
|
|
|
246
322
|
const Dashboard: Handler<"dashboard"> = (ctx) => {
|
|
@@ -253,6 +329,18 @@ This works alongside `ctx.get("key")` / `ctx.set("key", value)` (global typing
|
|
|
253
329
|
via Rango.Vars augmentation). Use `createVar` for route-local or feature-scoped
|
|
254
330
|
data; use Rango.Vars for app-wide middleware state.
|
|
255
331
|
|
|
332
|
+
Mark request-specific data (sessions, users, tokens) non-cacheable: either on
|
|
333
|
+
the token (`createVar<T>({ cache: false })`) or per write
|
|
334
|
+
(`ctx.set("user", user, { cache: false })`). Reading it with `ctx.get()` inside
|
|
335
|
+
a `cache()` boundary or a `"use cache"` function then throws instead of baking
|
|
336
|
+
one user's data into a shared entry (inside `"use cache"`, also through a
|
|
337
|
+
middleware `ctx` passed in); loaders can still read it because they always run
|
|
338
|
+
fresh (a `"use cache"` function a loader calls cannot). A middleware `ctx`
|
|
339
|
+
passed to a `"use cache"` function keys the call by host, pathname, params and
|
|
340
|
+
search, plus the route name once the route is matched (a global middleware
|
|
341
|
+
before `next()` has none yet), and its `set()`, `header()` and `headers` writes
|
|
342
|
+
throw inside the function (`/use-cache`).
|
|
343
|
+
|
|
256
344
|
## Build-Time PPR Middleware
|
|
257
345
|
|
|
258
346
|
Normal `Prerender` Flight payload collection does not run middleware: there is
|
|
@@ -285,12 +373,20 @@ export const commerceMiddleware: Middleware = async (ctx, next) => {
|
|
|
285
373
|
## Redirect with State in Middleware
|
|
286
374
|
|
|
287
375
|
```typescript
|
|
288
|
-
|
|
289
|
-
|
|
376
|
+
// location-states.ts — shared module; the client component that reads the
|
|
377
|
+
// state must import the same definition
|
|
378
|
+
import { createLocationState } from "@rangojs/router";
|
|
290
379
|
|
|
291
380
|
export const FlashMessage = createLocationState<{ text: string }>({
|
|
292
381
|
flash: true,
|
|
293
382
|
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// middleware/auth.ts
|
|
387
|
+
import { redirect } from "@rangojs/router";
|
|
388
|
+
import type { Middleware } from "@rangojs/router";
|
|
389
|
+
import { FlashMessage } from "../location-states";
|
|
294
390
|
|
|
295
391
|
export const requireAuthMiddleware: Middleware = async (ctx, next) => {
|
|
296
392
|
const token = ctx.request.headers.get("Authorization");
|
|
@@ -400,7 +496,7 @@ import {
|
|
|
400
496
|
} from "./middleware";
|
|
401
497
|
|
|
402
498
|
export const urlpatterns = urls(({ path, layout, middleware }) => [
|
|
403
|
-
//
|
|
499
|
+
// Route middleware for every route in this tree (use router.use() to also wrap actions)
|
|
404
500
|
middleware(loggerMiddleware),
|
|
405
501
|
middleware(mockAuthMiddleware),
|
|
406
502
|
|
|
@@ -6,6 +6,13 @@ argument-hint: [path-to-nextjs-app]
|
|
|
6
6
|
|
|
7
7
|
# Migrate from Next.js App Router to @rangojs/router
|
|
8
8
|
|
|
9
|
+
This skill maps a Next.js App Router project onto Rango: project setup, the
|
|
10
|
+
`app/` file conventions to the `urls()` DSL, data fetching and rendering modes
|
|
11
|
+
(SSG, ISR, PPR), middleware, navigation, server actions, metadata, API routes,
|
|
12
|
+
and theming. Use it when porting a Next.js app, or when a Next.js habit needs a
|
|
13
|
+
Rango equivalent. For a side-by-side evaluation rather than a port, see
|
|
14
|
+
`/comparison`; for React Router or Remix apps, see `/migrate-react-router`.
|
|
15
|
+
|
|
9
16
|
## Why Rango
|
|
10
17
|
|
|
11
18
|
Common reasons to migrate:
|
|
@@ -99,10 +106,14 @@ Replace Next.js tooling with Vite + Rango:
|
|
|
99
106
|
|
|
100
107
|
```bash
|
|
101
108
|
npm remove next @next/env
|
|
102
|
-
npm install @rangojs/router
|
|
109
|
+
npm install @rangojs/router # keep react and react-dom
|
|
103
110
|
npm install -D vite
|
|
104
111
|
```
|
|
105
112
|
|
|
113
|
+
`rango()` already includes `@vitejs/plugin-rsc` and supplies the client and
|
|
114
|
+
server entries. Add `@vitejs/plugin-react` only if you want Fast Refresh or the
|
|
115
|
+
React Compiler (see `/react-compiler`).
|
|
116
|
+
|
|
106
117
|
```typescript
|
|
107
118
|
// vite.config.ts
|
|
108
119
|
import { defineConfig } from "vite";
|
|
@@ -119,7 +130,7 @@ import { createRouter } from "@rangojs/router";
|
|
|
119
130
|
import { Document } from "./document";
|
|
120
131
|
import { urlpatterns } from "./urls";
|
|
121
132
|
|
|
122
|
-
export
|
|
133
|
+
export const router = createRouter({
|
|
123
134
|
document: Document,
|
|
124
135
|
}).routes(urlpatterns);
|
|
125
136
|
```
|
|
@@ -223,8 +234,8 @@ The `include()` name has three deliberate modes:
|
|
|
223
234
|
| Form | Child route names |
|
|
224
235
|
| ----------------------------------------------- | ----------------------------------------------------------------------- |
|
|
225
236
|
| `include("/", patterns)` | private to the included module; omitted from the app-wide generated map |
|
|
226
|
-
| `include("/", patterns, { name: "marketing" })` | globally registered as `marketing.
|
|
227
|
-
| `include("/", patterns, { name: "" })` | flattened into the parent map as `
|
|
237
|
+
| `include("/", patterns, { name: "marketing" })` | globally registered as `marketing.landing`, `marketing.pricing`, ... |
|
|
238
|
+
| `include("/", patterns, { name: "" })` | flattened into the parent map as `landing`, `pricing`, ... |
|
|
228
239
|
|
|
229
240
|
Use the empty-string form only when the child names are intentionally global
|
|
230
241
|
and unique. Inside a private/namespaced module, prefer dot-local reversal or
|
|
@@ -300,17 +311,23 @@ run the Phase 0 audit before carrying those calls over unchanged.
|
|
|
300
311
|
```typescript
|
|
301
312
|
// Next.js:
|
|
302
313
|
async function ProductPage({ params }) {
|
|
303
|
-
const product = await fetch(
|
|
314
|
+
const product = await fetch(`https://api.example.com/products/${params.slug}`).then(r => r.json());
|
|
304
315
|
return <div>{product.name}</div>;
|
|
305
316
|
}
|
|
306
317
|
|
|
307
318
|
// Rango: same pattern, params come from ctx
|
|
319
|
+
import type { Handler } from "@rangojs/router";
|
|
320
|
+
|
|
308
321
|
const ProductPage: Handler<"product"> = async (ctx) => {
|
|
309
|
-
const product = await fetch(
|
|
322
|
+
const product = await fetch(`https://api.example.com/products/${ctx.params.slug}`).then(r => r.json());
|
|
310
323
|
return <div>{product.name}</div>;
|
|
311
324
|
};
|
|
312
325
|
```
|
|
313
326
|
|
|
327
|
+
If the Next page fetched its own `/api/...` route, call the underlying service
|
|
328
|
+
function directly instead of making a request back to the same app (see
|
|
329
|
+
[backend-host-swap.md](backend-host-swap.md#extract-shared-server-services)).
|
|
330
|
+
|
|
314
331
|
### When to use createLoader
|
|
315
332
|
|
|
316
333
|
Loaders are Rango's live data layer. Use them when you need:
|
|
@@ -360,9 +377,11 @@ export const ProductDef = Prerender<{ slug: string }>(
|
|
|
360
377
|
);
|
|
361
378
|
|
|
362
379
|
// Rango (with live fallback — matches Next.js dynamicParams behavior):
|
|
363
|
-
import { Prerender, Passthrough } from "@rangojs/router";
|
|
380
|
+
import { Prerender, Passthrough, notFound } from "@rangojs/router";
|
|
364
381
|
|
|
365
|
-
|
|
382
|
+
// Keep the definition exported: the Vite plugin injects the stable id only
|
|
383
|
+
// into `export const X = Prerender(...)` (or `const X = ...; export { X }`).
|
|
384
|
+
export const ProductDef = Prerender<{ slug: string }>(
|
|
366
385
|
async () => [{ slug: "a" }, { slug: "b" }],
|
|
367
386
|
async (ctx) => {
|
|
368
387
|
const product = await getProduct(ctx.params.slug);
|
|
@@ -373,8 +392,12 @@ const ProductDef = Prerender<{ slug: string }>(
|
|
|
373
392
|
|
|
374
393
|
export const Product = Passthrough(ProductDef, async (ctx) => {
|
|
375
394
|
const product = await getProduct(ctx.params.slug);
|
|
395
|
+
if (!product) notFound();
|
|
376
396
|
return <ProductPage product={product} />;
|
|
377
397
|
});
|
|
398
|
+
|
|
399
|
+
// In urls:
|
|
400
|
+
path("/products/:slug", Product, { name: "product" });
|
|
378
401
|
```
|
|
379
402
|
|
|
380
403
|
Use `Passthrough()` whenever the Next.js route has `dynamicParams: true` (the
|
|
@@ -415,6 +438,9 @@ export default async function Page({ params }) {
|
|
|
415
438
|
// Rango, step 1 — direct carry-over. Your Suspense tree IS the hole model:
|
|
416
439
|
// hand the un-awaited promise down, keep the boundary, add the ppr option.
|
|
417
440
|
// No loader, no loading(), no restructuring.
|
|
441
|
+
import { Suspense } from "react";
|
|
442
|
+
import type { HandlerContext } from "@rangojs/router";
|
|
443
|
+
|
|
418
444
|
function ProductPage(ctx: HandlerContext) {
|
|
419
445
|
const price = fetchPrice(ctx.params.id); // pending promise — NOT awaited
|
|
420
446
|
return (
|
|
@@ -430,10 +456,9 @@ path("/products/:id", ProductPage, {
|
|
|
430
456
|
ppr: { ttl: 600, swr: 120 }, // or ppr: true (default ttl 300s)
|
|
431
457
|
});
|
|
432
458
|
|
|
433
|
-
// Rango, step 2 (optional refinement) — promote the fetch to a loader
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
// would bake into the shell). loading() is the loader's hole boundary.
|
|
459
|
+
// Rango, step 2 (optional refinement) — promote the fetch to a live loader
|
|
460
|
+
// (no ssr: false) for a GUARANTEED hole, even when the value resolves
|
|
461
|
+
// instantly; loading() is its boundary (/ppr → The loader lane rule).
|
|
437
462
|
path(
|
|
438
463
|
"/products/:id",
|
|
439
464
|
ProductPage,
|
|
@@ -449,20 +474,18 @@ Differences that matter during migration:
|
|
|
449
474
|
postpones at capture and becomes a hole — existing Next PPR trees keep
|
|
450
475
|
working as-is, no `loading()` required. One container rule everywhere
|
|
451
476
|
(handlers, handles, loaders): awaited/settled data bakes into the shell; a
|
|
452
|
-
promise nested inside your data stays a live hole. For loaders, `
|
|
453
|
-
selects the lane
|
|
454
|
-
|
|
455
|
-
the
|
|
456
|
-
nested promises stay live). Identity reads (`cookies()`/`headers()`) where
|
|
457
|
-
the value would bake refuse the capture by construction.
|
|
477
|
+
promise nested inside your data stays a live hole. For loaders, `ssr: false`
|
|
478
|
+
(not `loading()`) selects the lane — see `/ppr` → The loader lane rule.
|
|
479
|
+
Identity reads (`cookies()`/`headers()`) where the value would bake refuse
|
|
480
|
+
the capture by construction.
|
|
458
481
|
- **Shell freshness is explicit.** Next's PPR shell is fixed until the next
|
|
459
482
|
build; Rango's has `ttl`/`swr`/`tags` per route, and `updateTag()` /
|
|
460
483
|
`revalidateTag()` drop the shell (`revalidate()` does not — it is a data
|
|
461
484
|
lever and never touches shell HTML).
|
|
462
485
|
- **`cookies()`/`headers()` in shell material THROW during capture** (in Next
|
|
463
|
-
they silently force dynamic rendering). Per-user reads must move
|
|
464
|
-
|
|
465
|
-
|
|
486
|
+
they silently force dynamic rendering). Per-user reads must move into a live
|
|
487
|
+
loader (no `ssr: false`; a nested promise does not help). The refusal
|
|
488
|
+
surfaces at migration time, which is the point.
|
|
466
489
|
- **A store is required.** PPR needs the app-level `createRouter({ cache })`
|
|
467
490
|
store to implement the shell family (`MemorySegmentCacheStore`,
|
|
468
491
|
`CFCacheStore`, `VercelCacheStore`). Without one the route quietly stays
|
|
@@ -496,7 +519,8 @@ renders into). Then invalidate by tag:
|
|
|
496
519
|
`updateTag` is awaitable and immediate; `revalidateTag` is fire-and-forget. Both
|
|
497
520
|
hard-purge (the next read re-renders fresh); the only difference is awaitability —
|
|
498
521
|
despite the Next.js name, `revalidateTag` here is NOT stale-while-revalidate.
|
|
499
|
-
Built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`)
|
|
522
|
+
Built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`, `VercelCacheStore`)
|
|
523
|
+
index by tag. Next's
|
|
500
524
|
`revalidatePath` has no path-based equivalent — tag the relevant entries instead.
|
|
501
525
|
|
|
502
526
|
**2. Partial-render selection (which segments re-run after an action).** This is
|
|
@@ -507,7 +531,8 @@ re-rendering:
|
|
|
507
531
|
```typescript
|
|
508
532
|
import { updateBlog } from "./actions/blog";
|
|
509
533
|
|
|
510
|
-
// Re-run this layout when a blog action fires
|
|
534
|
+
// Re-run this layout when a blog action fires (layouts above the route are
|
|
535
|
+
// skipped after actions by default; undefined defers otherwise)
|
|
511
536
|
layout(BlogLayout, () => [
|
|
512
537
|
revalidate((ctx) => ctx.isAction(updateBlog) || undefined),
|
|
513
538
|
path("/blog/:slug", BlogPost, { name: "blogPost" }),
|
|
@@ -617,12 +642,14 @@ path("/dashboard", DashboardPage, { name: "dashboard" }, () => [
|
|
|
617
642
|
"use client";
|
|
618
643
|
export default function Error({ error, reset }) { ... }
|
|
619
644
|
|
|
620
|
-
// Rango: errorBoundary wrapping a group of routes
|
|
645
|
+
// Rango: errorBoundary wrapping a group of routes. The fallback renders on the
|
|
646
|
+
// server and receives only `error` — there is no `reset` (a server render
|
|
647
|
+
// cannot be retried in place; the user navigates away or reloads).
|
|
621
648
|
layout(<DashboardLayout />, () => [
|
|
622
|
-
errorBoundary(({ error
|
|
649
|
+
errorBoundary(({ error }) => (
|
|
623
650
|
<div>
|
|
624
651
|
<h2>Something went wrong</h2>
|
|
625
|
-
<
|
|
652
|
+
<p>{error.message}</p>
|
|
626
653
|
</div>
|
|
627
654
|
)),
|
|
628
655
|
path("/dashboard", DashboardIndex, { name: "dashboard" }),
|
|
@@ -630,6 +657,30 @@ layout(<DashboardLayout />, () => [
|
|
|
630
657
|
])
|
|
631
658
|
```
|
|
632
659
|
|
|
660
|
+
For client-side render errors with a "Try again" button (Next's `reset`), wrap
|
|
661
|
+
the client subtree in `ErrorBoundary` from `@rangojs/router/client`; its
|
|
662
|
+
function fallback receives `{ error, reset }`:
|
|
663
|
+
|
|
664
|
+
```tsx
|
|
665
|
+
"use client";
|
|
666
|
+
import { ErrorBoundary } from "@rangojs/router/client";
|
|
667
|
+
|
|
668
|
+
export function ChartPanel() {
|
|
669
|
+
return (
|
|
670
|
+
<ErrorBoundary
|
|
671
|
+
fallback={({ error, reset }) => (
|
|
672
|
+
<div>
|
|
673
|
+
<p>{error.message}</p>
|
|
674
|
+
<button onClick={reset}>Try again</button>
|
|
675
|
+
</div>
|
|
676
|
+
)}
|
|
677
|
+
>
|
|
678
|
+
<Chart />
|
|
679
|
+
</ErrorBoundary>
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
633
684
|
```typescript
|
|
634
685
|
// Next.js: app/not-found.tsx
|
|
635
686
|
export default function NotFound() { ... }
|
|
@@ -657,15 +708,17 @@ children in their scope — handlers, loaders, and nested segments.
|
|
|
657
708
|
|
|
658
709
|
## 6. Navigation
|
|
659
710
|
|
|
660
|
-
| Next.js
|
|
661
|
-
|
|
|
662
|
-
| `import Link from "next/link"`
|
|
663
|
-
| `<Link href="/about">`
|
|
664
|
-
| `useRouter().push("/about")`
|
|
665
|
-
| `useRouter().replace("/about")`
|
|
666
|
-
| `
|
|
667
|
-
| `
|
|
668
|
-
| `
|
|
711
|
+
| Next.js | Rango |
|
|
712
|
+
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
713
|
+
| `import Link from "next/link"` | `import { Link } from "@rangojs/router/client"` |
|
|
714
|
+
| `<Link href="/about">` | `<Link to="/about">` |
|
|
715
|
+
| `useRouter().push("/about")` | `useRouter().push("/about")` |
|
|
716
|
+
| `useRouter().replace("/about")` | `useRouter().replace("/about")` |
|
|
717
|
+
| `useRouter().back()` / `refresh()` / `prefetch(url)` | same methods on `useRouter()` from `@rangojs/router/client` |
|
|
718
|
+
| `useParams()` | `useParams()` from `@rangojs/router/client` (or `ctx.params` in a server handler) |
|
|
719
|
+
| `usePathname()` | `usePathname()` from `@rangojs/router/client` |
|
|
720
|
+
| `useSearchParams()` | `useSearchParams()` from `@rangojs/router/client` — returns an RR-style TUPLE, so destructure the reader: `const [searchParams] = useSearchParams()`; the second element is a setter Next does not have |
|
|
721
|
+
| `redirect("/login")` (server) | `redirect("/login")` from `@rangojs/router` |
|
|
669
722
|
|
|
670
723
|
### "Instant navigations" (Link prefetching)
|
|
671
724
|
|
|
@@ -699,22 +752,22 @@ has. Server-component routes and `clientUrls()` groups compose in one tree.
|
|
|
699
752
|
|
|
700
753
|
## 7. Server Actions
|
|
701
754
|
|
|
702
|
-
Server actions work the same way — `"use server"` directive, `useActionState`, form actions. No migration needed for action logic.
|
|
755
|
+
Server actions work the same way — `"use server"` directive, `useActionState`, form actions. No migration needed for action logic. Keep passing the imported action itself to `<form action={...}>` or `useActionState(action, initial)` (not a client-side closure around it) so the form still submits with JavaScript disabled.
|
|
703
756
|
|
|
704
757
|
Key difference: in Rango, route middleware does NOT wrap action execution. Actions only see global middleware context. Use `getRequestContext()` in actions to access `ctx.set()`/`ctx.get()`.
|
|
705
758
|
|
|
706
|
-
Next.js's `revalidateTag()` maps directly: tag entries via `cache({ tags })` / `cacheTag(...)`, then invalidate. **In a server action use `await updateTag(tag)`** — it is read-your-own-writes, so the action's own re-render sees fresh data; `revalidateTag(tag)` is a background (non-blocking) hard-purge and is NOT read-your-own-writes, so reserve it for route handlers / webhooks (calling it from an action can leave that action's re-render stale). `revalidatePath()` has no path-based equivalent — tag the route's entries instead. Separately,
|
|
759
|
+
Next.js's `revalidateTag()` maps directly: tag entries via `cache({ tags })` / `cacheTag(...)`, then invalidate. **In a server action use `await updateTag(tag)`** — it is read-your-own-writes, so the action's own re-render sees fresh data; `revalidateTag(tag)` is a background (non-blocking) hard-purge and is NOT read-your-own-writes, so reserve it for route handlers / webhooks (calling it from an action can leave that action's re-render stale). `revalidatePath()` has no path-based equivalent — tag the route's entries instead. Separately, the route, the segments inside its `path()`, and all loaders already re-run after every action; layouts and parallels above the route are skipped. To re-render one of those skipped segments after an action, attach `revalidate((ctx) => ctx.isAction(updateBlog) || undefined)` to it; to narrow a loader or route segment to one action, use `revalidate((ctx) => (ctx.isAction() ? ctx.isAction(updateBlog) : undefined))` (match the imported action by reference, as in §3). See `/server-actions` for the full pattern (validation, error handling, file uploads), `/caching` for tag invalidation, and `/loader` for revalidation rule semantics.
|
|
707
760
|
|
|
708
761
|
## 8. Metadata / Head
|
|
709
762
|
|
|
710
|
-
Rango uses the `Meta` handle + `<
|
|
763
|
+
Rango uses the `Meta` handle + `<Html.Meta />` client component:
|
|
711
764
|
|
|
712
765
|
```typescript
|
|
713
766
|
// Next.js: export const metadata = { title: "Home" }
|
|
714
767
|
// Next.js: export function generateMetadata({ params }) { ... }
|
|
715
768
|
|
|
716
|
-
// Rango: Meta handle in handlers (server),
|
|
717
|
-
import { Meta } from "@rangojs/router";
|
|
769
|
+
// Rango: Meta handle in handlers (server), Html.Meta in document <head> (client)
|
|
770
|
+
import { Meta, type Handler } from "@rangojs/router";
|
|
718
771
|
|
|
719
772
|
const HomePage: Handler<"home"> = (ctx) => {
|
|
720
773
|
const meta = ctx.use(Meta);
|
|
@@ -751,16 +804,16 @@ Without the flag the push still applies, but a slow loader's title lands
|
|
|
751
804
|
post-hydration instead of in the document — see `/loader` → "Writing Handles
|
|
752
805
|
from Loaders" for the delivery race.
|
|
753
806
|
|
|
754
|
-
Add `<
|
|
807
|
+
Add `<Html.Meta />` in the Document component's `<head>`:
|
|
755
808
|
|
|
756
809
|
```typescript
|
|
757
|
-
import {
|
|
810
|
+
import { Html } from "@rangojs/router/client";
|
|
758
811
|
|
|
759
812
|
function Document({ children }: { children: ReactNode }) {
|
|
760
813
|
return (
|
|
761
814
|
<html>
|
|
762
815
|
<head>
|
|
763
|
-
<
|
|
816
|
+
<Html.Meta />
|
|
764
817
|
</head>
|
|
765
818
|
<body>{children}</body>
|
|
766
819
|
</html>
|
|
@@ -835,13 +888,15 @@ See `/theme` for full API including system detection and cookie persistence.
|
|
|
835
888
|
4. [ ] Create `router.tsx` with `createRouter()`
|
|
836
889
|
5. [ ] Convert file-based routes to `urls()` DSL in `urls.tsx`
|
|
837
890
|
6. [ ] Migrate layouts to `layout()` with `<Outlet />`
|
|
838
|
-
7. [ ]
|
|
891
|
+
7. [ ] Keep inline server `fetch`/DB calls in handlers; add `createLoader()` +
|
|
892
|
+
`loader()` only where you need live, refreshable, or independently
|
|
893
|
+
cached data (§3)
|
|
839
894
|
8. [ ] Migrate `middleware.ts` to `router.use()` (auth, guards, logging)
|
|
840
895
|
9. [ ] Replace `next/link` with `Link` from `@rangojs/router/client`; keep
|
|
841
896
|
"instant navigations" via `prefetch="viewport"`/`defaultPrefetch` (§6)
|
|
842
897
|
10. [ ] Convert loading/error files to `loading()` / `errorBoundary()`
|
|
843
898
|
11. [ ] Migrate API routes to `path.json()` / `path.text()`
|
|
844
|
-
12. [ ] Update metadata to use `Meta` handle + `<
|
|
899
|
+
12. [ ] Update metadata to use `Meta` handle + `<Html.Meta />` in document head
|
|
845
900
|
(`generateMetadata` → loader push + `{ ssr: false }`)
|
|
846
901
|
13. [ ] Replace `next-themes` with `theme: true` in createRouter (see `/theme`)
|
|
847
902
|
14. [ ] Map rendering-mode segment config: `revalidate = N` → `cache({ ttl })`,
|
|
@@ -55,7 +55,9 @@ actor and tenant scope at the repository boundary, verify membership/role, and
|
|
|
55
55
|
perform the data query through that scope. Avoid exporting an unrestricted DB
|
|
56
56
|
handle to page handlers.
|
|
57
57
|
|
|
58
|
-
Tests must prove denial, not only success
|
|
58
|
+
Tests must prove denial, not only success (the `/testing` skill's `runMiddleware`,
|
|
59
|
+
`runLoader`, and `runInRequestContext` take your DB double through `env`; see its
|
|
60
|
+
`bindings.md`):
|
|
59
61
|
|
|
60
62
|
- a user cannot read another tenant by guessing an ID;
|
|
61
63
|
- a member cannot perform an owner/admin mutation;
|
|
@@ -102,7 +104,8 @@ For a Node-to-Workers move, search for:
|
|
|
102
104
|
|
|
103
105
|
Prefer web standards (`fetch`, `Request`, `Response`, WebCrypto) and provider
|
|
104
106
|
bindings. Test the production bundle in `vite preview`; a successful TypeScript
|
|
105
|
-
check does not prove an SDK runs in workerd.
|
|
107
|
+
check does not prove an SDK runs in workerd. `/cloudflare` covers bindings,
|
|
108
|
+
local D1, streaming, and deploying the built Worker.
|
|
106
109
|
|
|
107
110
|
## Plan cutover and rollback
|
|
108
111
|
|