@rsc-kit/mcp 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/answers.d.ts +7 -0
  2. package/dist/answers.js +26 -0
  3. package/dist/answers.js.map +1 -1
  4. package/dist/bundleGuides.d.ts +22 -0
  5. package/dist/bundleGuides.js +130 -0
  6. package/dist/bundleGuides.js.map +1 -0
  7. package/dist/index.js +21 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/recipes.js +81 -6
  10. package/dist/recipes.js.map +1 -1
  11. package/dist/report.d.ts +11 -0
  12. package/dist/report.js +1 -1
  13. package/dist/report.js.map +1 -1
  14. package/guides/api-routes.md +168 -0
  15. package/guides/authorization.md +288 -0
  16. package/guides/caching.md +57 -0
  17. package/guides/connection.md +98 -0
  18. package/guides/edge-caching.md +159 -0
  19. package/guides/errors.md +109 -0
  20. package/guides/file-uploads.md +119 -0
  21. package/guides/fonts.md +117 -0
  22. package/guides/forms.md +528 -0
  23. package/guides/images.md +83 -0
  24. package/guides/index.json +162 -0
  25. package/guides/mcp.md +113 -0
  26. package/guides/metadata.md +289 -0
  27. package/guides/navigation.md +84 -0
  28. package/guides/no-javascript.md +39 -0
  29. package/guides/offline.md +215 -0
  30. package/guides/ppr.md +181 -0
  31. package/guides/pwa.md +260 -0
  32. package/guides/queries.md +340 -0
  33. package/guides/react-compiler.md +153 -0
  34. package/guides/redirects.md +143 -0
  35. package/guides/response-headers.md +66 -0
  36. package/guides/route-interception.md +206 -0
  37. package/guides/routing.md +458 -0
  38. package/guides/sections.md +74 -0
  39. package/guides/server-actions.md +444 -0
  40. package/guides/static-generation.md +347 -0
  41. package/guides/testing.md +158 -0
  42. package/guides/third-party-scripts.md +105 -0
  43. package/guides/typed-routes.md +139 -0
  44. package/guides/url-validation.md +143 -0
  45. package/guides/validation.md +175 -0
  46. package/guides/view-transitions.md +120 -0
  47. package/package.json +4 -3
@@ -0,0 +1,162 @@
1
+ [
2
+ {
3
+ "slug": "api-routes",
4
+ "title": "API routes",
5
+ "description": "Web-standard endpoints, colocated with your pages, with no configuration."
6
+ },
7
+ {
8
+ "slug": "authorization",
9
+ "title": "Authorization",
10
+ "description": "Protecting pages, server actions and API routes."
11
+ },
12
+ {
13
+ "slug": "caching",
14
+ "title": "Asking once per request",
15
+ "description": "cache() — one lookup, however many places need it."
16
+ },
17
+ {
18
+ "slug": "connection",
19
+ "title": "Rendering per request",
20
+ "description": "Marking work that belongs to the visitor, not to the build."
21
+ },
22
+ {
23
+ "slug": "edge-caching",
24
+ "title": "Serving shells from a CDN",
25
+ "description": "Putting build-time shells on the edge, and what rsc-kit does not do."
26
+ },
27
+ {
28
+ "slug": "errors",
29
+ "title": "Errors and 404s",
30
+ "description": "What a visitor sees when a page throws, or asks for a url nothing answers."
31
+ },
32
+ {
33
+ "slug": "file-uploads",
34
+ "title": "File uploads",
35
+ "description": "Sending files through a server action without encoding them."
36
+ },
37
+ {
38
+ "slug": "fonts",
39
+ "title": "Fonts",
40
+ "description": "Self-hosted fonts from npm, and what next/font was doing for you."
41
+ },
42
+ {
43
+ "slug": "forms",
44
+ "title": "Forms",
45
+ "description": "Progressive forms, pending state and validation errors."
46
+ },
47
+ {
48
+ "slug": "images",
49
+ "title": "Images",
50
+ "description": "Responsive images with no optimizer to run — unpic for a CDN, imagetools for files in the repo."
51
+ },
52
+ {
53
+ "slug": "mcp",
54
+ "title": "Working with an AI agent",
55
+ "description": "An MCP server that answers from your actual build, and the AGENTS.md written into every project."
56
+ },
57
+ {
58
+ "slug": "metadata",
59
+ "title": "Page metadata",
60
+ "description": "Titles, descriptions and Open Graph tags, exported from the page."
61
+ },
62
+ {
63
+ "slug": "navigation",
64
+ "title": "Navigation",
65
+ "description": "What happens when someone clicks a link, and why state survives it."
66
+ },
67
+ {
68
+ "slug": "no-javascript",
69
+ "title": "A page with no JavaScript",
70
+ "description": "Rendering a route to HTML and stopping there."
71
+ },
72
+ {
73
+ "slug": "offline",
74
+ "title": "Offline",
75
+ "description": "Knowing when the server cannot be reached, and carrying on without it."
76
+ },
77
+ {
78
+ "slug": "ppr",
79
+ "title": "Partial prerendering",
80
+ "description": "Storing what every visitor sees the same, and rendering the rest per request."
81
+ },
82
+ {
83
+ "slug": "pwa",
84
+ "title": "Progressive web apps",
85
+ "description": "Making the app installable, and adding push notifications and background sync."
86
+ },
87
+ {
88
+ "slug": "queries",
89
+ "title": "Queries",
90
+ "description": "Reading from the server over GET, and letting TanStack Query or SWR own everything above it."
91
+ },
92
+ {
93
+ "slug": "react-compiler",
94
+ "title": "React Compiler",
95
+ "description": "Enabling the compiler in the build."
96
+ },
97
+ {
98
+ "slug": "redirects",
99
+ "title": "Redirects",
100
+ "description": "Leaving a page from inside the render, and what that costs."
101
+ },
102
+ {
103
+ "slug": "response-headers",
104
+ "title": "Headers and cookies",
105
+ "description": "Setting response headers and cookies during a render."
106
+ },
107
+ {
108
+ "slug": "route-interception",
109
+ "title": "Route interception",
110
+ "description": "Opening a route as a modal over the page you were on."
111
+ },
112
+ {
113
+ "slug": "routing",
114
+ "title": "Routing",
115
+ "description": "File-based routes, layouts, loading states and parallel slots."
116
+ },
117
+ {
118
+ "slug": "sections",
119
+ "title": "Sections",
120
+ "description": "Refreshing one region of a page without re-rendering the rest."
121
+ },
122
+ {
123
+ "slug": "server-actions",
124
+ "title": "Server actions",
125
+ "description": "Calling the server from a client component, as an ordinary function."
126
+ },
127
+ {
128
+ "slug": "static-generation",
129
+ "title": "Static generation",
130
+ "description": "Rendering pages ahead of time, and exporting a site of files."
131
+ },
132
+ {
133
+ "slug": "testing",
134
+ "title": "Testing",
135
+ "description": "Three tiers, and the one thing that still needs a browser."
136
+ },
137
+ {
138
+ "slug": "third-party-scripts",
139
+ "title": "Third-party scripts",
140
+ "description": "Analytics, tag managers and widgets — and why there is no Script component."
141
+ },
142
+ {
143
+ "slug": "typed-routes",
144
+ "title": "Typed routes",
145
+ "description": "Links that fail the typecheck instead of the browser."
146
+ },
147
+ {
148
+ "slug": "url-validation",
149
+ "title": "Typed URLs",
150
+ "description": "Checking and typing params, query strings and request bodies with a schema you already write."
151
+ },
152
+ {
153
+ "slug": "validation",
154
+ "title": "Validation",
155
+ "description": "Surfacing server-side validation errors in a form."
156
+ },
157
+ {
158
+ "slug": "view-transitions",
159
+ "title": "View transitions",
160
+ "description": "What React's ViewTransition animates in an app built with this, and what it does not."
161
+ }
162
+ ]
package/guides/mcp.md ADDED
@@ -0,0 +1,113 @@
1
+ # Working with an AI agent
2
+
3
+ > An MCP server that answers from your actual build, and the AGENTS.md written into every project.
4
+
5
+ Two things ship for this, and they do different jobs.
6
+
7
+ `AGENTS.md` is written into your project by the scaffold. It is short on
8
+ purpose — it sits in an agent's context on every turn, so it states the rules
9
+ and nothing more.
10
+
11
+ The **MCP server** is for everything that does not fit in that budget: what your
12
+ build actually did, and the long-form answer to "how do I do X here".
13
+
14
+ ## Connecting it
15
+
16
+ The scaffold already did. Every project gets a `.mcp.json` at its root:
17
+
18
+ ```json title=".mcp.json"
19
+ {
20
+ "mcpServers": {
21
+ "rsc-kit": { "command": "npx", "args": ["-y", "@rsc-kit/mcp"] }
22
+ }
23
+ }
24
+ ```
25
+
26
+ Claude Code reads that as project-scoped configuration and asks you to approve
27
+ it the first time it starts the server. Nothing is installed until then, and
28
+ nothing about your build or dev server changes — it is a file an agent reads.
29
+ Commit it; it is the same for everyone on the project.
30
+
31
+ For a project that predates the file, `create-rsc-kit init` writes it (and
32
+ leaves one that is already there alone), or add it by hand:
33
+
34
+ ```sh
35
+ claude mcp add rsc-kit -- npx -y @rsc-kit/mcp
36
+ ```
37
+
38
+ Any other MCP client takes the same entry — a stdio server, command `npx`,
39
+ arguments `-y @rsc-kit/mcp` — in its own file: `.cursor/mcp.json` under
40
+ `mcpServers`, `.vscode/mcp.json` under `servers`.
41
+
42
+ ## What it answers about your app
43
+
44
+ Every build writes `build-report.json` — the same rows it printed, plus every
45
+ server action and whether a client built it. These read
46
+ it, so nothing runs a build or imports your code:
47
+
48
+ | tool | answers |
49
+ | --- | --- |
50
+ | `list_routes` | every route, what happened to it, what it ships |
51
+ | `explain_route` | why one url is stored or rendered per request |
52
+ | `what_is_dynamic` | the routes that are not stored, with reasons |
53
+ | `heaviest_routes` | what costs the browser most |
54
+
55
+ The reason a page is dynamic is **recorded, not guessed**:
56
+
57
+ ```
58
+ /locale — a stored shell, with the rest rendered per request (from the last build, 5 minutes ago)
59
+ Rendered by app/locale/page.
60
+ Ships 85 kB of javascript, gzipped.
61
+
62
+ Why it is not stored whole: dynamic — called cookies(), headers()
63
+ ```
64
+
65
+ That matters more than it sounds. Asked to "make this page faster", an agent
66
+ without this guesses at a cause and edits something. With it, the call that did
67
+ it is on the screen.
68
+
69
+ ## What it answers about the framework
70
+
71
+ | tool | answers |
72
+ | --- | --- |
73
+ | `list_topics` | every topic it can explain |
74
+ | `how_to` | the short answer: forms, prefetch, validation, the action client, data with TanStack Query or SWR, Suspense, offline, pwa, no-javascript, api routes, authorization, why a page is dynamic |
75
+ | `list_guides` | every guide on this site, one line each |
76
+ | `read_guide` | the full text of one, exactly as published here — bundled with the server, so it matches the version installed |
77
+ | `search_guides` | every line in the guides mentioning a word, with the guide it is in |
78
+
79
+ `how_to` is the opinionated summary an agent should read first; `read_guide`
80
+ is for when the summary is not enough. Both come from the same source, and the
81
+ guides are copied in at build time, so what an agent reads is what this site
82
+ says for the version it has.
83
+
84
+ These exist because the patterns here differ from Next and plain React **in
85
+ ways that compile either way** — a check written in the component instead of the
86
+ action, a hand-parsed query string, `"use server"` at the top of a page. An
87
+ agent that has not been told writes those confidently.
88
+
89
+ ## Two things worth knowing
90
+
91
+ **Every answer says how old it is.** It reports the last build, and your files
92
+ may have changed since — so `(from the last build, 5 minutes ago)` is on every
93
+ response. With no build at all it says to run one, rather than reporting that
94
+ there are no routes.
95
+
96
+ **It is read-only.** Nothing here edits, builds or deploys. Your agent already
97
+ has a shell for those, and a server that can change a project is one that can
98
+ change it while answering a question about it.
99
+
100
+ ## The docs are markdown too
101
+
102
+ Every page on this site is also a markdown file at the same url with
103
+ `index.md` added — `/guides/forms/index.md` — and
104
+ [`/llms.txt`](/llms.txt) is an index of all of them, with
105
+ [`/llms-full.txt`](/llms-full.txt) as one document. An agent without the MCP
106
+ server, or one asked about a version other than the one installed, reads
107
+ those. `read_guide` is the same text, offline, at the installed version.
108
+
109
+ ## Without an agent
110
+
111
+ `build-report.json` is an ordinary file. A CI step can assert on it — that
112
+ nothing stopped being prerendered, that no route crossed a size budget — which
113
+ is the check most likely to catch a regression nobody would otherwise see.
@@ -0,0 +1,289 @@
1
+ # Page metadata
2
+
3
+ > Titles, descriptions and Open Graph tags, exported from the page.
4
+
5
+ Titles and meta tags are exported by the page that needs them. There is no
6
+ document template to edit and nothing to register — React hoists the resulting
7
+ elements into `<head>`, so they stream with the page rather than waiting for
8
+ it.
9
+
10
+ ## Static metadata
11
+
12
+ ```tsx title="src/app/about/page.tsx"
13
+ export const metadata: Metadata = {
14
+ title: 'About us',
15
+ description: 'Learn more about our team.',
16
+ keywords: ['about', 'team', 'company'],
17
+ openGraph: {
18
+ title: 'About us',
19
+ description: 'Learn more about our team.',
20
+ },
21
+ };
22
+
23
+ export default function AboutPage() {
24
+ return <h1>About us</h1>;
25
+ }
26
+ ```
27
+
28
+ `Metadata` is imported from the package:
29
+
30
+ ```tsx
31
+ import type { Metadata } from '@rsc-kit/core/metadata';
32
+ ```
33
+
34
+ It used to be ambient, written into your project by the build. An import
35
+ resolves as soon as dependencies are installed, where a generated file does not
36
+ exist until something has been built once — so a freshly cloned project no
37
+ longer reports an error on every page it has not built yet.
38
+
39
+ :::tip[Custom meta tags go under `other`]
40
+ ```tsx
41
+ export const metadata: Metadata = {
42
+ title: 'Pricing',
43
+ other: { 'fb:app_id': '1234', 'theme-color': '#111' },
44
+ };
45
+ ```
46
+
47
+ The named keys are a closed set on purpose. When any key was legal, a typo like
48
+ `titel` was accepted in silence — and your editor offered no completions at
49
+ all.
50
+ :::
51
+
52
+ ## Metadata from data
53
+
54
+ When the values depend on what the page loaded, export `generateMetadata`. It
55
+ receives the route params:
56
+
57
+ ```tsx title="src/app/posts/[slug]/page.tsx"
58
+ import { findPost } from '../../../data';
59
+
60
+ export const generateMetadata: GenerateMetadata<{ slug: string }> = async ({ params }) => {
61
+ const { slug } = await params;
62
+ const post = await findPost(slug);
63
+
64
+ return {
65
+ title: post.title,
66
+ description: post.excerpt,
67
+ openGraph: { title: post.title, description: post.excerpt, images: post.cover },
68
+ };
69
+ };
70
+
71
+ export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
72
+ const { slug } = await params;
73
+ const post = await findPost(slug);
74
+
75
+ return <h1>{post.title}</h1>;
76
+ }
77
+ ```
78
+
79
+ It receives the same `params` and `searchParams` a page does, so there is one
80
+ shape to learn rather than two. Unlike a page's, these always resolve — a
81
+ `<title>` has no fallback, so there is nothing to suspend into.
82
+
83
+ Metadata always resolves against the **full** layout chain, even when the
84
+ render is partial. A navigation that replaces one segment still produces the
85
+ same `<title>` a full load would, because the template on the outer layout is
86
+ consulted either way.
87
+
88
+ ## Title templates
89
+
90
+ A layout can define the shape every title beneath it takes. `%s` stands in for
91
+ the page's own title:
92
+
93
+ ```tsx title="src/app/layout.tsx"
94
+ export const metadata: Metadata = {
95
+ title: {
96
+ template: '%s | Acme',
97
+ default: 'Acme',
98
+ },
99
+ description: 'The Acme dashboard.',
100
+ };
101
+ ```
102
+
103
+ ```tsx title="src/app/settings/page.tsx"
104
+ export const metadata: Metadata = { title: 'Settings' };
105
+
106
+ // <title>Settings | Acme</title>
107
+ ```
108
+
109
+ The nearest layout with a template wins, so a nested layout can override the
110
+ one above it. A page that exports no title gets the `default`.
111
+
112
+ ## Supported keys
113
+
114
+ | Key | Becomes |
115
+ | --- | --- |
116
+ | `title` | `<title>` — a string on a page, a template on a layout |
117
+ | `description` | `<meta name="description">` |
118
+ | `keywords` | `<meta name="keywords">`, joined with commas if you pass an array |
119
+ | `author` | `<meta name="author">` |
120
+ | `robots` | `<meta name="robots">` |
121
+ | `metadataBase` | nothing itself — it is what makes every relative url below absolute |
122
+ | `openGraph` | `<meta property="og:…">`, one per field; each image brings its size and alt |
123
+ | `twitter` | `<meta name="twitter:…">` |
124
+ | `icons` | `<link rel="icon">`, `apple-touch-icon` and so on |
125
+ | `other` | `<meta name="…">`, or `property=` for an `og:`/`fb:`/`article:` key |
126
+ | anything else | `<meta name="…">` |
127
+
128
+ `keywords` takes either form — `'react, rsc'` and `['react', 'rsc']` produce
129
+ the same tag.
130
+
131
+ The flat spellings — `'og:title'`, `'twitter:card'` — still work and render
132
+ correctly, but the structured objects are the ones to write: they are typed,
133
+ they take an image object with a width and height, and they are what a Next
134
+ app already has.
135
+
136
+ ## The share card
137
+
138
+ ```tsx title="src/app/layout.tsx"
139
+ export const metadata: Metadata = {
140
+ metadataBase: new URL('https://remorva.com'),
141
+ openGraph: {
142
+ siteName: 'Remorva',
143
+ type: 'website',
144
+ },
145
+ twitter: {
146
+ card: 'summary_large_image',
147
+ site: '@remorva',
148
+ },
149
+ };
150
+ ```
151
+
152
+ ```tsx title="src/app/posts/[slug]/page.tsx"
153
+ export async function generateMetadata({ params }): Promise<Metadata> {
154
+ const post = await findPost((await params).slug);
155
+
156
+ return {
157
+ title: post.title,
158
+ openGraph: {
159
+ title: post.title,
160
+ description: post.excerpt,
161
+ url: `/posts/${post.slug}`,
162
+ images: [{ url: post.cover, width: 1200, height: 630, alt: post.title }],
163
+ },
164
+ };
165
+ }
166
+ ```
167
+
168
+ Layouts merge outward-in, so `metadataBase`, `siteName` and the card type are
169
+ set once and every page gets them.
170
+
171
+ :::caution[`metadataBase` is not optional in production]
172
+ A share-card scraper needs an **absolute** image url. Facebook, Slack and
173
+ LinkedIn all refuse a relative one, silently — the link unfurls with no image
174
+ and nothing says why.
175
+
176
+ Set `metadataBase` on the root layout and every relative `url`, image and icon
177
+ below it is made absolute. Without it, an `opengraph-image.png` in `app/` goes
178
+ out as `/_app/opengraph-image.png`, which works in a browser and nowhere else.
179
+
180
+ It is the same name as Next's, so a port carries it across unchanged.
181
+ :::
182
+
183
+ `og:` tags render with `property=` and `twitter:` with `name=`, because that is
184
+ what each scraper reads. It is worth stating because it is easy to get
185
+ backwards, and a tag with the wrong attribute is not an error — it is a card
186
+ that never shows.
187
+
188
+ ## Icons
189
+
190
+ A string, a list, or a categorised object:
191
+
192
+ ```tsx
193
+ // One favicon
194
+ export const metadata: Metadata = { icons: '/favicon.ico' };
195
+
196
+ // Several, with attributes
197
+ export const metadata: Metadata = {
198
+ icons: [
199
+ { url: '/favicon.ico', sizes: '32x32' },
200
+ { url: '/icon-192.png', sizes: '192x192', type: 'image/png' },
201
+ ],
202
+ };
203
+
204
+ // By role
205
+ export const metadata: Metadata = {
206
+ icons: {
207
+ icon: '/favicon.ico',
208
+ apple: '/apple-touch-icon.png',
209
+ other: { rel: 'mask-icon', url: '/safari-pinned-tab.svg', color: '#5bbad5' },
210
+ },
211
+ };
212
+ ```
213
+
214
+ Each descriptor takes `url`, `type`, `sizes`, `color`, `rel`, `media` and
215
+ `fetchPriority`.
216
+
217
+ ## How it reaches the document
218
+
219
+ ```text
220
+ Build the plugin finds the metadata exports and generates a resolver
221
+ Render the resolver runs alongside the page, against the full layout chain
222
+ Load React hoists <title> and <meta> into <head> as they are emitted
223
+ Navigate the resolved metadata rides on the X-RSC-Meta header, and the
224
+ client updates document.title and the tags in place
225
+ ```
226
+
227
+ The last line is why a title is correct after an SPA navigation without the
228
+ page having been re-rendered from the root.
229
+
230
+ ## Icons and share images
231
+
232
+ Put them in `src/app/` and the build finds them. There is nothing to register
233
+ and no layout to edit:
234
+
235
+ | file | what it becomes |
236
+ | --- | --- |
237
+ | `favicon.ico` | served at `/favicon.ico` |
238
+ | `icon.png`, `icon-192.png`, `icon-512.png` | `<link rel="icon">`, and the [manifest](/guides/offline/)'s icons |
239
+ | `apple-icon.png` | `<link rel="apple-touch-icon">` |
240
+ | `opengraph-image.png` | `<meta property="og:image">` |
241
+ | `twitter-image.png` | `<meta name="twitter:image">`, with the large-card type |
242
+
243
+ The names are Next's. There is nothing to gain from inventing different ones —
244
+ an app moving between the two should not have to rename its favicon.
245
+
246
+ **Sizes are read from the filename**, so `icon-192.png` needs no entry anywhere
247
+ saying it is 192 pixels. Which is why a manifest usually lists no icons at all:
248
+
249
+ ```ts title="src/app/manifest.ts"
250
+ export default {
251
+ name: 'Orders',
252
+ themeColor: '#0b0b0c',
253
+ // icons omitted — the build uses the icon-*.png beside this file
254
+ } satisfies WebManifest
255
+ ```
256
+
257
+ Listing them as well would be the same set written twice, and the copy that
258
+ goes stale is the one nobody looks at.
259
+
260
+ ### Why `app/` and not `public/`
261
+
262
+ `public/` means "serve this file unchanged". These are *read* as well as
263
+ served: an icon's filename decides what goes in the manifest, and its presence
264
+ decides what goes in the head. A file whose name the build interprets belongs
265
+ with the other files whose names the build interprets.
266
+
267
+ They are copied into the build output under `/_app/`, so they never collide
268
+ with something you put in `public/` yourself. The favicon is the exception —
269
+ it is served at `/favicon.ico`, because browsers ask for that url whatever the
270
+ markup says.
271
+
272
+ :::note[Set `metadataBase`]
273
+ The image is emitted as `/_app/opengraph-image.png`, which a scraper will not
274
+ fetch. With `metadataBase` on the root layout it goes out absolute — see [the
275
+ share card](#the-share-card) above.
276
+ :::
277
+
278
+ ### Not `opengraph-image.tsx`
279
+
280
+ Next also accepts a *component* by that name and renders it to a PNG at build
281
+ time. This package does not, on purpose. Rendering an image means bundling a
282
+ layout engine and a rasteriser — a couple of megabytes of dependencies, and
283
+ seconds added to every build for every image — for a picture that changes
284
+ when the design does, which is to say rarely.
285
+
286
+ Make the image once with whatever you like, and commit the file. If a page
287
+ needs a *different* image per record — a card for each post — that is an
288
+ [api route](/guides/api-routes/) that returns one, cached, and the
289
+ `openGraph.images` url points at it. Neither touches the build.
@@ -0,0 +1,84 @@
1
+ # Navigation
2
+
3
+ > What happens when someone clicks a link, and why state survives it.
4
+
5
+ `Link` does not reload the page. It asks the server for the part of the tree
6
+ that actually changed, and swaps it in.
7
+
8
+ ```tsx
9
+ import Link from '@rsc-kit/core/Link'
10
+
11
+ <Link href="/about">About</Link>
12
+ ```
13
+
14
+ ## What a navigation actually sends
15
+
16
+ Going from `/posts/one` to `/posts/two`, the root layout and the posts layout
17
+ are already mounted and identical. The client says which layouts it holds, the
18
+ server compares that with the new route's chain, and answers with the deepest
19
+ piece that differs — the page alone.
20
+
21
+ You get this for free. There is nothing to configure and no `loading` prop to
22
+ thread.
23
+
24
+ Two things follow from it, and they are the reason it works this way:
25
+
26
+ **State above the swap survives.** A sidebar's scroll position, an open menu, a
27
+ half-typed form in a layout — none of it is touched, because none of it was
28
+ re-rendered.
29
+
30
+ **A page you return to is still there.** Recently visited pages are kept mounted
31
+ and hidden rather than thrown away, so going back restores a half-filled form
32
+ exactly as you left it. Only a handful are kept, ordered by last visit.
33
+
34
+ ## Programmatic navigation
35
+
36
+ ```tsx
37
+ 'use client'
38
+
39
+ import { visit, prefetch } from '@rsc-kit/core/router'
40
+
41
+ <button onClick={() => visit('/checkout')}>Checkout</button>
42
+ ```
43
+
44
+ Links prefetch on hover already. `prefetch` is for when you know where someone
45
+ is going before they hover:
46
+
47
+ ```tsx
48
+ useEffect(() => { prefetch('/step-2') }, [])
49
+ ```
50
+
51
+ ## Showing progress
52
+
53
+ ```tsx
54
+ 'use client'
55
+
56
+ import { useLinkStatus } from '@rsc-kit/core/useLinkStatus'
57
+
58
+ function Spinner() {
59
+ const { pending } = useLinkStatus()
60
+
61
+ return pending ? <span>Loading…</span> : null
62
+ }
63
+ ```
64
+
65
+ A `loading.tsx` beside a page is shown while that page's data resolves:
66
+
67
+ ```tsx title="src/app/posts/loading.tsx"
68
+ export default function Loading() {
69
+ return <p>Loading posts…</p>
70
+ }
71
+ ```
72
+
73
+ ## Refreshing the current page
74
+
75
+ ```tsx
76
+ 'use client'
77
+
78
+ import { refresh } from '@rsc-kit/core/router'
79
+
80
+ <button onClick={() => refresh()}>Reload</button>
81
+ ```
82
+
83
+ Scroll positions are restored afterwards — the window's and any element with
84
+ its own overflow, such as a sidebar.
@@ -0,0 +1,39 @@
1
+ # A page with no JavaScript
2
+
3
+ > Rendering a route to HTML and stopping there.
4
+
5
+ Some pages have nothing to hydrate. A marketing page, a changelog, an article.
6
+ For those, shipping React is paying about 70 kB gzipped for nothing.
7
+
8
+ ```tsx title="src/app/about/page.tsx"
9
+ export const clientJs = false
10
+
11
+ export default function AboutPage() {
12
+ return <h1>About</h1>
13
+ }
14
+ ```
15
+
16
+ That route now renders to HTML and stops. No bootstrap script, so no React, no
17
+ Flight client, no router.
18
+
19
+ ## What stops working
20
+
21
+ Links on that page are ordinary links — a full page load rather than a segment
22
+ swap. Anything interactive stops working, which is why the build refuses the
23
+ combination rather than shipping it:
24
+
25
+ ```
26
+ app/about/page.tsx declares clientJs = false but renders client components:
27
+ src/components/Counter.tsx
28
+ ```
29
+
30
+ That is a build failure, not a warning. A client component with no runtime is
31
+ inert markup — a button that does nothing — and it is usually inherited from a
32
+ shared layout rather than written on the page, so the message names the
33
+ components responsible.
34
+
35
+ ## It cannot be a PPR route
36
+
37
+ With no client runtime there is nothing to fill a Suspense hole after the
38
+ shell arrives. A `clientJs = false` route should be fully static: fetch what it
39
+ needs at build time and freeze the whole page.