@rsc-kit/mcp 0.16.3 → 0.18.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.
@@ -87,3 +87,25 @@ jar.set('name', 'value', {
87
87
 
88
88
  Names are validated as cookie tokens and `sameSite` / `expires` are checked, so
89
89
  a typo is an error rather than a header the browser quietly ignores.
90
+
91
+ ## What a response says about itself
92
+
93
+ Two things, and they are not the same kind:
94
+
95
+ **How it was served**, always: `X-RSC-Kit: stored`, `rendered` or `shell` on
96
+ every response — a page from a file the build wrote, a page rendered for this
97
+ visitor, or a stored shell with its holes rendered now. It is the header a
98
+ developer reads in the Network tab when a page is slower than expected, the
99
+ way `X-Nextjs-Cache` is, and a CDN rule or a health check can key on it. It
100
+ names no product, so there is nothing to strip.
101
+
102
+ **What built it**, by default: `X-Powered-By: rsc-kit` on every response and
103
+ `<meta name="generator" content="rsc-kit">` in every document — what
104
+ BuiltWith and Wappalyzer read. The name only, never the version: a version in
105
+ every response is what a vulnerability scanner filters on. A policy that
106
+ strips every framework identifier turns both off, which matters for the tag,
107
+ since a proxy can strip a header but not a line inside the HTML:
108
+
109
+ ```ts title="vite.config.ts"
110
+ rscKit({ identify: false })
111
+ ```
package/guides/routing.md CHANGED
@@ -303,7 +303,11 @@ A **query string** is not — the same route is asked for with `?q=shoes` and
303
303
  `?q=hats` — so there is no honest answer at build time, and `useSearchParams`
304
304
  throws rather than pretending it is empty. The dev server does the same, on
305
305
  purpose: a page has one shape, and a boundary missing in dev is the one the
306
- build will refuse.
306
+ build will refuse. Under a `<Suspense>` you wrote, nothing is said. With
307
+ nothing closer than a `loading.tsx`, dev prints one line and shows it at the
308
+ bottom of the page: the whole segment is that fallback until the query
309
+ arrives, and a boundary around the component that reads keeps the rest
310
+ painted.
307
311
 
308
312
  Wrap it, and the throw becomes the fallback:
309
313
 
@@ -0,0 +1,140 @@
1
+ # robots, sitemap and llms.txt
2
+
3
+ > The files a site describes itself with, from a file beside the root layout — written the way Next writes them, stored at build when they can be.
4
+
5
+ A crawler asks for `/robots.txt` and `/sitemap.xml` before it reads a page,
6
+ and a model asks for `/llms.txt`. Each comes from a file beside the root
7
+ layout, named for what it answers:
8
+
9
+ | file | answers | returns |
10
+ | --- | --- | --- |
11
+ | `src/app/robots.ts` | `/robots.txt` | `MetadataRoute.Robots` |
12
+ | `src/app/sitemap.ts` | `/sitemap.xml` | `MetadataRoute.Sitemap` |
13
+ | `src/app/llms.ts` | `/llms.txt` | `MetadataRoute.Llms` |
14
+ | `src/app/llms-full.ts` | `/llms-full.txt` | a string |
15
+
16
+ The same names and shapes as Next, so a port copies them across unchanged.
17
+
18
+ ```ts title="src/app/robots.ts"
19
+ import type { MetadataRoute } from '@rsc-kit/core/metadata';
20
+
21
+ export default function robots(): MetadataRoute.Robots {
22
+ return {
23
+ rules: [
24
+ { userAgent: '*', allow: '/', disallow: ['/api/', '/studio'] },
25
+ { userAgent: 'GPTBot', disallow: '/' },
26
+ ],
27
+ sitemap: '/sitemap.xml',
28
+ };
29
+ }
30
+ ```
31
+
32
+ ```ts title="src/app/sitemap.ts"
33
+ import type { MetadataRoute } from '@rsc-kit/core/metadata';
34
+ import { db } from '@/lib/db';
35
+
36
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
37
+ const posts = await db.post.findMany({ select: { slug: true, updatedAt: true } });
38
+
39
+ return [
40
+ { url: '/', changeFrequency: 'weekly', priority: 1 },
41
+ { url: '/pricing' },
42
+ ...posts.map((post) => ({ url: `/blog/${post.slug}`, lastModified: post.updatedAt })),
43
+ ];
44
+ }
45
+ ```
46
+
47
+ ```ts title="src/app/llms.ts"
48
+ import type { MetadataRoute } from '@rsc-kit/core/metadata';
49
+
50
+ export default function llms(): MetadataRoute.Llms {
51
+ return {
52
+ title: 'Remorva',
53
+ summary: 'Photo restoration: damage repaired and colour restored, in about a minute.',
54
+ sections: [
55
+ { title: 'Pages', links: [{ title: 'Pricing', url: '/pricing', description: 'Per restoration, no subscription' }] },
56
+ ],
57
+ };
58
+ }
59
+ ```
60
+
61
+ A relative url in any of them is made absolute with the root layout's
62
+ `metadataBase`; without one, a relative url is a build error that says so.
63
+ Any of the three may return a string instead, served as written.
64
+
65
+ ## A sitemap you do not write
66
+
67
+ With no `sitemap.ts`, the build writes `/sitemap.xml` itself, from what it
68
+ already knows: every page it stored and every url `generateStaticParams`
69
+ listed, each with the build as `lastModified`. Left out: anything under a
70
+ `middleware.ts` (a guard means not for everyone), a page the build could not
71
+ render, and the not-found page. The build's table says so:
72
+
73
+ ```
74
+ ○ /sitemap.xml
75
+ written by the build: 5 urls; a sitemap.ts beside the root layout replaces it
76
+ ```
77
+
78
+ It needs the root layout's `metadataBase` for the host; without one the line
79
+ says it was not written. A `sitemap.ts` replaces it entirely — the moment you
80
+ want a page the build cannot see (a post per database row without
81
+ `generateStaticParams`) or a `changeFrequency`, write one.
82
+
83
+ ## How fresh
84
+
85
+ Three choices, and the function decides — the same rule every route follows:
86
+
87
+ | you write | served | fresh |
88
+ | --- | --- | --- |
89
+ | nothing | the build's own sitemap, stored | every deploy |
90
+ | a `sitemap.ts` that reads the database | stored at build | every deploy |
91
+ | a `sitemap.ts` that awaits `connection()` | rendered per request | every crawl |
92
+
93
+ ```ts title="src/app/sitemap.ts"
94
+ import { connection } from '@rsc-kit/core/request';
95
+
96
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
97
+ await connection(); // per request — the same mark a page uses
98
+
99
+ const posts = await db.post.findMany({ select: { slug: true, updatedAt: true } });
100
+
101
+ return posts.map((post) => ({ url: `/blog/${post.slug}`, lastModified: post.updatedAt }));
102
+ }
103
+ ```
104
+
105
+ Without the `connection()` line the same function runs once at build and the
106
+ answer is stored; the build's table shows `○` for a stored one and `ƒ` for
107
+ one that runs per request. Reading the database at build is fine — it is the
108
+ request that makes a route dynamic, not the data.
109
+
110
+ ## How they are served
111
+
112
+ Each file becomes an api route, and that decides the rest. A `sitemap.ts`
113
+ that reads nothing per request — the database counts as nothing, the request
114
+ does not — is answered once at build and stored, like any frozen route, and
115
+ served from the file after. One that reads `cookies()` or awaits
116
+ `connection()` stays dynamic and runs per request. The build's table lists
117
+ them with the other routes and says which.
118
+
119
+ They run no middleware, on purpose: a guard on the root layout's directory
120
+ would otherwise answer a crawler's request for `robots.txt` with a 401.
121
+
122
+ The urls are typed like every other route, so `route('/sitemap.xml')` is a
123
+ link the build checks.
124
+
125
+ ## Files as written
126
+
127
+ A hand-written file beside the root layout is served at the root as it is:
128
+ `robots.txt`, `sitemap.xml` (or `sitemap-posts.xml`), `llms.txt`,
129
+ `llms-full.txt`, and the others a site is asked for there — `humans.txt`,
130
+ `security.txt`, `ads.txt`. In development they are read from `app/`; a build
131
+ copies them beside the client output. A file and a function for the same url
132
+ is a build error naming both.
133
+
134
+ ## Coming from Next
135
+
136
+ `app/robots.ts` and `app/sitemap.ts` carry across unchanged. `app/llms.txt`
137
+ in Next is usually a `route.ts`; here it is `llms.ts` with a shape, or a file
138
+ as written. Next's `generateSitemaps()` for a sitemap split across files is
139
+ not here: a `sitemap-posts.xml` written by hand, or a `route.ts` under
140
+ `app/sitemap/`, covers it.
@@ -94,6 +94,15 @@ const nav = [
94
94
  Without `satisfies`, TypeScript infers `string` for `href` and you lose the
95
95
  check.
96
96
 
97
+ ## The build checks
98
+
99
+ `vite build` runs the project's typecheck before it bundles, and stops on an
100
+ error — so a link to a route that does not exist is a failed build, not a 404
101
+ found after deploying. It is the same `tsc --noEmit` the `check` script runs,
102
+ on the same tsconfig, and it costs about a second. `rscKit({ typecheck: false })`
103
+ turns it off; a project with no `tsconfig.json`, or no `typescript`
104
+ installed, is skipped without being asked.
105
+
97
106
  ## If you never run the generator
98
107
 
99
108
  `.rsc-kit/rsc-routes.d.ts` is written by the build. Without it — or with a
@@ -137,3 +146,20 @@ For end-to-end types without a fetch at all, a [server action or
137
146
  query](/guides/queries/) is already typed across the boundary: the return type
138
147
  is the function's, because it is the same function.
139
148
  :::
149
+
150
+ ## Regions
151
+
152
+ `revalidate()` in an action and `refresh()` in the browser take a region by
153
+ name, and the names are typed to the ones the build found: every
154
+ `section('orders', …)` and every `@slot` directory, plus `'page'` and
155
+ `'all'`. A typo, or a section renamed since, stops compiling instead of being
156
+ refused by the renderer at runtime:
157
+
158
+ ```ts
159
+ revalidate('orders') // a section the build found
160
+ revalidate('order') // does not compile
161
+ ```
162
+
163
+ A name computed at runtime is cast, as an href is: `revalidate(name as
164
+ RevalidateTarget)`. A name that reaches the renderer unknown anyway is still
165
+ refused with the names it does know.
@@ -17,7 +17,7 @@ badly.
17
17
  | a server action's result, put into state | yes |
18
18
  | `<Form>` — errors, success, optimistic updates | yes |
19
19
  | a streamed `<Suspense>` boundary arriving | yes, React does this on its own |
20
- | navigating to another page | yes, behind a flag |
20
+ | navigating to another page | yes the update carries a transition type |
21
21
 
22
22
  ## What works
23
23
 
@@ -62,15 +62,61 @@ throughout, so everything it drives animates the same way.
62
62
 
63
63
  ## Navigating between pages
64
64
 
65
- Off by default, because it changes how every navigation commits:
65
+ A navigation replaces the segment inside `startTransition`, so a
66
+ `<ViewTransition>` around it animates like any other. What you cannot tell
67
+ from outside is a navigation from the first commit after hydration — the
68
+ boundary taking over the server-rendered page, which animated would be the
69
+ page fading into itself on every load. So a navigation's update carries the
70
+ transition type `rsc-navigation`, and that first commit does not. Key the
71
+ boundary on it:
66
72
 
67
- ```ts title="vite.config.ts"
68
- rscKit({ viewTransitions: true })
73
+ ```tsx title="src/app/layout.tsx"
74
+ import { ViewTransition } from 'react';
75
+
76
+ export default function RootLayout({ children }) {
77
+ return (
78
+ <html lang="en">
79
+ <body className="min-h-full flex flex-col">
80
+ <ViewTransition default={{ 'rsc-navigation': 'page', default: 'none' }}>
81
+ <div className="flex flex-1 flex-col">{children}</div>
82
+ </ViewTransition>
83
+ </body>
84
+ </html>
85
+ );
86
+ }
87
+ ```
88
+
89
+ That is a server component, and it can be: `ViewTransition` is a built-in like
90
+ `Suspense`, which the payload carries as a symbol. No `'use client'` file is
91
+ needed for it.
92
+
93
+ `default: 'none'` is what makes a first load or a reload never animate. The
94
+ `<div>` matters (here it also carries the body's flex layout, so a `<main>`
95
+ inside still fills the height): React names each top-level element under a boundary and
96
+ animates every one on its own, so a page of five sections becomes five groups,
97
+ each morphing from where it was to where it is now. One wrapper element is one
98
+ snapshot pair — the old page and the new one dissolving into each other, the
99
+ way Inertia's does. A navbar inside the pair cross-fades into itself, which is
100
+ invisible; put the boundary around `<main>` in a nested layout instead when
101
+ you want the navbar left out of it entirely. Leave the wrapper out when you
102
+ want the per-element movement instead.
103
+
104
+ ### Shaping the animation
105
+
106
+ The pair carries the class you chose, so your CSS shapes it there. The
107
+ browser's default is a 250 ms cross-fade:
108
+
109
+ ```css title="src/app/styles.css"
110
+ /* Shorter, or none at all — the instant swap Inertia does by default. */
111
+ ::view-transition-old(.page),
112
+ ::view-transition-new(.page) {
113
+ animation-duration: 120ms;
114
+ }
69
115
  ```
70
116
 
71
- A build-time constant rather than a runtime setting, so an app that does not
72
- ask for it does not carry the boundary at all. What it animates is the segment
73
- a navigation replaces; what a page does inside itself needs no flag.
117
+ `animation: none` on both is the instant swap, with the transition still in
118
+ place for the shared elements you name yourself (`<ViewTransition name="hero">`
119
+ still morphs).
74
120
 
75
121
  ## Coming back to a page you were just on
76
122
 
@@ -0,0 +1,155 @@
1
+ # Where it runs
2
+
3
+ > A host is a Nitro preset, not a server you write.
4
+
5
+ There used to be a page here for each runtime, and a server file generated to
6
+ match. There is no server file now, and no choice about it either:
7
+ [Nitro](https://nitro.build) builds the server around the route tree, and
8
+ choosing where an app runs is choosing a preset.
9
+
10
+ ```ts title="vite.config.ts"
11
+ import { defineConfig } from 'vite';
12
+ import { nitro } from 'nitro/vite';
13
+ import react from '@vitejs/plugin-react';
14
+ import { rscKit } from '@rsc-kit/core/vite';
15
+
16
+ export default defineConfig({
17
+ plugins: [
18
+ nitro({ preset: 'bun', serveStatic: 'inline' }),
19
+ rscKit(),
20
+ react(),
21
+ ],
22
+ });
23
+ ```
24
+
25
+ `bun create rsc-kit@latest my-app` writes that for you. Changing where it deploys is
26
+ changing the one string.
27
+
28
+ ## The presets
29
+
30
+ `rsc-kit` asks about three, because those are the ones it can check for you.
31
+ Nitro carries [many more](https://nitro.build/deploy) and they need nothing from
32
+ this package — a preset is a string, not an integration.
33
+
34
+ | `--host` | preset | what you get |
35
+ | --- | --- | --- |
36
+ | `bun` | `bun` | `.output/server/index.mjs`, and `bun run compile` for a single binary |
37
+ | `node` | `node` | `.output/server/index.mjs`, run with `node` |
38
+ | `worker` | `cloudflare_module` | a Worker, plus the `wrangler.json` and `_headers` Nitro generates |
39
+
40
+ Everything else — Vercel, Netlify, Azure, Deno, AWS Amplify — is the same
41
+ change:
42
+
43
+ ```ts
44
+ nitro({ preset: 'vercel', serveStatic: 'inline' })
45
+ ```
46
+
47
+ ## Running it
48
+
49
+ ```bash
50
+ npm run dev # vite is the renderer; nothing is prebuilt
51
+ npm run build # writes .output/
52
+ npm run start # runs .output/server/index.mjs
53
+ ```
54
+
55
+ A Worker has no `start`, because it is deployed rather than started:
56
+
57
+ ```bash
58
+ npm run preview # wrangler dev, on workerd
59
+ npm run deploy # nitro deploy --prebuilt
60
+ ```
61
+
62
+ ## Compiling to a single binary
63
+
64
+ Bun only, and the whole application ends up inside one file — engine, route
65
+ tree and assets:
66
+
67
+ ```bash
68
+ npm run compile # builds, then bun build --compile
69
+ ./dist/app
70
+ ```
71
+
72
+ It builds first on purpose. Compiling whatever `.output` happens to hold means
73
+ a binary one version behind the source with nothing to say so — and on a
74
+ project that has never been built, an `ENOENT` naming a path the app did not
75
+ write.
76
+
77
+ <Aside type="note" title="A Worker serves the frozen pages from a module">
78
+ A Worker has no filesystem, so the directory the build writes is not there
79
+ at request time. The build also writes the same pages as one module beside
80
+ the bundle, `rsc-static-inline.mjs`, which wrangler uploads with the rest
81
+ and the server imports when the directory is missing. Nothing to configure;
82
+ it is why `○` on a Worker means the stored page and not a live render that
83
+ happens to be the same. It counts toward the Worker's script size, which is
84
+ worth knowing on a site with hundreds of frozen pages.
85
+ </Aside>
86
+
87
+ <Aside type="note" title="Frozen pages stay outside the binary">
88
+ The build freezes pages into `.output/server/rsc-static` and the server reads
89
+ them from there. A compiled binary has no filesystem to read — the directory
90
+ is not embedded — so it renders those pages live instead. Everything still
91
+ answers; what you lose is the stored render, not the page.
92
+ </Aside>
93
+
94
+ <Aside type="caution" title="serveStatic: 'inline' is what makes this work">
95
+ Without it the binary compiles, starts, serves pages, and 404s every asset.
96
+ Inside a compiled binary the static path resolves into Bun's virtual
97
+ filesystem — where the files on disk are not — and the failure is
98
+ `ENOENT: /$bunfs/public/assets/…` with the pages themselves looking fine.
99
+
100
+ The generated config sets it. If you write your own, set it too.
101
+ </Aside>
102
+
103
+ ## Offline
104
+
105
+ `rscKit({ offline: true })` writes a service worker into `.output/public`
106
+ alongside the assets, so a page someone has visited survives a reload with no
107
+ network. Off by default, and covered in [Offline](/guides/offline#surviving-without-one).
108
+
109
+ ## Why `nitro` is pinned
110
+
111
+ The generated `package.json` pins an exact version rather than a range:
112
+
113
+ ```json
114
+ "nitro": "3.0.260903-beta"
115
+ ```
116
+
117
+ Nitro's own `latest` tag is a dated prerelease, and it sorts **above** the plain
118
+ `3.0.0` on npm. So `^3.0.0` resolves to the older release, which builds without
119
+ complaint and then answers 404 to every route. TanStack Start pins a dated beta
120
+ for the same reason.
121
+
122
+ ## Assets are Nitro's
123
+
124
+ The build writes browser assets to `.output/public`, and Nitro serves them from
125
+ its own root. There is no `assetsDir` or `assetsUrl` to set: both were removed,
126
+ and `rscKit()` refuses a config that still passes them rather than reading a
127
+ prefix and ignoring it.
128
+
129
+ The alternative was the silent version — the markup asks for the app's prefix,
130
+ Nitro answers at its own, and the page arrives unstyled and never hydrates with
131
+ nothing logged anywhere.
132
+
133
+ Assets live in `.output/public` and are served by the same process that serves
134
+ your pages. If you want nginx or a CDN serving them instead, point it at
135
+ `.output/public` — that directory is the deployment.
136
+
137
+ ---
138
+
139
+ Next: [Deploying →](/hosts/deployment)
140
+
141
+ ## Which entry point is whose
142
+
143
+ `@rsc-kit/core/host` is the engine's front door for a **host adapter** — the
144
+ handler, and beside it the few functions an adapter needs when it embeds the
145
+ engine. Several of those are re-exports of app-facing modules, so an editor's
146
+ auto-import may offer `redirect` or `revalidate` from `host`. They are the
147
+ same functions; an app imports them from their own entry points:
148
+
149
+ | in an app | not |
150
+ | --- | --- |
151
+ | `@rsc-kit/core/redirect` | `@rsc-kit/core/host` |
152
+ | `@rsc-kit/core/revalidate` | `@rsc-kit/core/host` |
153
+
154
+ Only an adapter imports `host`. The re-exports there are marked internal, so
155
+ they sort below the app entry in a completion list.