@rsc-kit/mcp 0.14.0 → 0.16.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/answers.d.ts +7 -0
- package/dist/answers.js +28 -0
- package/dist/answers.js.map +1 -1
- package/dist/bundleGuides.d.ts +27 -0
- package/dist/bundleGuides.js +138 -0
- package/dist/bundleGuides.js.map +1 -0
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/recipes.js +138 -17
- package/dist/recipes.js.map +1 -1
- package/dist/report.d.ts +13 -0
- package/dist/report.js +1 -1
- package/dist/report.js.map +1 -1
- package/guides/api-routes.md +168 -0
- package/guides/authorization.md +288 -0
- package/guides/caching.md +57 -0
- package/guides/coming-from-next.md +151 -0
- package/guides/connection.md +98 -0
- package/guides/edge-caching.md +159 -0
- package/guides/errors.md +109 -0
- package/guides/file-uploads.md +119 -0
- package/guides/fonts.md +117 -0
- package/guides/forms.md +528 -0
- package/guides/getting-started.md +132 -0
- package/guides/images.md +83 -0
- package/guides/index.json +187 -0
- package/guides/installation.md +338 -0
- package/guides/introduction.md +119 -0
- package/guides/mcp.md +113 -0
- package/guides/metadata.md +289 -0
- package/guides/navigation.md +84 -0
- package/guides/no-javascript.md +76 -0
- package/guides/offline.md +215 -0
- package/guides/ppr.md +181 -0
- package/guides/pwa.md +260 -0
- package/guides/queries.md +340 -0
- package/guides/quick-start.md +99 -0
- package/guides/react-compiler.md +153 -0
- package/guides/redirects.md +143 -0
- package/guides/response-headers.md +66 -0
- package/guides/route-interception.md +206 -0
- package/guides/routing.md +458 -0
- package/guides/sections.md +74 -0
- package/guides/server-actions.md +444 -0
- package/guides/static-generation.md +347 -0
- package/guides/testing.md +158 -0
- package/guides/third-party-scripts.md +105 -0
- package/guides/typed-routes.md +139 -0
- package/guides/url-validation.md +143 -0
- package/guides/validation.md +175 -0
- package/guides/view-transitions.md +120 -0
- package/package.json +4 -3
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Introduction
|
|
2
|
+
|
|
3
|
+
> React Server Components as a Vite plugin, deployed wherever you like.
|
|
4
|
+
|
|
5
|
+
`rsc-kit` is a Vite plugin that gives you React Server Components. You write
|
|
6
|
+
pages, layouts and server actions the way you would in the Next.js App Router;
|
|
7
|
+
it renders them, streams the HTML, and turns links into navigations that replace
|
|
8
|
+
only the part of the page that changed.
|
|
9
|
+
|
|
10
|
+
It brings no build system of its own — it is a Vite plugin — and it does not
|
|
11
|
+
write a server for you either. [Nitro](https://nitro.build) builds one around
|
|
12
|
+
your route tree, so where an app runs is a preset: Bun, Node, a Cloudflare
|
|
13
|
+
Worker, Vercel, Netlify, Deno.
|
|
14
|
+
|
|
15
|
+
## What an app looks like
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
src/app/
|
|
19
|
+
layout.tsx the document — <html>, <head>, <body>
|
|
20
|
+
loading.tsx Suspense fallback for everything below
|
|
21
|
+
page.tsx GET /
|
|
22
|
+
posts/
|
|
23
|
+
[slug]/
|
|
24
|
+
page.tsx GET /posts/:slug
|
|
25
|
+
@modal/
|
|
26
|
+
default.tsx a parallel slot, empty until something fills it
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Nothing registers those files. The plugin reads the directory at build time and
|
|
30
|
+
generates the entry that knows about them.
|
|
31
|
+
|
|
32
|
+
```tsx title="src/app/posts/[slug]/page.tsx"
|
|
33
|
+
import { findPost } from '../../../data'
|
|
34
|
+
|
|
35
|
+
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
36
|
+
const { slug } = await params
|
|
37
|
+
const post = await findPost(slug)
|
|
38
|
+
|
|
39
|
+
if (!post) return <h1>No such post</h1>
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<article>
|
|
43
|
+
<h1>{post.title}</h1>
|
|
44
|
+
<p>{post.body}</p>
|
|
45
|
+
</article>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A server component is an ordinary async function. It imports its data module
|
|
51
|
+
directly, and neither the module nor its secrets reach the browser.
|
|
52
|
+
|
|
53
|
+
## What you get
|
|
54
|
+
|
|
55
|
+
<CardGrid>
|
|
56
|
+
<Card title="Streaming, not waterfalls" icon="ph:waves">
|
|
57
|
+
The shell paints before the data resolves. Suspense boundaries fill in as
|
|
58
|
+
they finish, in whatever order they finish.
|
|
59
|
+
</Card>
|
|
60
|
+
<Card title="Navigations that keep the page" icon="ph:arrows-left-right">
|
|
61
|
+
A navigation sends the layout chain it already has, and the server answers
|
|
62
|
+
with only the part that changed. Scroll, focus and half-typed forms survive.
|
|
63
|
+
</Card>
|
|
64
|
+
<Card title="Parallel routes and interception" icon="ph:frame-corners">
|
|
65
|
+
`@slot` directories render alongside the page. `(.)folder` opens a route as
|
|
66
|
+
a modal over the page you were on, and as a real page on refresh.
|
|
67
|
+
</Card>
|
|
68
|
+
<Card title="Server actions" icon="ph:lightning">
|
|
69
|
+
`"use server"` makes an async function callable from a client component.
|
|
70
|
+
The body never ships; the call becomes one POST.
|
|
71
|
+
</Card>
|
|
72
|
+
<Card title="Queries, without an endpoint" icon="ph:magnifying-glass">
|
|
73
|
+
`query()` marks a read. It travels as a GET, so it can be cached and
|
|
74
|
+
prefetched — and reads that happen together leave as one request.
|
|
75
|
+
</Card>
|
|
76
|
+
<Card title="Frozen where it can be" icon="ph:snowflake">
|
|
77
|
+
Pages that ask for nothing dynamic are rendered at build time. Pages that do
|
|
78
|
+
get their shell frozen and their data streamed.
|
|
79
|
+
</Card>
|
|
80
|
+
<Card title="An export target" icon="ph:folder-open">
|
|
81
|
+
The same app can build to a directory of files and be served by anything —
|
|
82
|
+
including a CDN with no origin at all.
|
|
83
|
+
</Card>
|
|
84
|
+
</CardGrid>
|
|
85
|
+
|
|
86
|
+
## Where it runs
|
|
87
|
+
|
|
88
|
+
You do not write a server. [Nitro](https://nitro.build) builds one from your
|
|
89
|
+
route tree, and where it runs is one line in your Vite config:
|
|
90
|
+
|
|
91
|
+
```ts title="vite.config.ts"
|
|
92
|
+
nitro({ preset: 'bun' })
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Swap the preset for `node`, `cloudflare_module`, `vercel`, `netlify` or `deno`
|
|
96
|
+
and the same app deploys there instead. The build produces a `.output`
|
|
97
|
+
directory; nothing else changes.
|
|
98
|
+
|
|
99
|
+
## What it is not
|
|
100
|
+
|
|
101
|
+
<Aside type="note" title="No data layer, no CSS pipeline, no dev server of its own">
|
|
102
|
+
The build runs your project's Vite config, so Tailwind, PostCSS and the rest
|
|
103
|
+
are configured the way they are in any Vite app. Data access is an import.
|
|
104
|
+
There is no `getServerSideProps` equivalent, and no request context to learn.
|
|
105
|
+
</Aside>
|
|
106
|
+
|
|
107
|
+
## Where to go next
|
|
108
|
+
|
|
109
|
+
<CardGrid>
|
|
110
|
+
<Card title="Getting started" icon="ph:play">
|
|
111
|
+
[Build and deploy your first app →](/getting-started)
|
|
112
|
+
</Card>
|
|
113
|
+
<Card title="Routing" icon="ph:tree-structure">
|
|
114
|
+
[Pages, layouts, slots and navigation →](/guides/routing)
|
|
115
|
+
</Card>
|
|
116
|
+
<Card title="How it works" icon="ph:gear">
|
|
117
|
+
[What happens between a request and a page →](/reference/how-it-works)
|
|
118
|
+
</Card>
|
|
119
|
+
</CardGrid>
|
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,76 @@
|
|
|
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 — so the
|
|
7
|
+
build does not.
|
|
8
|
+
|
|
9
|
+
## It is automatic
|
|
10
|
+
|
|
11
|
+
A route that freezes whole, renders **no client component** of yours, and has
|
|
12
|
+
**no server action** in its tree is stored without the bootstrap script. No
|
|
13
|
+
React, no Flight client, no router. The build says so:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
○ /about no js
|
|
17
|
+
no client components, so ships no javascript
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Nothing to declare. The check is made on the rendered tree, not the source,
|
|
21
|
+
so a client component reached through a shared layout counts — which is why
|
|
22
|
+
most pages in an app with a `<Link>` in its header keep the runtime, and a
|
|
23
|
+
route group with its own plain layout can drop it.
|
|
24
|
+
|
|
25
|
+
The line is drawn at what the runtime would *do*. A client component needs
|
|
26
|
+
hydrating. A `<form action={serverFn}>` written in a server component needs
|
|
27
|
+
React to submit it. A `<Suspense>` hole that was not filled at build time
|
|
28
|
+
needs the client to fill it, which is why only a route stored whole
|
|
29
|
+
qualifies. Everything else — anchors, styles, a third-party `<script>` — works
|
|
30
|
+
the same with or without.
|
|
31
|
+
|
|
32
|
+
Navigation is unchanged in both directions. A page with no client component
|
|
33
|
+
has no `<Link>`, so its anchors were already full loads. A `<Link>` elsewhere
|
|
34
|
+
that points *at* it still fetches its flight payload and swaps the segment,
|
|
35
|
+
because that payload is still written from the render with the runtime.
|
|
36
|
+
|
|
37
|
+
## Declaring it
|
|
38
|
+
|
|
39
|
+
To force it on, for a page that renders a client component you are content
|
|
40
|
+
to leave inert:
|
|
41
|
+
|
|
42
|
+
```tsx title="src/app/about/page.tsx"
|
|
43
|
+
export const clientJs = false
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A page stored this way still registers the [service worker](/guides/offline)
|
|
47
|
+
when the app has one: the runtime's one-line registration is inlined in its
|
|
48
|
+
place, so a visitor who lands here first gets the worker the second visit is
|
|
49
|
+
for. What such a page cannot show is the update prompt, which is a client
|
|
50
|
+
component. To keep the runtime on a page the build would otherwise strip, say
|
|
51
|
+
so:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
export const clientJs = true
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## What stops working when you declare it
|
|
58
|
+
|
|
59
|
+
With `clientJs = false`, anything interactive stops working, which is why the
|
|
60
|
+
build refuses the combination rather than shipping it:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
app/about/page.tsx declares clientJs = false but renders client components:
|
|
64
|
+
src/components/Counter.tsx
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
That is a build failure, not a warning. A client component with no runtime is
|
|
68
|
+
inert markup — a button that does nothing — and it is usually inherited from a
|
|
69
|
+
shared layout rather than written on the page, so the message names the
|
|
70
|
+
components responsible.
|
|
71
|
+
|
|
72
|
+
## It cannot be a PPR route
|
|
73
|
+
|
|
74
|
+
With no client runtime there is nothing to fill a Suspense hole after the
|
|
75
|
+
shell arrives. A `clientJs = false` route should be fully static: fetch what it
|
|
76
|
+
needs at build time and freeze the whole page.
|