@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.
- package/dist/answers.d.ts +7 -0
- package/dist/answers.js +26 -0
- package/dist/answers.js.map +1 -1
- package/dist/bundleGuides.d.ts +22 -0
- package/dist/bundleGuides.js +130 -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 +81 -6
- package/dist/recipes.js.map +1 -1
- package/dist/report.d.ts +11 -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/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/images.md +83 -0
- package/guides/index.json +162 -0
- package/guides/mcp.md +113 -0
- package/guides/metadata.md +289 -0
- package/guides/navigation.md +84 -0
- package/guides/no-javascript.md +39 -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/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
package/guides/errors.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Errors and 404s
|
|
2
|
+
|
|
3
|
+
> What a visitor sees when a page throws, or asks for a url nothing answers.
|
|
4
|
+
|
|
5
|
+
Two files, both optional, both following the same rule as `loading.tsx`: the
|
|
6
|
+
nearest one wins.
|
|
7
|
+
|
|
8
|
+
## When a page throws
|
|
9
|
+
|
|
10
|
+
Put an `error.tsx` in the directory you want to cover:
|
|
11
|
+
|
|
12
|
+
```tsx title="src/app/orders/error.tsx"
|
|
13
|
+
'use client'
|
|
14
|
+
|
|
15
|
+
import type { RouteErrorProps } from '@rsc-kit/core/RouteErrorBoundary'
|
|
16
|
+
|
|
17
|
+
export default function OrdersError({ error, reset }: RouteErrorProps) {
|
|
18
|
+
return (
|
|
19
|
+
<section>
|
|
20
|
+
<h1>That did not work</h1>
|
|
21
|
+
<p>{error.message}</p>
|
|
22
|
+
<button type="button" onClick={reset}>Try again</button>
|
|
23
|
+
</section>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Anything below it that throws renders this instead. The layouts above stay on
|
|
29
|
+
screen — only the segment that failed is replaced.
|
|
30
|
+
|
|
31
|
+
`reset()` renders the segment again, for a failure that might not happen twice.
|
|
32
|
+
Navigating away clears it on its own.
|
|
33
|
+
|
|
34
|
+
:::caution[It has to be a client component]
|
|
35
|
+
`'use client'` at the top, or the build refuses it. Catching a render error is
|
|
36
|
+
a class component's job, it runs in the browser, and `reset` is a callback the
|
|
37
|
+
browser calls — none of which a server component can do.
|
|
38
|
+
:::
|
|
39
|
+
|
|
40
|
+
### What `error.message` says in production
|
|
41
|
+
|
|
42
|
+
React replaces it with a generic sentence and puts a hash on `error.digest`.
|
|
43
|
+
That is React, not this package: the real message could name a table, a query or
|
|
44
|
+
a path, and none of that should reach a browser.
|
|
45
|
+
|
|
46
|
+
Log the digest where you log the error, and the two line up.
|
|
47
|
+
|
|
48
|
+
### It does not catch everything
|
|
49
|
+
|
|
50
|
+
- **Errors in the layout above it.** The boundary sits inside that layout, so a
|
|
51
|
+
layout that throws needs an `error.tsx` a directory up.
|
|
52
|
+
- **The build.** A page that throws every time it renders fails the build
|
|
53
|
+
rather than shipping a stored error page. The boundary is for a request that
|
|
54
|
+
goes wrong, not a page that is broken.
|
|
55
|
+
|
|
56
|
+
## When nothing answers the url
|
|
57
|
+
|
|
58
|
+
`src/app/not-found.tsx` is rendered for any url no route matches:
|
|
59
|
+
|
|
60
|
+
```tsx title="src/app/not-found.tsx"
|
|
61
|
+
import Link from '@rsc-kit/core/Link'
|
|
62
|
+
|
|
63
|
+
export default function NotFound() {
|
|
64
|
+
return (
|
|
65
|
+
<main>
|
|
66
|
+
<h1>No such page</h1>
|
|
67
|
+
<p>Nothing answers that url. <Link href="/">Go home</Link>.</p>
|
|
68
|
+
</main>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
It renders through your root layout like any other page, and is served with a
|
|
74
|
+
real **404** — a page that says "not found" under a `200` is a page search
|
|
75
|
+
engines index.
|
|
76
|
+
|
|
77
|
+
An ordinary server component, so it can be `async` and read whatever it likes.
|
|
78
|
+
Without one, an unmatched url gets a plain `Not found` string.
|
|
79
|
+
|
|
80
|
+
### When the page has to decide
|
|
81
|
+
|
|
82
|
+
A url can match a route and still name nothing — `/posts/42` is a real route
|
|
83
|
+
and there may be no post 42. Only the page can know that, so the page says so:
|
|
84
|
+
|
|
85
|
+
```tsx title="src/app/posts/[slug]/page.tsx"
|
|
86
|
+
import { notFound } from '@rsc-kit/core/not-found'
|
|
87
|
+
|
|
88
|
+
export default async function PostPage({ params }) {
|
|
89
|
+
const post = await findPost((await params).slug)
|
|
90
|
+
|
|
91
|
+
if (!post) notFound()
|
|
92
|
+
|
|
93
|
+
return <article>{post.title}</article>
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Same page, same **404**. It throws, so nothing after it runs — and if you wrap
|
|
98
|
+
the call in a `try`/`catch`, rethrow what you do not recognise, or a missing
|
|
99
|
+
page becomes a blank region.
|
|
100
|
+
|
|
101
|
+
:::caution[Where you call it decides the status]
|
|
102
|
+
Called above every Suspense boundary — at the top of a page, in a layout —
|
|
103
|
+
nothing has been written yet, so the response is a real 404.
|
|
104
|
+
|
|
105
|
+
Called deeper, inside a boundary, the shell has already gone out with the
|
|
106
|
+
status line on it. The boundary shows its fallback instead and the status stays
|
|
107
|
+
`200`. If the status matters — and for anything a crawler or a cache sees, it
|
|
108
|
+
does — do the lookup above the boundary.
|
|
109
|
+
:::
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# File uploads
|
|
2
|
+
|
|
3
|
+
> Sending files through a server action without encoding them.
|
|
4
|
+
|
|
5
|
+
A `File` is a value a server action can take. React's Flight format carries it
|
|
6
|
+
as binary, so there is nothing to encode on the way out and nothing to decode
|
|
7
|
+
on the way in.
|
|
8
|
+
|
|
9
|
+
## One file
|
|
10
|
+
|
|
11
|
+
```tsx title="src/components/AvatarUpload.tsx"
|
|
12
|
+
"use client";
|
|
13
|
+
|
|
14
|
+
import { useState } from 'react';
|
|
15
|
+
import { uploadAvatar } from '../actions';
|
|
16
|
+
|
|
17
|
+
export function AvatarUpload() {
|
|
18
|
+
const [url, setUrl] = useState<string | null>(null);
|
|
19
|
+
|
|
20
|
+
async function submit(formData: FormData) {
|
|
21
|
+
const file = formData.get('avatar') as File;
|
|
22
|
+
|
|
23
|
+
if (!file || file.size === 0) return;
|
|
24
|
+
|
|
25
|
+
setUrl(await uploadAvatar(file));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<form action={submit}>
|
|
30
|
+
<input type="file" name="avatar" accept="image/*" />
|
|
31
|
+
<button type="submit">Upload</button>
|
|
32
|
+
{url && <img src={url} alt="" />}
|
|
33
|
+
</form>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The action receives the `File` itself — name, type, size and all:
|
|
39
|
+
|
|
40
|
+
```ts title="src/actions.ts"
|
|
41
|
+
'use server'
|
|
42
|
+
|
|
43
|
+
import { writeFile } from 'node:fs/promises';
|
|
44
|
+
|
|
45
|
+
export async function uploadAvatar(file: File): Promise<string> {
|
|
46
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
47
|
+
|
|
48
|
+
await writeFile(`./public/avatars/${file.name}`, bytes);
|
|
49
|
+
|
|
50
|
+
return `/avatars/${file.name}`;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Nothing about that is upload-specific: it is the same call, the same
|
|
55
|
+
serialisation and the same round trip as [any other server
|
|
56
|
+
action](/guides/server-actions).
|
|
57
|
+
|
|
58
|
+
## The whole form at once
|
|
59
|
+
|
|
60
|
+
An action can take the `FormData` instead of individual arguments, which is
|
|
61
|
+
usually simpler when the form mixes files and fields:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
"use client";
|
|
65
|
+
|
|
66
|
+
import { createPost } from '../actions';
|
|
67
|
+
|
|
68
|
+
export function NewPost() {
|
|
69
|
+
return (
|
|
70
|
+
<form action={createPost}>
|
|
71
|
+
<input name="title" />
|
|
72
|
+
<textarea name="body" />
|
|
73
|
+
<input type="file" name="cover" accept="image/*" />
|
|
74
|
+
<button type="submit">Create</button>
|
|
75
|
+
</form>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```ts title="src/actions.ts"
|
|
81
|
+
'use server'
|
|
82
|
+
|
|
83
|
+
export async function createPost(formData: FormData) {
|
|
84
|
+
const title = formData.get('title') as string;
|
|
85
|
+
const cover = formData.get('cover') as File;
|
|
86
|
+
|
|
87
|
+
// …
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Several files
|
|
92
|
+
|
|
93
|
+
`formData.getAll` gives every file from a `multiple` input, and an array of
|
|
94
|
+
`File` serialises as readily as one:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
const files = formData.getAll('photos') as File[];
|
|
98
|
+
|
|
99
|
+
await uploadPhotos(files);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```ts title="src/actions.ts"
|
|
103
|
+
'use server'
|
|
104
|
+
|
|
105
|
+
export async function uploadPhotos(files: File[]) {
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
// …
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Size
|
|
113
|
+
|
|
114
|
+
The whole body is buffered before the action runs — it is one `POST`, not a
|
|
115
|
+
stream — so an upload occupies memory for as long as it takes. Two consequences
|
|
116
|
+
worth planning for:
|
|
117
|
+
|
|
118
|
+
- Keep any limit your host imposes as low as the app actually needs.
|
|
119
|
+
- For genuinely large files, do not send them through an action at all. Have the action mint a pre-signed URL and let the browser upload straight to storage; the bytes never touch your server.
|
package/guides/fonts.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Fonts
|
|
2
|
+
|
|
3
|
+
> Self-hosted fonts from npm, and what next/font was doing for you.
|
|
4
|
+
|
|
5
|
+
Install the font, import its CSS, name it in a variable. Vite does the rest.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @fontsource-variable/fraunces @fontsource-variable/geist
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```css title="src/app/styles.css"
|
|
12
|
+
@import '@fontsource-variable/fraunces/full.css';
|
|
13
|
+
@import '@fontsource-variable/fraunces/full-italic.css';
|
|
14
|
+
@import '@fontsource-variable/geist';
|
|
15
|
+
|
|
16
|
+
:root {
|
|
17
|
+
--font-display: 'Fraunces Variable', ui-serif, Georgia, Cambria, 'Times New Roman', serif;
|
|
18
|
+
--font-sans: 'Geist Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
19
|
+
'Helvetica Neue', Arial, sans-serif,
|
|
20
|
+
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Put your font in *front* of a full stack rather than in place of one. Two things
|
|
25
|
+
the rest of the stack does that a bare `sans-serif` does not:
|
|
26
|
+
|
|
27
|
+
- **Emoji.** Geist has no emoji glyphs, so an emoji falls through to whatever is
|
|
28
|
+
named after it. With nothing there, some Linux and older Android setups draw a
|
|
29
|
+
monochrome glyph or a box. The four emoji fonts at the end are consulted only
|
|
30
|
+
for characters the main font cannot draw, so they cost nothing.
|
|
31
|
+
- **A closer fallback.** During the `swap` window the browser shows the next
|
|
32
|
+
font. `-apple-system` and `Segoe UI` are nearer Geist in width and x-height
|
|
33
|
+
than Helvetica or Arial, so the moment of swap is a smaller jolt.
|
|
34
|
+
|
|
35
|
+
Those are Tailwind's own defaults, and shadcn's generated `--font-sans:
|
|
36
|
+
'Geist Variable', sans-serif` drops both. Keep the stack.
|
|
37
|
+
|
|
38
|
+
[Fontsource](https://fontsource.org) packages every Google font — and many
|
|
39
|
+
others — as woff2 files with the `@font-face` rules already written. Vite
|
|
40
|
+
hashes the files and serves them beside your other assets, so **nothing is
|
|
41
|
+
fetched from Google at runtime**, and nothing is downloaded at build time
|
|
42
|
+
either. The files are in `node_modules`.
|
|
43
|
+
|
|
44
|
+
## Porting from `next/font`
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// before
|
|
48
|
+
const fraunces = Fraunces({
|
|
49
|
+
variable: '--font-display',
|
|
50
|
+
subsets: ['latin'],
|
|
51
|
+
style: ['normal', 'italic'],
|
|
52
|
+
axes: ['SOFT', 'opsz'],
|
|
53
|
+
display: 'swap',
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Every option there is something Fontsource already did:
|
|
58
|
+
|
|
59
|
+
| `next/font` | Fontsource |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `variable: '--font-display'` | the `:root` rule above — you write the variable, once |
|
|
62
|
+
| `subsets: ['latin']` | every subset ships, each with a `unicode-range`; the browser fetches only the ones the page uses |
|
|
63
|
+
| `style: ['normal', 'italic']` | `full.css` and `full-italic.css` |
|
|
64
|
+
| `axes: ['SOFT', 'opsz']` | `full.css` — every axis of the variable font; `standard.css` is weight alone |
|
|
65
|
+
| `display: 'swap'` | already in every rule |
|
|
66
|
+
| `className={fraunces.variable}` | nothing — the variable is on `:root` |
|
|
67
|
+
|
|
68
|
+
So the layout loses its font imports and the `className` gains nothing:
|
|
69
|
+
|
|
70
|
+
```tsx title="src/app/layout.tsx"
|
|
71
|
+
import './styles.css'
|
|
72
|
+
|
|
73
|
+
export default function RootLayout({ children }) {
|
|
74
|
+
return (
|
|
75
|
+
<html lang="en" className="h-full antialiased dark font-sans">
|
|
76
|
+
…
|
|
77
|
+
</html>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The subsetting is worth a second look, because it is *better* than what you
|
|
83
|
+
had. `next/font` asks you to name subsets at build time and ships those.
|
|
84
|
+
Fontsource ships all of them, each behind a `unicode-range`, and the browser
|
|
85
|
+
downloads only the ones a page's text actually needs — a Latin-only page
|
|
86
|
+
fetches one file whatever else is installed.
|
|
87
|
+
|
|
88
|
+
## Preloading
|
|
89
|
+
|
|
90
|
+
The one thing `next/font` did that needs a line here. It added a `<link
|
|
91
|
+
rel="preload">` for each font so the browser finds it before the stylesheet
|
|
92
|
+
does. Without one, the font is discovered when the CSS is parsed, and the text
|
|
93
|
+
shows in the fallback face for a moment longer.
|
|
94
|
+
|
|
95
|
+
Import the file for its url, and React hoists the link:
|
|
96
|
+
|
|
97
|
+
```tsx title="src/app/layout.tsx"
|
|
98
|
+
import fraunces from '@fontsource-variable/fraunces/files/fraunces-latin-full-normal.woff2?url'
|
|
99
|
+
|
|
100
|
+
export default function RootLayout({ children }) {
|
|
101
|
+
return (
|
|
102
|
+
<html>
|
|
103
|
+
<head>
|
|
104
|
+
<link rel="preload" href={fraunces} as="font" type="font/woff2" crossOrigin="anonymous" />
|
|
105
|
+
</head>
|
|
106
|
+
…
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`?url` is Vite's — it hands back the hashed path the build will serve. Preload
|
|
110
|
+
the one file the first paint needs, usually the Latin regular; preloading all
|
|
111
|
+
of them defeats the subsetting.
|
|
112
|
+
|
|
113
|
+
## A font that is not on npm
|
|
114
|
+
|
|
115
|
+
A file you own goes in `public/` and gets its own `@font-face` in your CSS, the
|
|
116
|
+
same as anywhere. Or in `src/` and imported with `?url` as above, which hashes
|
|
117
|
+
it.
|