@rangojs/router 0.7.0 → 0.9.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/dist/types/cache/cf/cf-cache-store.d.ts +30 -16
- package/dist/types/cache/shell-snapshot.d.ts +2 -2
- package/dist/types/cache/types.d.ts +26 -7
- package/dist/types/client-urls/server-projection.d.ts +4 -4
- package/dist/types/client-urls/types.d.ts +13 -12
- package/dist/types/route-definition/helpers-types.d.ts +6 -5
- package/dist/types/router/segment-resolution/loader-mask.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +14 -4
- package/dist/types/rsc/types.d.ts +8 -0
- package/dist/types/server/context.d.ts +1 -1
- package/dist/types/server/request-context.d.ts +1 -1
- package/dist/types/ssr/index.d.ts +16 -0
- package/dist/types/ssr/ssr-root.d.ts +5 -0
- package/dist/types/types/loader-types.d.ts +24 -22
- package/dist/types/urls/path-helper-types.d.ts +8 -7
- package/dist/vite/index.js +8 -5
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +2 -2
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +9 -8
- package/skills/cloudflare/SKILL.md +5 -3
- package/skills/comparison/references/framework-comparison.md +2 -2
- package/skills/hooks/data.md +1 -1
- package/skills/hooks/handle-and-actions.md +1 -1
- package/skills/loader/SKILL.md +29 -28
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/migrate-react-router/component-migration.md +1 -1
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/migrate-react-router/route-mapping.md +1 -1
- package/skills/parallel/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +12 -8
- package/skills/rango/SKILL.md +20 -20
- package/skills/router-setup/SKILL.md +1 -1
- package/skills/scripts/SKILL.md +1 -1
- package/skills/shell-manifest/SKILL.md +1 -1
- package/src/browser/react/Link.tsx +27 -4
- package/src/cache/cf/cf-cache-store.ts +118 -62
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/types.ts +27 -7
- package/src/client-urls/client-urls.ts +12 -9
- package/src/client-urls/server-projection.ts +6 -6
- package/src/client-urls/types.ts +13 -12
- package/src/route-definition/dsl-helpers.ts +7 -3
- package/src/route-definition/helpers-types.ts +6 -5
- package/src/router/loader-resolution.ts +3 -3
- package/src/router/segment-resolution/fresh.ts +2 -2
- package/src/router/segment-resolution/loader-cache.ts +1 -1
- package/src/router/segment-resolution/loader-mask.ts +1 -1
- package/src/rsc/rsc-rendering.ts +10 -0
- package/src/rsc/shell-build-manifest.ts +13 -6
- package/src/rsc/shell-capture.ts +32 -5
- package/src/rsc/ssr-setup.ts +4 -0
- package/src/rsc/types.ts +18 -2
- package/src/server/context.ts +1 -1
- package/src/server/request-context.ts +1 -1
- package/src/ssr/index.tsx +22 -2
- package/src/ssr/ssr-root.tsx +17 -1
- package/src/types/loader-types.ts +21 -19
- package/src/urls/path-helper-types.ts +8 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: client-urls
|
|
3
|
-
description: Define client-component route groups with clientUrls() in a "use client" module — no handlers, useLoader at read sites, client-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the
|
|
3
|
+
description: Define client-component route groups with clientUrls() in a "use client" module — no handlers, useLoader at read sites, client-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the ssr:false SSR-completeness opt-in. Use when a route group's pages are client components, when building high-navigation-speed UIs (dashboards, admin panels, settings) where transitions must feel instant, when moving data reads from handler ctx.use() to useLoader at consumption sites, or when asking how routes defined in a "use client" file work.
|
|
4
4
|
argument-hint: "[setup]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -122,7 +122,7 @@ typing work exactly as for server routes (`/typesafety`).
|
|
|
122
122
|
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
123
|
| `path()` | Options are `name`, `search`, `trailingSlash`, `ppr` (shell caching — see /ppr skill; loader routes need `loading()` or capture refuses); no response variants |
|
|
124
124
|
| `layout()` | Must contain at least one `path()` |
|
|
125
|
-
| `loader()` | `loader(Def, use?)` or `loader(Def, {
|
|
125
|
+
| `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: false }, use?)` — see below |
|
|
126
126
|
| `loading()` | Route/layout-level pending UI; inline `<Suspense>` at read sites is usually better |
|
|
127
127
|
| `revalidate()` | Valid **inside a loader() use callback only**; runs in the browser |
|
|
128
128
|
| `transition()` | Data-only ViewTransition config — no `when` (that is a server-executed predicate) |
|
|
@@ -214,7 +214,7 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
214
214
|
On a document load, `notFound()` streams the resolved not-found UI in the
|
|
215
215
|
envelope and _opportunistically_ sets a real 404 status — the status write only
|
|
216
216
|
wins if the rejection settles before the document Response is constructed (a
|
|
217
|
-
fast, pre-fetch existence check usually wins; see `
|
|
217
|
+
fast, pre-fetch existence check usually wins; see `ssr: false` below
|
|
218
218
|
for the deterministic version). On navigations the 404 UI swaps in with the
|
|
219
219
|
URL preserved. A loader `redirect()` is always a client-side navigate — there
|
|
220
220
|
is no document-lane 302 from loaders; pre-stream redirect authority belongs to
|
|
@@ -228,7 +228,7 @@ ride the SSR handle snapshot; later ones stream and apply post-hydration
|
|
|
228
228
|
(document loads) or progressively (navigations). See `/loader` for the full
|
|
229
229
|
contract.
|
|
230
230
|
|
|
231
|
-
## `
|
|
231
|
+
## `ssr: false` — the SSR-completeness opt-in
|
|
232
232
|
|
|
233
233
|
By default every loader streams on every render, so nothing a slow loader
|
|
234
234
|
produces is _guaranteed_ to be in the SSR'd HTML — data SSRs as the Suspense
|
|
@@ -238,12 +238,13 @@ document — `<head>` meta via a handle, or a real 404 status — flag it:
|
|
|
238
238
|
|
|
239
239
|
```ts
|
|
240
240
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
241
|
-
loader(ProductLoader, {
|
|
241
|
+
loader(ProductLoader, { ssr: false }, () => [revalidate(productData)]),
|
|
242
242
|
loader(RelatedLoader), // untouched: still streams behind its boundary
|
|
243
243
|
]),
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
The
|
|
246
|
+
The knob mirrors `loading(fallback, { ssr: false })` — SSR delivery is off
|
|
247
|
+
for this loader, so nothing of it is left to stream in the document: document
|
|
247
248
|
renders await this loader before first flush (data settled — no fallback
|
|
248
249
|
paints; handle pushes beat the barrier snapshot; a thrown `notFound()` is a
|
|
249
250
|
deterministic real 404, no warm-up race); client navigations stream exactly as
|
|
@@ -303,7 +304,7 @@ a hard load of the target URL renders the full route.
|
|
|
303
304
|
`<Suspense>` above each `useLoader` read is still the finer-grained tool
|
|
304
305
|
when different reads on one route should wait independently.
|
|
305
306
|
- **Two parallel loaders with equal latency look "SSR'd" together.** Loaders
|
|
306
|
-
kick off in parallel, so awaiting one (`
|
|
307
|
+
kick off in parallel, so awaiting one (`ssr: false`) gives
|
|
307
308
|
same-or-faster siblings time to settle coincidentally. Do not read "it was
|
|
308
309
|
in the HTML once" as a guarantee — only the flagged loader is guaranteed.
|
|
309
310
|
- **Hook semantics shift inside a group.** `usePathname` is ABSOLUTE (mount
|
|
@@ -332,7 +333,7 @@ The router repository (not shipped in this package) carries a canonical
|
|
|
332
333
|
consumer at `tests/vite-rsc-demo/src/urls/client-shop.client.tsx`: layout +
|
|
333
334
|
index + product routes, param-sensitive predicates, action-scoped cart
|
|
334
335
|
revalidation, loader-thrown `notFound()`/`redirect()`, loader-written
|
|
335
|
-
Meta/Breadcrumbs, viewport prefetch on cards, and the `
|
|
336
|
+
Meta/Breadcrumbs, viewport prefetch on cards, and the `ssr: false`
|
|
336
337
|
fixtures — with e2e suites next to it pinning each contract in dev and
|
|
337
338
|
production. The sections above are self-contained; the app is corroboration,
|
|
338
339
|
not required reading.
|
|
@@ -130,9 +130,11 @@ export const router = createRouter<AppBindings>({
|
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
PPR shells use Cache API as the per-colo L1 and KV as the durable cross-colo
|
|
133
|
-
L2; a KV hit promotes the coupled shell envelope back into L1. KV
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
L2; a KV hit promotes the coupled shell envelope back into L1. KV is optional:
|
|
134
|
+
`CFCacheStore` without `kv` runs the shell family L1-only (edge-only ppr —
|
|
135
|
+
each colo captures and serves its own shell). Tag eviction then needs purge
|
|
136
|
+
mode (`tagPurge`); a tagged shell without either warns once and expires by
|
|
137
|
+
ttl+swr only.
|
|
136
138
|
|
|
137
139
|
## Commands
|
|
138
140
|
|
|
@@ -621,7 +621,7 @@ boundaries, compose server-side via `ctx.use(OtherLoader)`, and carry
|
|
|
621
621
|
route-level authority: they can throw `notFound()`/`redirect()`, WRITE handles
|
|
622
622
|
(`ctx.use(Meta)({ title })` — data-derived page metadata pushed from the data's
|
|
623
623
|
producer), read handle data after the render settles (`ctx.get(handle)` behind
|
|
624
|
-
`await ctx.rendered()`), and opt into `loader(Def, {
|
|
624
|
+
`await ctx.rendered()`), and opt into `loader(Def, { ssr: false })`
|
|
625
625
|
so a document render awaits them before first flush (deterministic SSR'd data,
|
|
626
626
|
meta, and 404 status). Reads happen through `useLoader` in a client component
|
|
627
627
|
(including its SSR pass) or `useFetchLoader` for standalone client fetches. "Fetchable" loaders are callable
|
|
@@ -691,7 +691,7 @@ that no loader-before-render has:
|
|
|
691
691
|
read by many segments, composed via `ctx.use`, or exposed as a fetchable endpoint;
|
|
692
692
|
- it **streams as a hole, not a gate** — concurrent, Suspense-resolved under
|
|
693
693
|
`loading()`, so the shell never blocks on it (and gating is a deliberate
|
|
694
|
-
per-loader opt-in — `{
|
|
694
|
+
per-loader opt-in — `{ ssr: false }` awaits ONE loader on document
|
|
695
695
|
renders while the rest keep streaming — not the model's default);
|
|
696
696
|
- it is **client-addressable and refreshable** independent of navigation (`key`,
|
|
697
697
|
`refreshGroup`, `useRefreshLoaders`), behaving like a built-in, server-defined
|
package/skills/hooks/data.md
CHANGED
|
@@ -26,7 +26,7 @@ boundary (or the route's `loading()`) — it does NOT render with
|
|
|
26
26
|
`isLoading: true`. Put a boundary above every read whose loader can be slow;
|
|
27
27
|
`isLoading` covers later refetches (`load()`, key/group refreshes). Once the
|
|
28
28
|
component renders, `data` is present. (On document loads a loader registered
|
|
29
|
-
with `{
|
|
29
|
+
with `{ ssr: false }` is already settled at first paint, so its
|
|
30
30
|
reads never suspend there — see `/loader`.)
|
|
31
31
|
|
|
32
32
|
**Precondition**: Loader must be registered on route via `loader()` helper.
|
|
@@ -33,7 +33,7 @@ const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
|
|
|
33
33
|
Writers: handlers AND loader bodies — `ctx.use(Handle)` returns the push
|
|
34
34
|
function in each (middleware has no handle APIs). Loader pushes follow a delivery race (settle before
|
|
35
35
|
the handler barrier → in the SSR'd document; later → applied client-side), and
|
|
36
|
-
`loader(Def, {
|
|
36
|
+
`loader(Def, { ssr: false })` makes them deterministic in the
|
|
37
37
|
document. See `/loader` → "Writing Handles from Loaders". `useHandle`
|
|
38
38
|
re-renders on every handle update, late loader pushes included.
|
|
39
39
|
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loader
|
|
3
|
-
description: Define data loaders for fetching data in routes with createLoader. Use when pages need per-request data that stays fresh, data should stream while the page renders, client components need reactive server data, a loader should throw notFound()/redirect(), set page meta/breadcrumbs from loader data (handle writes), or loader data must be guaranteed in the SSR'd document (
|
|
3
|
+
description: Define data loaders for fetching data in routes with createLoader. Use when pages need per-request data that stays fresh, data should stream while the page renders, client components need reactive server data, a loader should throw notFound()/redirect(), set page meta/breadcrumbs from loader data (handle writes), or loader data must be guaranteed in the SSR'd document (ssr:false).
|
|
4
4
|
argument-hint: "[loader]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -172,23 +172,23 @@ Loaders receive the same context shape as route handlers.
|
|
|
172
172
|
|
|
173
173
|
### Full field surface
|
|
174
174
|
|
|
175
|
-
| Field | Type | Notes
|
|
176
|
-
| -------------- | ------------------------------- |
|
|
177
|
-
| `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`.
|
|
178
|
-
| `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden.
|
|
179
|
-
| `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort).
|
|
180
|
-
| `url` | `URL` | Parsed request URL.
|
|
181
|
-
| `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`).
|
|
182
|
-
| `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`.
|
|
183
|
-
| `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise.
|
|
184
|
-
| `env` | `TEnv` | Plain bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.).
|
|
185
|
-
| `get` | `(key \| ContextVar \| handle)` | Reads middleware variables/context-vars — or READS a handle's collected data, after `await ctx.rendered()`.
|
|
186
|
-
| `use` | `(loader \| handle) => T` | Access another loader's data (Promise), or WRITE a handle: `ctx.use(Meta)({ title })` returns the push function — handler parity. Reads moved to `get`.
|
|
187
|
-
| `rendered` | `() => Promise<void>` | **Experimental.** DSL loaders only — waits for all non-loader segments (including `loading()` streaming handlers) to settle before reading handle data. Not with `
|
|
188
|
-
| `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders.
|
|
189
|
-
| `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls.
|
|
190
|
-
| `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission.
|
|
191
|
-
| `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers).
|
|
175
|
+
| Field | Type | Notes |
|
|
176
|
+
| -------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
177
|
+
| `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`. |
|
|
178
|
+
| `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden. |
|
|
179
|
+
| `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort). |
|
|
180
|
+
| `url` | `URL` | Parsed request URL. |
|
|
181
|
+
| `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`). |
|
|
182
|
+
| `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`. |
|
|
183
|
+
| `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise. |
|
|
184
|
+
| `env` | `TEnv` | Plain bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). |
|
|
185
|
+
| `get` | `(key \| ContextVar \| handle)` | Reads middleware variables/context-vars — or READS a handle's collected data, after `await ctx.rendered()`. |
|
|
186
|
+
| `use` | `(loader \| handle) => T` | Access another loader's data (Promise), or WRITE a handle: `ctx.use(Meta)({ title })` returns the push function — handler parity. Reads moved to `get`. |
|
|
187
|
+
| `rendered` | `() => Promise<void>` | **Experimental.** DSL loaders only — waits for all non-loader segments (including `loading()` streaming handlers) to settle before reading handle data. Not with `ssr: false` (cycle; throws). |
|
|
188
|
+
| `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
|
|
189
|
+
| `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
|
|
190
|
+
| `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
|
|
191
|
+
| `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers). |
|
|
192
192
|
|
|
193
193
|
### Example
|
|
194
194
|
|
|
@@ -443,8 +443,8 @@ boundary a parallel loader blocks its parent, so add one to keep the overlap.)
|
|
|
443
443
|
If you come from a framework where the loader is a blocking step that runs
|
|
444
444
|
before the response is built, this is the shift to internalize: here the
|
|
445
445
|
response starts streaming first and loader data fills in. (The one deliberate
|
|
446
|
-
exception is per-loader: `loader(Def, {
|
|
447
|
-
loader before first flush on document renders — see "`
|
|
446
|
+
exception is per-loader: `loader(Def, { ssr: false })` awaits that
|
|
447
|
+
loader before first flush on document renders — see "`ssr: false`"
|
|
448
448
|
below.)
|
|
449
449
|
|
|
450
450
|
### See it: `debugPerformance`
|
|
@@ -687,10 +687,10 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
687
687
|
|
|
688
688
|
Semantics by lane:
|
|
689
689
|
|
|
690
|
-
| Signal | Document load
|
|
691
|
-
| ------------ |
|
|
692
|
-
| `notFound()` | Not-found UI resolves server-side (nearest `notFoundBoundary` → router option → default) and rides the envelope; the 404 STATUS is **opportunistic** — real only if the rejection beats Response construction. `
|
|
693
|
-
| `redirect()` | 200 document, then a client-side replace to the target — **no document-lane 302 from loaders**; pre-stream redirect authority belongs to middleware
|
|
690
|
+
| Signal | Document load | Client navigation |
|
|
691
|
+
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
692
|
+
| `notFound()` | Not-found UI resolves server-side (nearest `notFoundBoundary` → router option → default) and rides the envelope; the 404 STATUS is **opportunistic** — real only if the rejection beats Response construction. `ssr: false` (below) makes it deterministic. | 404 UI swaps in, URL preserved, payload stays 200 |
|
|
693
|
+
| `redirect()` | 200 document, then a client-side replace to the target — **no document-lane 302 from loaders**; pre-stream redirect authority belongs to middleware | Redirect envelope navigates to the target |
|
|
694
694
|
|
|
695
695
|
Session/auth gates belong in middleware (they are request-shaped, not
|
|
696
696
|
data-shaped, and middleware CAN emit a real pre-stream 302). Data-dependent
|
|
@@ -725,14 +725,14 @@ barrier ride the SSR handle snapshot (in the SSR'd document — `<MetaTags />`,
|
|
|
725
725
|
on document loads (`metadata.handlesLate`) or progressively on navigations.
|
|
726
726
|
A push before your slow fetch usually beats the barrier; a push derived from
|
|
727
727
|
the fetched data usually does not. When it MUST be in the document, use
|
|
728
|
-
`
|
|
728
|
+
`ssr: false` below.
|
|
729
729
|
|
|
730
730
|
Reads are the other direction and gated: `ctx.get(handle)` throws unless the
|
|
731
731
|
loader first does `await ctx.rendered()` (DSL-registered loaders only —
|
|
732
732
|
handler-invoked loaders cannot use `rendered()`, and a handler already
|
|
733
733
|
awaiting the loader via `ctx.use()` makes it a detected deadlock).
|
|
734
734
|
|
|
735
|
-
## `
|
|
735
|
+
## `ssr: false` — Guarantee a Loader in the Document
|
|
736
736
|
|
|
737
737
|
Streaming means nothing a slow loader produces is _guaranteed_ in the SSR'd
|
|
738
738
|
HTML: its section SSRs as the fallback, a late handle push applies
|
|
@@ -743,12 +743,13 @@ use callback:
|
|
|
743
743
|
|
|
744
744
|
```typescript
|
|
745
745
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
746
|
-
loader(ProductLoader, {
|
|
746
|
+
loader(ProductLoader, { ssr: false }, () => [cache({ ttl: 60 })]),
|
|
747
747
|
loader(RelatedLoader), // untouched: still streams behind its boundary
|
|
748
748
|
]),
|
|
749
749
|
```
|
|
750
750
|
|
|
751
|
-
The
|
|
751
|
+
The knob mirrors `loading(fallback, { ssr: false })` — SSR delivery is off
|
|
752
|
+
for this loader, so nothing of it is left to stream in the document:
|
|
752
753
|
document renders await this loader before first flush — data is settled
|
|
753
754
|
(`useLoader` reads it synchronously, no fallback paints), handle pushes beat
|
|
754
755
|
the barrier snapshot, and a thrown `notFound()` deterministically precedes
|
|
@@ -726,7 +726,7 @@ const HomePage: Handler<"home"> = (ctx) => {
|
|
|
726
726
|
|
|
727
727
|
`generateMetadata({ params })` — DATA-derived, document-blocking metadata —
|
|
728
728
|
maps to a Meta push from the LOADER that owns the data, plus
|
|
729
|
-
`{
|
|
729
|
+
`{ ssr: false }` for the blocking-until-in-head part:
|
|
730
730
|
|
|
731
731
|
```typescript
|
|
732
732
|
// Next.js: export async function generateMetadata({ params }) {
|
|
@@ -744,7 +744,7 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
744
744
|
});
|
|
745
745
|
|
|
746
746
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
747
|
-
loader(ProductLoader, {
|
|
747
|
+
loader(ProductLoader, { ssr: false }),
|
|
748
748
|
]);
|
|
749
749
|
```
|
|
750
750
|
|
|
@@ -843,7 +843,7 @@ See `/theme` for full API including system detection and cookie persistence.
|
|
|
843
843
|
10. [ ] Convert loading/error files to `loading()` / `errorBoundary()`
|
|
844
844
|
11. [ ] Migrate API routes to `path.json()` / `path.text()`
|
|
845
845
|
12. [ ] Update metadata to use `Meta` handle + `<MetaTags />` in document head
|
|
846
|
-
(`generateMetadata` → loader push + `{
|
|
846
|
+
(`generateMetadata` → loader push + `{ ssr: false }`)
|
|
847
847
|
13. [ ] Replace `next-themes` with `theme: true` in createRouter (see `/theme`)
|
|
848
848
|
14. [ ] Map rendering-mode segment config: `revalidate = N` → `cache({ ttl })`,
|
|
849
849
|
`force-static` → `Static()`/`Prerender()`, `experimental_ppr` → the
|
|
@@ -134,7 +134,7 @@ const HomePage: Handler<"home"> = (ctx) => {
|
|
|
134
134
|
|
|
135
135
|
RR's data-derived `meta({ data })` maps to the same push from the LOADER that
|
|
136
136
|
owns the data — `ctx.use(Meta)({ title: data.name })` in the loader body, with
|
|
137
|
-
`loader(Def, {
|
|
137
|
+
`loader(Def, { ssr: false })` when the title must be in the SSR'd
|
|
138
138
|
head. See `/loader` → "Writing Handles from Loaders".
|
|
139
139
|
|
|
140
140
|
Add `<MetaTags />` in the Document component's `<head>`:
|
|
@@ -46,7 +46,7 @@ Use it only when you need capabilities beyond what the handler provides:
|
|
|
46
46
|
document loads, never an HTTP 302 — pre-stream 302s move to middleware)
|
|
47
47
|
- **`meta({ data })` / `handle` exports** — data-derived page metadata becomes
|
|
48
48
|
a handle push from the loader body (`ctx.use(Meta)({ title: data.name })`),
|
|
49
|
-
with `loader(L, {
|
|
49
|
+
with `loader(L, { ssr: false })` when it must be in the SSR'd head
|
|
50
50
|
|
|
51
51
|
If the React Router loader just fetches data for its page component AND the
|
|
52
52
|
component can become a server component, merge it into the handler. If the
|
|
@@ -135,7 +135,7 @@ The loader-shaped variant is equally valid — and closer to the RR module when
|
|
|
135
135
|
the loader carried authority. A `createLoader()` body can throw `notFound()`
|
|
136
136
|
for the missing product AND push the data-derived meta itself
|
|
137
137
|
(`ctx.use(Meta)({ title: product.name })`); register it with
|
|
138
|
-
`loader(ProductLoader, {
|
|
138
|
+
`loader(ProductLoader, { ssr: false })` when the 404 status and
|
|
139
139
|
title must be in the document deterministically. See `/loader` → "Loader
|
|
140
140
|
Authority" and "Writing Handles from Loaders". (One RR habit that does NOT
|
|
141
141
|
carry over: a loader `throw redirect()` is a client-side navigate on document
|
package/skills/parallel/SKILL.md
CHANGED
|
@@ -136,7 +136,7 @@ naturally.
|
|
|
136
136
|
> the baked lane under `cache()`/PPR — just to reach a `ctx.use(Meta)` call.
|
|
137
137
|
> The loader can now push `Meta` itself (`/loader` → "Writing Handles from
|
|
138
138
|
> Loaders"), keeping the data on the live lane; add
|
|
139
|
-
> `loader(Def, {
|
|
139
|
+
> `loader(Def, { ssr: false })` when the meta must be in the SSR'd
|
|
140
140
|
> head. Keep the `@meta` slot for metadata that is NOT loader-derived
|
|
141
141
|
> (templates, static descriptions, structured data with independent inputs).
|
|
142
142
|
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -356,13 +356,17 @@ curl -s -D - -o /dev/null https://app.example.com/products/1 | grep -i x-rango-s
|
|
|
356
356
|
key — no header + a declared `ppr` means look for that warning.
|
|
357
357
|
- On Cloudflare, `CFCacheStore` reads PPR shells from the per-colo Cache API,
|
|
358
358
|
falls through to KV on a miss, and promotes the KV hit back into that colo.
|
|
359
|
-
WITHOUT a KV namespace
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
359
|
+
WITHOUT a KV namespace the family runs L1-only (edge-only ppr): every colo
|
|
360
|
+
captures and serves its own shell from the Cache API. What changes KV-less
|
|
361
|
+
is tag eviction — with `tagPurge` the purge-by-tag evicts shell L1 entries;
|
|
362
|
+
without it a TAGGED shell warns once that `updateTag()` cannot reach it
|
|
363
|
+
(ttl/swr-only freshness). Untagged edge-only ppr is warning-free. Tagged
|
|
364
|
+
BUILD-manifest shells decline on a KV-less store (`tagHistoryInert` — the
|
|
365
|
+
immutable asset could never be evicted), and an over-limit tag set is
|
|
366
|
+
acknowledged `"uncacheable"` so the capture backs off instead of
|
|
367
|
+
re-rendering per MISS. A custom store can still declare `shellFamilyInert`
|
|
368
|
+
to stop captures at the gate (`skip-inert-store` on the debug event); the
|
|
369
|
+
built-in stores never do.
|
|
366
370
|
- Structured capture diagnostics: `createRouter({ debugShellCapture: true })`
|
|
367
371
|
logs one line per capture attempt/skip (outcome, durations, prelude and
|
|
368
372
|
snapshot bytes, backoff state); pass a function to receive each
|
|
@@ -665,7 +669,7 @@ LOADER-pushed Meta is different: loaders are masked at capture, so the push
|
|
|
665
669
|
happens at request time and applies client-side (`metadata.handlesLate`) — it
|
|
666
670
|
is never in the cached shell's head, by construction.
|
|
667
671
|
|
|
668
|
-
One flag to know about here: `loader(Def, {
|
|
672
|
+
One flag to know about here: `loader(Def, { ssr: false })` (the
|
|
669
673
|
document-render await, `/loader`) is **inert under PPR** — capture renders
|
|
670
674
|
mask loaders and skip the await, and a shell HIT flushes the stored prelude
|
|
671
675
|
before loaders resolve. Flagging a loader on a `ppr` route does not bake it
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -95,26 +95,26 @@ stated, greppable contract.
|
|
|
95
95
|
|
|
96
96
|
## Pick a primitive
|
|
97
97
|
|
|
98
|
-
| I need to… | Use
|
|
99
|
-
| --------------------------------------- |
|
|
100
|
-
| render data fresh every request | `loader()` + `useLoader()`
|
|
101
|
-
| cache a rendered subtree | `cache()` on a segment
|
|
102
|
-
| cache one function/component's result | `"use cache"`
|
|
103
|
-
| cache a loader's data | `loader(L, () => [cache()])`
|
|
104
|
-
| re-render a segment after an action | `revalidate()`
|
|
105
|
-
| mutate | `"use server"` action
|
|
106
|
-
| debug a slow request | `debugPerformance` / telemetry
|
|
107
|
-
| share config across routes | factory returning a helper array
|
|
108
|
-
| compose a sub-app / module | `include()`
|
|
109
|
-
| modal / soft navigation | `intercept()`
|
|
110
|
-
| route group of client components | `clientUrls()` in `"use client"`
|
|
111
|
-
| set meta/breadcrumbs from loader data | `ctx.use(Handle)` in the loader
|
|
112
|
-
| guarantee loader output in the SSR HTML | `loader(L, {
|
|
113
|
-
| pre-render a route at build time | `Prerender(...)` wrapper
|
|
114
|
-
| feed live loaders from a cached shell | replayed handle + `ctx.rendered()`
|
|
115
|
-
| cache the HTML shell, keep loaders live | `ppr` path option
|
|
116
|
-
| choose in-function vs CDN caching | deployment cache boundary
|
|
117
|
-
| stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()`
|
|
98
|
+
| I need to… | Use | Skill |
|
|
99
|
+
| --------------------------------------- | ---------------------------------- | ----------------------- |
|
|
100
|
+
| render data fresh every request | `loader()` + `useLoader()` | /loader |
|
|
101
|
+
| cache a rendered subtree | `cache()` on a segment | /caching |
|
|
102
|
+
| cache one function/component's result | `"use cache"` | /use-cache |
|
|
103
|
+
| cache a loader's data | `loader(L, () => [cache()])` | /loader, /caching |
|
|
104
|
+
| re-render a segment after an action | `revalidate()` | /loader |
|
|
105
|
+
| mutate | `"use server"` action | /server-actions |
|
|
106
|
+
| debug a slow request | `debugPerformance` / telemetry | /observability |
|
|
107
|
+
| share config across routes | factory returning a helper array | /composability |
|
|
108
|
+
| compose a sub-app / module | `include()` | /route |
|
|
109
|
+
| modal / soft navigation | `intercept()` | /intercept |
|
|
110
|
+
| route group of client components | `clientUrls()` in `"use client"` | /client-urls |
|
|
111
|
+
| set meta/breadcrumbs from loader data | `ctx.use(Handle)` in the loader | /loader |
|
|
112
|
+
| guarantee loader output in the SSR HTML | `loader(L, { ssr: false })` | /loader |
|
|
113
|
+
| pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
|
|
114
|
+
| feed live loaders from a cached shell | replayed handle + `ctx.rendered()` | /shell-manifest |
|
|
115
|
+
| cache the HTML shell, keep loaders live | `ppr` path option | /ppr |
|
|
116
|
+
| choose in-function vs CDN caching | deployment cache boundary | /deployment-caching |
|
|
117
|
+
| stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
|
|
118
118
|
|
|
119
119
|
## Invariants
|
|
120
120
|
|
|
@@ -375,7 +375,7 @@ Handler and no-match cases set HTTP 404 status. A LOADER-thrown `notFound()`
|
|
|
375
375
|
on a document load always streams the resolved not-found UI, but the 404
|
|
376
376
|
STATUS is opportunistic — real only when the rejection settles before the
|
|
377
377
|
document Response is constructed (loaders stream). Register the loader as
|
|
378
|
-
`loader(Def, {
|
|
378
|
+
`loader(Def, { ssr: false })` to make the 404 status deterministic;
|
|
379
379
|
on client navigations the 404 UI swaps in with the URL preserved (payload
|
|
380
380
|
stays 200 — the client owns presentation there). See `/loader` → "Loader
|
|
381
381
|
Authority".
|
package/skills/scripts/SKILL.md
CHANGED
|
@@ -106,7 +106,7 @@ during navigation.
|
|
|
106
106
|
> settles before the handler barrier. A push that lands after a slow fetch
|
|
107
107
|
> arrives post-hydration — and for an inline/ordered script the frozen set
|
|
108
108
|
> means it is silently dropped. If a loader must contribute an inline script
|
|
109
|
-
> to the document, register it `loader(Def, {
|
|
109
|
+
> to the document, register it `loader(Def, { ssr: false })` so the
|
|
110
110
|
> document render awaits the push; otherwise push from a handler (or use an
|
|
111
111
|
> `async` config, which stays reactive).
|
|
112
112
|
|
|
@@ -151,7 +151,7 @@ cache({ ttl: 600, tags: ["products"] }, () => [
|
|
|
151
151
|
fetchable/standalone loader calls that run outside a route render, in
|
|
152
152
|
handler-invoked loaders (a handler already awaiting the loader via
|
|
153
153
|
`ctx.use()` is a detected deadlock), and in loaders registered with
|
|
154
|
-
`{
|
|
154
|
+
`{ ssr: false }` (the document render awaits the loader before
|
|
155
155
|
the barrier — a cycle by construction; see `/loader`).
|
|
156
156
|
- **The reading loader serializes after the shell.** `await ctx.rendered()`
|
|
157
157
|
deliberately gives up loader/render parallelism — on a miss the loader
|
|
@@ -167,17 +167,28 @@ export interface LinkProps extends Omit<
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
|
-
* Check if URL is external (different origin)
|
|
170
|
+
* Check if URL is external (different origin). `origin` is resolved by the
|
|
171
|
+
* component from the navigation store location so SSR and browser agree —
|
|
172
|
+
* reading window here made every absolute-URL Link a hydration mismatch:
|
|
173
|
+
* on the server the ReferenceError was swallowed by the malformed-URL catch
|
|
174
|
+
* and the SSR HTML never carried data-external, then the browser evaluated
|
|
175
|
+
* the same link as external.
|
|
171
176
|
*/
|
|
172
|
-
function isExternalUrl(href: string): boolean {
|
|
177
|
+
function isExternalUrl(href: string, origin: string | undefined): boolean {
|
|
173
178
|
// Protocol-relative URLs
|
|
174
179
|
if (href.startsWith("//")) return true;
|
|
175
180
|
|
|
176
181
|
// Absolute URLs
|
|
177
182
|
if (href.startsWith("http://") || href.startsWith("https://")) {
|
|
183
|
+
// No known origin (no provider and no window): treat as internal — the
|
|
184
|
+
// pre-fix behavior for that configuration, and stable across hydration
|
|
185
|
+
// because both sides resolve the same store origin when a provider
|
|
186
|
+
// exists (the real-app case).
|
|
187
|
+
if (!origin) return false;
|
|
178
188
|
try {
|
|
179
|
-
return new URL(href).origin !==
|
|
189
|
+
return new URL(href).origin !== origin;
|
|
180
190
|
} catch {
|
|
191
|
+
// Genuinely malformed absolute URL.
|
|
181
192
|
return false;
|
|
182
193
|
}
|
|
183
194
|
}
|
|
@@ -225,7 +236,19 @@ export const Link: ForwardRefExoticComponent<
|
|
|
225
236
|
ref,
|
|
226
237
|
) {
|
|
227
238
|
const ctx = useContext(NavigationStoreContext);
|
|
228
|
-
|
|
239
|
+
// Origin from the store location — the same both-sides source
|
|
240
|
+
// useSearchParams seeds from (SSR: the live request's URL; browser:
|
|
241
|
+
// window.location), so data-external agrees across hydration. Origin is
|
|
242
|
+
// immutable per document, so the inline getState() read cannot tear.
|
|
243
|
+
// window is the provider-less browser fallback (tests, portals outside
|
|
244
|
+
// the app root); provider-less SSR has no origin and keeps links internal.
|
|
245
|
+
const storeLocation = ctx?.eventController.getState().location as
|
|
246
|
+
| URL
|
|
247
|
+
| undefined;
|
|
248
|
+
const origin =
|
|
249
|
+
storeLocation?.origin ??
|
|
250
|
+
(typeof window !== "undefined" ? window.location.origin : undefined);
|
|
251
|
+
const isExternal = isExternalUrl(to, origin);
|
|
229
252
|
|
|
230
253
|
// Auto-prefix with basename for app-local paths.
|
|
231
254
|
// Skip if external, already prefixed, or not a root-relative path.
|