@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
|
@@ -13,7 +13,7 @@ function NavIndicator() {
|
|
|
13
13
|
|
|
14
14
|
// State properties
|
|
15
15
|
nav.state; // 'idle' | 'loading'
|
|
16
|
-
nav.isStreaming; // boolean
|
|
16
|
+
nav.isStreaming; // boolean — RSC data is still streaming
|
|
17
17
|
nav.location; // Current URL
|
|
18
18
|
nav.pendingUrl; // Target URL during navigation (or null)
|
|
19
19
|
|
|
@@ -29,7 +29,9 @@ function IsLoading() {
|
|
|
29
29
|
|
|
30
30
|
### useRouter()
|
|
31
31
|
|
|
32
|
-
Access stable router actions
|
|
32
|
+
Access stable router actions. The returned object never changes identity, so
|
|
33
|
+
components using it do not re-render on navigation. Call its methods from
|
|
34
|
+
event handlers or effects:
|
|
33
35
|
|
|
34
36
|
```tsx
|
|
35
37
|
"use client";
|
|
@@ -38,15 +40,41 @@ import { useRouter } from "@rangojs/router/client";
|
|
|
38
40
|
function NavigationControls() {
|
|
39
41
|
const router = useRouter();
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<button onClick={() => router.push("/products")}>Products</button>
|
|
46
|
+
<button onClick={() => router.replace("/login", { scroll: false })}>
|
|
47
|
+
Log in
|
|
48
|
+
</button>
|
|
49
|
+
<button onClick={() => router.refresh()}>Reload data</button>
|
|
50
|
+
<button onMouseEnter={() => router.prefetch("/dashboard")}>
|
|
51
|
+
Dashboard
|
|
52
|
+
</button>
|
|
53
|
+
<button onClick={() => router.back()}>Back</button>
|
|
54
|
+
<button onClick={() => router.forward()}>Forward</button>
|
|
55
|
+
</>
|
|
56
|
+
);
|
|
47
57
|
}
|
|
48
58
|
```
|
|
49
59
|
|
|
60
|
+
| Method | Behaviour |
|
|
61
|
+
| ------------------------ | --------------------------------------------------------------------------------------------------------------- |
|
|
62
|
+
| `push(url, options?)` | Navigate and add a history entry. Returns a promise. |
|
|
63
|
+
| `replace(url, options?)` | Navigate and replace the current history entry. Returns a promise. |
|
|
64
|
+
| `refresh()` | Re-fetch the current route's server data, keeping client state. Returns a promise. |
|
|
65
|
+
| `prefetch(url, opts?)` | Prefetch a target. `{ key: ":source" }` scopes the entry to the source page (parity with `<Link prefetchKey>`). |
|
|
66
|
+
| `back()` | `history.back()`; on the first entry of the session it replaces to the app root instead of leaving the app. |
|
|
67
|
+
| `forward()` | `history.forward()`. |
|
|
68
|
+
|
|
69
|
+
`push` / `replace` options: `scroll` (`false` keeps the current scroll
|
|
70
|
+
position), `revalidate` (default `true`, see below), and `state` (location
|
|
71
|
+
state, see [`./state.md`](./state.md)).
|
|
72
|
+
|
|
73
|
+
Target resolution: a path starting with `/` is app-absolute and gets the
|
|
74
|
+
router `basename` prefixed. A relative path (`"cart"`, `"./cart"`) resolves
|
|
75
|
+
against the current `include()` mount, so `router.push("cart")` inside
|
|
76
|
+
`include("/shop", ...)` navigates to `/shop/cart`.
|
|
77
|
+
|
|
50
78
|
#### Skipping revalidation
|
|
51
79
|
|
|
52
80
|
Pass `revalidate: false` to skip the RSC server fetch for same-pathname navigations (search param or hash changes). The URL updates and all hooks re-render, but server components stay as-is.
|
|
@@ -79,7 +107,7 @@ function Breadcrumbs() {
|
|
|
79
107
|
const { path, segmentIds, location } = useSegments();
|
|
80
108
|
|
|
81
109
|
// path: ["shop", "products", "123"] (split on "/", no leading slash on any element)
|
|
82
|
-
// segmentIds: ["L0", "L0L1", "L0L1R0"] (opaque internal short-codes, not route names)
|
|
110
|
+
// segmentIds: ["L0", "L0L1", "L0L1R0"] (layouts and routes only; opaque internal short-codes, not route names)
|
|
83
111
|
// location: URL object
|
|
84
112
|
|
|
85
113
|
return <nav>{path.join(" > ")}</nav>;
|
|
@@ -91,7 +119,8 @@ const isShopRoute = useSegments((s) => s.path[0] === "shop");
|
|
|
91
119
|
|
|
92
120
|
### useLinkStatus()
|
|
93
121
|
|
|
94
|
-
Track pending state inside a Link component
|
|
122
|
+
Track pending state inside a Link component. `pending` is `true` while a
|
|
123
|
+
navigation to this link's `to` is in flight:
|
|
95
124
|
|
|
96
125
|
```tsx
|
|
97
126
|
"use client";
|
package/skills/hooks/outlets.md
CHANGED
|
@@ -2,24 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
### Outlet / ParallelOutlet
|
|
4
4
|
|
|
5
|
-
Render child content in layouts
|
|
5
|
+
Render child content in layouts. `<Outlet />` renders the matched child
|
|
6
|
+
segment; `<ParallelOutlet name="@slot" />` renders a named `parallel()` slot.
|
|
7
|
+
Both apply the segment's `loading()` fallback as a Suspense boundary:
|
|
6
8
|
|
|
7
9
|
```tsx
|
|
8
10
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
9
11
|
|
|
10
|
-
function DashboardLayout(
|
|
12
|
+
function DashboardLayout() {
|
|
11
13
|
return (
|
|
12
14
|
<div className="dashboard">
|
|
13
15
|
<aside>
|
|
14
16
|
<ParallelOutlet name="@sidebar" />
|
|
15
17
|
</aside>
|
|
16
|
-
<main>
|
|
18
|
+
<main>
|
|
19
|
+
<Outlet />
|
|
20
|
+
</main>
|
|
17
21
|
<ParallelOutlet name="@notifications" />
|
|
18
22
|
</div>
|
|
19
23
|
);
|
|
20
24
|
}
|
|
21
25
|
```
|
|
22
26
|
|
|
27
|
+
`<Outlet name="@sidebar" />` is equivalent to `<ParallelOutlet name="@sidebar" />`;
|
|
28
|
+
`ParallelOutlet` requires the name and reads clearer at the call site. See
|
|
29
|
+
`/layout` and `/parallel` for the server side.
|
|
30
|
+
|
|
23
31
|
### useOutlet()
|
|
24
32
|
|
|
25
33
|
Access outlet content and its client-route presentation state programmatically:
|
|
@@ -48,9 +56,8 @@ function ConditionalLayout() {
|
|
|
48
56
|
}
|
|
49
57
|
```
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`<Outlet />` itself is unchanged.
|
|
59
|
+
Migrating from the older API where `useOutlet()` returned the node directly:
|
|
60
|
+
destructure `content` and render it where you rendered the hook result.
|
|
54
61
|
|
|
55
62
|
`pending` is narrow. After hydration, a `clientUrls()` layout receives `true`
|
|
56
63
|
while a browser-local match to a different client route beneath it is presenting
|
package/skills/hooks/state.md
CHANGED
|
@@ -74,6 +74,16 @@ Plain state can also be evaluated just in time (also requires a client component
|
|
|
74
74
|
</Link>
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
Read plain (untyped) state by calling the hook with a type argument and no
|
|
78
|
+
definition:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
const state = useLocationState<{ from?: string }>(); // { from?: string } | undefined
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The same `state` option exists on `router.push()` / `router.replace()` (see
|
|
85
|
+
[`./navigation.md`](./navigation.md)).
|
|
86
|
+
|
|
77
87
|
### Flash State (read-once)
|
|
78
88
|
|
|
79
89
|
Create a location state with `{ flash: true }` for read-once state that
|
|
@@ -106,22 +116,22 @@ function FlashBanner() {
|
|
|
106
116
|
```
|
|
107
117
|
|
|
108
118
|
Flash behavior is determined by the definition (`{ flash: true }`), not by which
|
|
109
|
-
hook reads it. `useLocationState` reads the value
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
hook reads it. `useLocationState` reads the value during render (on the
|
|
120
|
+
hydration render it returns `undefined` and reads in a post-mount effect, so
|
|
121
|
+
SSR and hydration agree), then clears it from `history.state` via
|
|
122
|
+
`replaceState` in a `useEffect`. Multiple components reading the same flash
|
|
123
|
+
definition all see the value. Pressing back/forward will not re-show the flash
|
|
124
|
+
since it was cleared.
|
|
113
125
|
|
|
114
|
-
Set flash state from the server via `redirect()` with state
|
|
126
|
+
Set flash state from the server via `redirect()` with state (from a handler,
|
|
127
|
+
middleware, or server action). Import the definition from the shared module so
|
|
128
|
+
the client reader uses the same one:
|
|
115
129
|
|
|
116
130
|
```tsx
|
|
117
131
|
// In a route handler
|
|
118
|
-
import { redirect
|
|
119
|
-
|
|
120
|
-
export const FlashMessage = createLocationState<{ text: string }>({
|
|
121
|
-
flash: true,
|
|
122
|
-
});
|
|
132
|
+
import { redirect } from "@rangojs/router";
|
|
133
|
+
import { FlashMessage } from "../location-states";
|
|
123
134
|
|
|
124
|
-
// Handler
|
|
125
135
|
(ctx) => {
|
|
126
136
|
return redirect("/dashboard", {
|
|
127
137
|
state: [FlashMessage({ text: "Item saved!" })],
|
|
@@ -129,7 +139,7 @@ export const FlashMessage = createLocationState<{ text: string }>({
|
|
|
129
139
|
};
|
|
130
140
|
```
|
|
131
141
|
|
|
132
|
-
Or via `ctx.setLocationState()` on any response:
|
|
142
|
+
Or via `ctx.setLocationState()` on any response (handlers and middleware):
|
|
133
143
|
|
|
134
144
|
```tsx
|
|
135
145
|
(ctx) => {
|
|
@@ -194,9 +204,21 @@ ProductState.delete();
|
|
|
194
204
|
Force the client's caches to miss after a mutation the router can't see (a REST
|
|
195
205
|
call, a WebSocket push, a login). It is a plain function, not a hook, so it works
|
|
196
206
|
from module-level callbacks too. Imported from the root entry `@rangojs/router`,
|
|
197
|
-
it is selected by export conditions:
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
it is selected by export conditions:
|
|
208
|
+
|
|
209
|
+
- **Client** (client component or module): marks the history cache stale,
|
|
210
|
+
flushes the prefetch cache, rotates the router state, and notifies other tabs
|
|
211
|
+
— the same thing a completed server action does. During SSR of a client
|
|
212
|
+
component it is a no-op (dev warning).
|
|
213
|
+
- **Server** (handler, server component, middleware, loader, or action): writes
|
|
214
|
+
a rotated state `Set-Cookie` for the responding client, which marks its
|
|
215
|
+
caches stale when it next reads them. Idempotent within a request; throws
|
|
216
|
+
inside a `cache()` / `"use cache"` boundary; a no-op with a dev warning
|
|
217
|
+
outside a request.
|
|
218
|
+
|
|
219
|
+
Server actions already invalidate automatically; to suppress that for a no-op
|
|
220
|
+
action, call `keepClientCache()` inside it (see `/server-actions` → "Client
|
|
221
|
+
Cache After an Action").
|
|
200
222
|
|
|
201
223
|
```tsx
|
|
202
224
|
"use client";
|
package/skills/hooks/urls.md
CHANGED
|
@@ -29,7 +29,7 @@ function ProductId() {
|
|
|
29
29
|
}
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Returns merged params from all matched route segments as a `Readonly<T>` map. Updates on navigation commit
|
|
32
|
+
Returns merged params from all matched route segments as a `Readonly<T>` map (default `Record<string, string | undefined>` — absent optional params are omitted, so guard them). Updates on navigation commit, not during a pending navigation — except inside an optimistically rendered `clientUrls()` destination, where it reports that destination's params (see `/client-urls`).
|
|
33
33
|
|
|
34
34
|
### usePathname()
|
|
35
35
|
|
|
@@ -119,19 +119,21 @@ import { useHref, href, Link } from "@rangojs/router/client";
|
|
|
119
119
|
|
|
120
120
|
// Inside include("/shop", shopPatterns)
|
|
121
121
|
function ShopNav() {
|
|
122
|
-
const
|
|
122
|
+
const localHref = useHref();
|
|
123
123
|
|
|
124
124
|
return (
|
|
125
125
|
<>
|
|
126
126
|
{/* Local paths - auto-prefixed with /shop */}
|
|
127
|
-
<Link to={
|
|
128
|
-
<Link to={
|
|
127
|
+
<Link to={localHref("/cart")}>Cart</Link>
|
|
128
|
+
<Link to={localHref("/product/widget")}>Widget</Link>
|
|
129
|
+
{/* Absolute path - not prefixed */}
|
|
130
|
+
<Link to={href("/about")}>About</Link>
|
|
129
131
|
</>
|
|
130
132
|
);
|
|
131
133
|
}
|
|
132
134
|
```
|
|
133
135
|
|
|
134
|
-
Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths.
|
|
136
|
+
Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths. The function `useHref()` returns is referentially stable within a mount, so it is safe as a hook dependency or memoized prop.
|
|
135
137
|
|
|
136
138
|
### useMount()
|
|
137
139
|
|
|
@@ -8,6 +8,12 @@ argument-hint:
|
|
|
8
8
|
|
|
9
9
|
Route requests to different apps based on domain, subdomain, or path prefix patterns. Supports middleware, lazy loading, cookie-based host override for dev, and a fallback handler.
|
|
10
10
|
|
|
11
|
+
Use it when one deployment serves several independently built Rango apps (or
|
|
12
|
+
plain request handlers) and the hostname or a path prefix decides which one
|
|
13
|
+
handles the request. Each sub-app is an ordinary `createRouter()` app; the host
|
|
14
|
+
router only picks which one runs. For a single app mounted under a sub-path,
|
|
15
|
+
use `basename` instead (`/router-setup`).
|
|
16
|
+
|
|
11
17
|
## Import
|
|
12
18
|
|
|
13
19
|
```typescript
|
|
@@ -64,6 +70,10 @@ import { router } from "./router.js";
|
|
|
64
70
|
export default (request: Request, input: any) => router.fetch(request, input);
|
|
65
71
|
```
|
|
66
72
|
|
|
73
|
+
Each sub-app has its own generated route map and `Rango.Env`; typecheck each
|
|
74
|
+
in its own TypeScript program (or give route names per-app prefixes) so the
|
|
75
|
+
global augmentations don't collide — see `/typesafety` (multi-project setup).
|
|
76
|
+
|
|
67
77
|
Selecting the host entry — a host app has several `createRouter()` sub-apps, so single-router auto-discovery can't pick one. Either let rango auto-detect the lone `createHostRouter()` file, or point at it explicitly:
|
|
68
78
|
|
|
69
79
|
```typescript
|
|
@@ -71,7 +81,7 @@ Selecting the host entry — a host app has several `createRouter()` sub-apps, s
|
|
|
71
81
|
rango({ preset: "vercel", hostRouter: "./src/worker.rsc.tsx" });
|
|
72
82
|
```
|
|
73
83
|
|
|
74
|
-
On Vercel this is a single function running `hostRouter.match()` for every request (mirrors the Cloudflare single-worker model); `{ env, ctx }` (`process.env` + `{ waitUntil }`) is threaded unchanged to each matched sub-app's handler and `cache(env, ctx)` factory. See
|
|
84
|
+
On Vercel this is a single function running `hostRouter.match()` for every request (mirrors the Cloudflare single-worker model); `{ env, ctx }` (`process.env` + `{ waitUntil }`) is threaded unchanged to each matched sub-app's handler and `cache(env, ctx)` factory. See `/vercel`.
|
|
75
85
|
|
|
76
86
|
Unmatched hosts on node/vercel: because rango owns the generated entry (you have no worker `try/catch`), it catches `NoRouteMatchError` and returns **404** by default — so you do **not** need a catch-all host route. If you want different behavior (a branded 404, a redirect, a default app), register a catch-all mount as the **last** route, e.g. `host(["**"]).lazy(() => import("./apps/site/handler.js"))` — it matches any host, so the built-in 404 only fires when nothing matched at all. (Note `fallback()` is for cookie-override errors, not general unmatched hosts.)
|
|
77
87
|
|
|
@@ -120,6 +130,18 @@ Why two methods instead of one overloaded `.map()`:
|
|
|
120
130
|
|
|
121
131
|
Patterns are tested in registration order. First match wins.
|
|
122
132
|
|
|
133
|
+
Matching details:
|
|
134
|
+
|
|
135
|
+
- **Apex means exactly two labels.** There is no Public Suffix List:
|
|
136
|
+
`example.co.uk` has three labels, so `.`/`*` do not match it and `*.` does.
|
|
137
|
+
`localhost` has one label, so it matches neither `.` nor `*.`; use an exact
|
|
138
|
+
`localhost` pattern (or `**`) for local dev.
|
|
139
|
+
- **Hosts are case-insensitive; paths are not.** A path prefix matches the
|
|
140
|
+
exact path or anything below it (`example.com/api` matches `"/api"` and
|
|
141
|
+
`"/api/x"`, not `"/apix"`). A trailing `/` on the pattern is ignored.
|
|
142
|
+
- A pattern that is empty, not a string, or contains whitespace throws
|
|
143
|
+
`InvalidPatternError` at registration.
|
|
144
|
+
|
|
123
145
|
## `defineHosts` for Type Safety
|
|
124
146
|
|
|
125
147
|
```typescript
|
|
@@ -142,6 +164,8 @@ Returns a frozen object — keys are autocompleted by TypeScript.
|
|
|
142
164
|
Global middleware runs for every matched route. Per-route middleware runs only for that host pattern.
|
|
143
165
|
|
|
144
166
|
```typescript
|
|
167
|
+
import { createHostRouter, type Middleware } from "@rangojs/router/host";
|
|
168
|
+
|
|
145
169
|
const router = createHostRouter();
|
|
146
170
|
|
|
147
171
|
// Global — runs for all routes
|
|
@@ -150,7 +174,14 @@ router.use(async (request, input, next) => {
|
|
|
150
174
|
return next();
|
|
151
175
|
});
|
|
152
176
|
|
|
153
|
-
// Per-route
|
|
177
|
+
// Per-route: return a Response without calling next() to short-circuit
|
|
178
|
+
const requireAuth: Middleware = async (request, input, next) => {
|
|
179
|
+
if (!request.headers.get("authorization")) {
|
|
180
|
+
return new Response("Unauthorized", { status: 401 });
|
|
181
|
+
}
|
|
182
|
+
return next();
|
|
183
|
+
};
|
|
184
|
+
|
|
154
185
|
router
|
|
155
186
|
.host(["admin.*"])
|
|
156
187
|
.use(requireAuth)
|
|
@@ -159,23 +190,37 @@ router
|
|
|
159
190
|
|
|
160
191
|
Middleware signature: `(request: Request, input: RouterRequestInput, next: () => Promise<Response>) => Promise<Response>`
|
|
161
192
|
|
|
193
|
+
Global middleware runs first, then the matched route's middleware, then the
|
|
194
|
+
handler. `input` is typed `RouterRequestInput<any>` on purpose: one host router
|
|
195
|
+
fans out to sub-apps with different env shapes, so `input.env`/`input.vars` are
|
|
196
|
+
`any` here and each sub-app types its own env via `createRouter<TEnv>()`.
|
|
197
|
+
|
|
162
198
|
Calling `next()` more than once throws.
|
|
163
199
|
|
|
164
200
|
## Fallback Handler
|
|
165
201
|
|
|
166
|
-
Handles cookie-override errors when `hostOverride` is configured (e.g., override from a disallowed host, invalid cookie hostname). The fallback does **not** catch unmatched hosts — those throw `NoRouteMatchError`. Catch that at the worker level if you need a 404.
|
|
202
|
+
Handles cookie-override errors when `hostOverride` is configured (e.g., override from a disallowed host, invalid cookie hostname, a throwing `validate`). The fallback does **not** catch unmatched hosts — those throw `NoRouteMatchError`. Catch that at the worker level if you need a 404.
|
|
167
203
|
|
|
168
204
|
```typescript
|
|
169
205
|
const router = createHostRouter({
|
|
170
206
|
hostOverride: { cookieName: "x-dev-host", allowedHosts: ["localhost"] },
|
|
171
207
|
});
|
|
172
208
|
|
|
173
|
-
// Called when cookie override fails (not for general unmatched hosts)
|
|
174
|
-
|
|
175
|
-
|
|
209
|
+
// Called when cookie override fails (not for general unmatched hosts).
|
|
210
|
+
// Global middleware and the fallback's own middleware still run.
|
|
211
|
+
router.fallback().map((request, input) => {
|
|
212
|
+
// The override error is passed on the input (not part of RouterRequestInput's type)
|
|
213
|
+
const { error } = input as { error?: Error };
|
|
214
|
+
return new Response(`Invalid host override: ${error?.message}`, {
|
|
215
|
+
status: 400,
|
|
216
|
+
});
|
|
176
217
|
});
|
|
177
218
|
```
|
|
178
219
|
|
|
220
|
+
Without a registered fallback, a failed override returns a `400` JSON
|
|
221
|
+
response (`{ error, message }`) that also clears the override cookie
|
|
222
|
+
(`Set-Cookie: <cookieName>=; Max-Age=0`).
|
|
223
|
+
|
|
179
224
|
For unmatched hosts without `hostOverride`, catch `NoRouteMatchError` in your worker fetch. Use the `isNoRouteMatchError()` guard rather than a bare `instanceof`: a workspace with a duplicated `@rangojs/router` copy can throw the error with a different class identity, and `instanceof` would then turn the 404 into an opaque 500.
|
|
180
225
|
|
|
181
226
|
```typescript
|
|
@@ -215,10 +260,14 @@ const router = createHostRouter({
|
|
|
215
260
|
When a request arrives:
|
|
216
261
|
|
|
217
262
|
1. If no cookie → use actual hostname
|
|
218
|
-
2. If cookie present and host
|
|
219
|
-
3. If cookie present but host not allowed →
|
|
263
|
+
2. If cookie present and the actual host matches an `allowedHosts` pattern → use the cookie value as the hostname (after `validate`, when given)
|
|
264
|
+
3. If cookie present but the host is not allowed → `HostOverrideNotAllowedError`, handled by the fallback (see above)
|
|
220
265
|
|
|
221
|
-
Without a custom `validate`, the cookie value
|
|
266
|
+
Without a custom `validate`, the cookie value must parse as a bare hostname via
|
|
267
|
+
`new URL()` (no port or path) and is lowercased; otherwise
|
|
268
|
+
`InvalidHostnameError`. A custom `validate` returns the effective hostname; if
|
|
269
|
+
it throws, the error is wrapped in `HostValidationError`. The effective
|
|
270
|
+
hostname is then matched against the host patterns as usual.
|
|
222
271
|
|
|
223
272
|
## Debug Mode
|
|
224
273
|
|
|
@@ -251,12 +300,15 @@ matchesHost(
|
|
|
251
300
|
new Request("https://foo.workers.dev/admin"),
|
|
252
301
|
); // true
|
|
253
302
|
|
|
254
|
-
// Create requests for integration tests
|
|
303
|
+
// Create requests for integration tests (method defaults to GET, path to "/")
|
|
255
304
|
const request = createTestRequest({
|
|
256
305
|
host: "admin.example.com",
|
|
257
306
|
path: "/dashboard",
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: { authorization: "Bearer test" },
|
|
258
309
|
cookies: { "x-dev-host": "api.example.com" },
|
|
259
310
|
});
|
|
311
|
+
const response = await router.match(request);
|
|
260
312
|
|
|
261
313
|
// Test which route would match (without executing)
|
|
262
314
|
router.test("admin.example.com"); // { pattern, handler, kind } | null
|
|
@@ -309,8 +361,8 @@ document navigation**, not a soft in-tree swap. When the server sees a partial
|
|
|
309
361
|
Why a reload rather than a soft swap: a soft swap can't faithfully re-establish
|
|
310
362
|
the target app's **document-level** state. Stylesheets shared across apps are
|
|
311
363
|
dropped by React 19's by-`href` resource dedup; and theme, warmup, and
|
|
312
|
-
prefetch-TTL are document-lifetime (captured once at load
|
|
313
|
-
|
|
364
|
+
prefetch-TTL are document-lifetime (captured once at document load), so the
|
|
365
|
+
target app's config would never take effect. A
|
|
314
366
|
full document load re-establishes the target app's entire document — CSS, theme,
|
|
315
367
|
meta, everything — by construction. So you do **not** need to coordinate
|
|
316
368
|
stylesheet `href`s, `precedence`, theme config, etc. across independently-authored
|
package/skills/i18n/SKILL.md
CHANGED
|
@@ -32,6 +32,12 @@ import { menuRoutes } from "./menu";
|
|
|
32
32
|
export const urlpatterns = urls(({ include }) => [
|
|
33
33
|
include("/:locale?", menuRoutes, { name: "menu" }),
|
|
34
34
|
]);
|
|
35
|
+
|
|
36
|
+
// menu.tsx
|
|
37
|
+
export const menuRoutes = urls(({ path }) => [
|
|
38
|
+
path("/", MenuIndex, { name: "index" }),
|
|
39
|
+
path("/c/:slug", MenuCategory, { name: "category" }),
|
|
40
|
+
]);
|
|
35
41
|
```
|
|
36
42
|
|
|
37
43
|
URLs that match:
|
|
@@ -59,7 +65,8 @@ URLs that match:
|
|
|
59
65
|
Absent optionals are `undefined` (not `""`), so `??` coalesces correctly:
|
|
60
66
|
|
|
61
67
|
```typescript
|
|
62
|
-
import { Handler } from "@rangojs/router";
|
|
68
|
+
import type { Handler } from "@rangojs/router";
|
|
69
|
+
import { resolveLocale } from "../lib/locale";
|
|
63
70
|
|
|
64
71
|
export const MenuIndex: Handler<"menu.index"> = (ctx) => {
|
|
65
72
|
// ctx.params.locale is `string | undefined`
|
|
@@ -87,9 +94,9 @@ import { cookies, headers } from "@rangojs/router";
|
|
|
87
94
|
|
|
88
95
|
export const SUPPORTED_LOCALES = ["en", "gb", "fr"] as const;
|
|
89
96
|
export type Locale = (typeof SUPPORTED_LOCALES)[number];
|
|
90
|
-
const DEFAULT_LOCALE: Locale = "en";
|
|
97
|
+
export const DEFAULT_LOCALE: Locale = "en";
|
|
91
98
|
|
|
92
|
-
const isSupported = (v: string): v is Locale =>
|
|
99
|
+
export const isSupported = (v: string): v is Locale =>
|
|
93
100
|
(SUPPORTED_LOCALES as readonly string[]).includes(v);
|
|
94
101
|
|
|
95
102
|
export function resolveLocale(ctx: {
|
|
@@ -104,24 +111,35 @@ export function resolveLocale(ctx: {
|
|
|
104
111
|
const accept = headers().get("accept-language") ?? "";
|
|
105
112
|
for (const tag of accept.split(",")) {
|
|
106
113
|
const code = tag.split(";")[0].trim().split("-")[0];
|
|
107
|
-
if (isSupported(code)) return code
|
|
114
|
+
if (isSupported(code)) return code;
|
|
108
115
|
}
|
|
109
116
|
return DEFAULT_LOCALE;
|
|
110
117
|
}
|
|
111
118
|
```
|
|
112
119
|
|
|
120
|
+
`cookies()` and `headers()` read the current request, so the helper works in
|
|
121
|
+
handlers, layouts, loaders, and middleware without passing the request in.
|
|
122
|
+
|
|
113
123
|
If you want to redirect to the canonical URL when the resolved locale
|
|
114
124
|
doesn't match the URL (e.g., user has `gb` cookie but visits `/`), do
|
|
115
|
-
that in
|
|
125
|
+
that in global middleware (`router.use()`). Global middleware runs before
|
|
126
|
+
route matching, and its `ctx.params` come from its own pattern — not from
|
|
127
|
+
`include("/:locale?")` — so read the locale segment from the pathname:
|
|
116
128
|
|
|
117
129
|
```typescript
|
|
130
|
+
// router.tsx
|
|
118
131
|
import { redirect } from "@rangojs/router";
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
import { DEFAULT_LOCALE, isSupported, resolveLocale } from "./lib/locale";
|
|
133
|
+
|
|
134
|
+
router.use(async (ctx, next) => {
|
|
135
|
+
if (ctx.request.method !== "GET") return next(); // leave actions alone
|
|
136
|
+
const first = ctx.url.pathname.split("/")[1] ?? "";
|
|
137
|
+
if (!isSupported(first)) {
|
|
138
|
+
const resolved = resolveLocale({ params: {} }); // cookie → Accept-Language → default
|
|
139
|
+
if (resolved !== DEFAULT_LOCALE) {
|
|
140
|
+
const rest = ctx.url.pathname === "/" ? "" : ctx.url.pathname;
|
|
141
|
+
return redirect(`/${resolved}${rest}${ctx.url.search}`);
|
|
142
|
+
}
|
|
125
143
|
}
|
|
126
144
|
await next();
|
|
127
145
|
});
|
|
@@ -138,9 +156,16 @@ ctx.reverse("menu.index", { locale: "" }); // → "/"
|
|
|
138
156
|
ctx.reverse("menu.index", { locale: undefined }); // → "/"
|
|
139
157
|
ctx.reverse("menu.index", { locale: "en" }); // → "/en"
|
|
140
158
|
ctx.reverse("menu.category", { locale: "en", slug: "breads" }); // → "/en/c/breads"
|
|
141
|
-
ctx.reverse("menu.category", { slug: "breads" }); // → "/c/breads"
|
|
159
|
+
ctx.reverse("menu.category", { slug: "breads" }); // → "/c/breads" on an unprefixed request
|
|
142
160
|
```
|
|
143
161
|
|
|
162
|
+
`ctx.reverse()` auto-fills missing params from the current request, so the
|
|
163
|
+
last call returns `/en/c/breads` when the current URL is `/en/...`. That is
|
|
164
|
+
what you want for in-locale links; to leave the locale, pass it explicitly
|
|
165
|
+
(`{ locale: "fr" }`, or `undefined`/`""` for the bare default). Middleware and
|
|
166
|
+
response routes are the exception: their `ctx.reverse` fills nothing from the
|
|
167
|
+
request, so pass `locale` there explicitly.
|
|
168
|
+
|
|
144
169
|
If the active locale is the app default and your URL strategy hides it
|
|
145
170
|
(`"en"` → `/`, others → `/<locale>`), normalize before calling reverse:
|
|
146
171
|
|
|
@@ -177,28 +202,27 @@ export async function loadMessages(locale: Locale) {
|
|
|
177
202
|
|
|
178
203
|
### Server layout: hand off to the client provider
|
|
179
204
|
|
|
205
|
+
A layout handler receives the handler context (not props) and renders its
|
|
206
|
+
children with `<Outlet />`. It sees the matched route's params, including
|
|
207
|
+
`locale` from the include below it:
|
|
208
|
+
|
|
180
209
|
```tsx
|
|
181
|
-
// layouts/intl-layout.tsx (server
|
|
182
|
-
import type {
|
|
210
|
+
// layouts/intl-layout.tsx (server)
|
|
211
|
+
import type { Handler } from "@rangojs/router";
|
|
212
|
+
import { Outlet } from "@rangojs/router/client";
|
|
183
213
|
import { resolveLocale } from "../lib/locale";
|
|
184
214
|
import { loadMessages } from "../lib/messages";
|
|
185
215
|
import { IntlClientProvider } from "../components/intl-client-provider";
|
|
186
216
|
|
|
187
|
-
export async
|
|
188
|
-
ctx,
|
|
189
|
-
children,
|
|
190
|
-
}: {
|
|
191
|
-
ctx: any;
|
|
192
|
-
children: ReactNode;
|
|
193
|
-
}) {
|
|
217
|
+
export const IntlLayout: Handler = async (ctx) => {
|
|
194
218
|
const locale = resolveLocale(ctx);
|
|
195
219
|
const messages = await loadMessages(locale);
|
|
196
220
|
return (
|
|
197
221
|
<IntlClientProvider locale={locale} messages={messages}>
|
|
198
|
-
|
|
222
|
+
<Outlet />
|
|
199
223
|
</IntlClientProvider>
|
|
200
224
|
);
|
|
201
|
-
}
|
|
225
|
+
};
|
|
202
226
|
```
|
|
203
227
|
|
|
204
228
|
### Client provider
|
|
@@ -252,20 +276,22 @@ export const urlpatterns = urls(({ layout, include }) => [
|
|
|
252
276
|
```
|
|
253
277
|
|
|
254
278
|
`<FormattedMessage>`, `useIntl()`, etc. work in any client component
|
|
255
|
-
under the layout. Server components can use
|
|
256
|
-
|
|
279
|
+
under the layout. Server components and handlers can't use React context
|
|
280
|
+
providers from the client; call `createIntl({ locale, messages })` (from
|
|
281
|
+
`@formatjs/intl` or `react-intl`) directly with the same `messages` map for
|
|
282
|
+
server-rendered text.
|
|
257
283
|
|
|
258
284
|
## Common Pitfalls
|
|
259
285
|
|
|
260
|
-
| Pitfall | Fix
|
|
261
|
-
| ------------------------------------------------------------- |
|
|
262
|
-
| `ctx.params.locale === ""` returns `false` | Absent optionals are `undefined`, not `""`. Use `=== undefined` or `??`.
|
|
263
|
-
|
|
|
264
|
-
|
|
|
265
|
-
|
|
|
266
|
-
|
|
|
267
|
-
|
|
|
268
|
-
|
|
|
286
|
+
| Pitfall | Fix |
|
|
287
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
288
|
+
| `ctx.params.locale === ""` returns `false` | Absent optionals are `undefined`, not `""`. Use `=== undefined` or `??`. |
|
|
289
|
+
| Unknown locale (e.g. `/de`) matches as `locale: "de"` | Add a constraint: `:locale(en\|gb\|fr)?`. Unknown values now 404. |
|
|
290
|
+
| Link meant for the default locale keeps `/en` | `ctx.reverse()` auto-fills the current `locale`. Pass `{ locale: undefined }` (or `""`) to drop it. |
|
|
291
|
+
| Reverse produces `//c/breads` for absent locale | `reverse()` collapses `undefined`/`""` segments — should not happen. File a bug. |
|
|
292
|
+
| Locale switcher loses search params | Append `ctx.url.search` to the reversed URL, or pass `Object.fromEntries(ctx.searchParams)` as the third argument. |
|
|
293
|
+
| Global middleware's `ctx.params.locale` is always `undefined` | Global middleware params come from its own pattern. Parse the first path segment (see "Locale Resolution"). |
|
|
294
|
+
| Route middleware doesn't see actions | Route middleware doesn't wrap action execution. Use global `router.use()` for action-time locale logic. |
|
|
269
295
|
|
|
270
296
|
## Cross-references
|
|
271
297
|
|