@rsc-kit/mcp 0.16.3 → 0.17.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 +14 -1
- package/dist/answers.js +76 -34
- package/dist/answers.js.map +1 -1
- package/dist/recipes.js +119 -4
- package/dist/recipes.js.map +1 -1
- package/dist/report.d.ts +12 -0
- package/dist/report.js +17 -15
- package/dist/report.js.map +1 -1
- package/guides/coming-from-next.md +9 -4
- package/guides/emails.md +90 -0
- package/guides/errors.md +5 -1
- package/guides/images.md +10 -4
- package/guides/index.json +10 -0
- package/guides/installation.md +8 -9
- package/guides/mcp.md +1 -1
- package/guides/metadata.md +1 -1
- package/guides/offline.md +8 -0
- package/guides/routing.md +5 -1
- package/guides/seo-files.md +95 -0
- package/guides/typed-routes.md +9 -0
- package/guides/view-transitions.md +53 -7
- package/package.json +1 -1
package/dist/recipes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recipes.js","sourceRoot":"","sources":["../src/recipes.ts"],"names":[],"mappings":"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","sourcesContent":["// How to build the things this framework has, in the shape that works.\n//\n// The other half of this server, and the more useful one. Introspection answers\n// \"what did my build do\"; this answers \"how do I do X here\", which is the\n// question an agent actually has — and the one it otherwise answers from Next\n// and React habits that produce code which looks right and is not.\n//\n// Long-form on purpose. AGENTS.md has to be short enough to sit in context for\n// every turn, so it can only say the rule. These are fetched when the topic\n// comes up, so they can afford the working example and the caveat under it.\n//\n// Every snippet here is the recommended spelling from the guides, not a\n// paraphrase. When a guide changes, this changes with it — a recipe that has\n// drifted is worse than no recipe, because it is followed with confidence.\n\nexport interface Recipe {\n topic: string\n summary: string\n body: string\n}\n\nconst RECIPES: Recipe[] = [\n {\n topic: 'forms',\n summary: 'Submitting to a server action, with pending state and field errors',\n body: `Use <Form>. It takes the server action itself, not a url.\n\n\\`\\`\\`tsx\n'use client'\nimport Form from '@rsc-kit/core/Form'\nimport { createPost } from '../actions'\n\nexport function NewPost() {\n return (\n <Form action={createPost} schema={schema}>\n {({ pending, errors }) => (\n <>\n <input name=\"title\" />\n {errors.title?.[0] && <p>{errors.title[0]}</p>}\n <button disabled={pending}>Save</button>\n </>\n )}\n </Form>\n )\n}\n\\`\\`\\`\n\nPassing \\`schema\\` validates in the browser BEFORE the action is called, so a\nmistake costs no round trip. It is a courtesy, never a control: the action is a\npublic endpoint reachable without your form, so the server must check too.\n\nA schema on the server (\\`client.input(schema)\\`) does NOT give you client-side\nvalidation. Pass it to the form as well — the same schema is fine.\n\nValues are uncontrolled, so an initial one is React's own \\`defaultValue\\`. A\nrefused submit keeps what was typed, because the DOM kept it.\n\nA repeated name is an array. With one selected it is a string, which no\nz.array() accepts - so for anything that is a list by nature end the name in\n\\`[]\\` and it is always an array, brackets dropped from the key:\n\n\\`\\`\\`tsx\n<input type=\"checkbox\" name=\"tags[]\" value=\"react\" /> // -> { tags: ['react'] }\n\\`\\`\\`\n\nNames that describe a shape build it: \\`address.city\\` nests, and\n\\`items[0].name\\` (or \\`items[0][name]\\`) makes an array of objects. That is\nthe shape the schema was written against, and errors come back keyed the same\nway because Standard Schema issue paths join with dots too.\n\nFor a control with no native element behind it - a rich editor, a Radix select -\nor a value read as it is typed, bind it with \\`field()\\`. It is the same four\nprops react-hook-form's Controller gives:\n\n\\`\\`\\`tsx\n<Form action={save} defaultValues={{ body: '' }}>\n {({ field }) => (\n <>\n <Editor {...field('body')} />\n <span>{field('body').value.length}/100</span>\n </>\n )}\n</Form>\n\\`\\`\\`\n\nonChange takes a DOM event OR a bare value, so native inputs and Radix\ncomponents both work. A bound field is still an ordinary named input, so it\narrives in FormData with the rest - nothing merges.\n\n\\`fieldState(name)\\` is the other half: { touched, invalid, errors }. Two\nobjects rather than one because touched and invalid are not DOM attributes and\nspreading them would warn on every field.\n\n\\`\\`\\`tsx\nconst title = fieldState('title')\n<Field data-invalid={title.invalid}>\n <Input {...field('title')} aria-invalid={title.invalid} />\n <FieldError errors={title.errors.map((message) => ({ message }))} />\n</Field>\n\\`\\`\\`\n\nA field is checked when it is LEFT, not as it is typed, and it works on\nuncontrolled fields too - the form listens for focusout rather than each field\nlistening for blur.\n\nThere is no per-field render prop component here, and that is deliberate.\nTanStack Form is controlled-first, so it needs one - without per-field\nsubscriptions a keystroke re-renders every field. react-hook-form is\nuncontrolled-first like this, and its Controller scopes the re-render of a\ncontrolled field to itself.\n\nfield() is a function call instead, which keeps the markup flat and means a\nbound field re-renders the form rather than only itself. Right for the one or\ntwo controlled fields a form usually has.\n\nWhen it is not, put the field in its own component and use \\`useField\\` there -\nit re-renders that component and nothing else, which is what Controller achieves\nwith a render prop:\n\n\\`\\`\\`tsx\nfunction Title() {\n const { invalid, errors, ...bound } = useField('title')\n\n return <Input {...bound} aria-invalid={invalid} />\n}\n\\`\\`\\`\n\n\\`useFormValues()\\` reads every bound value from anywhere inside the form - a\npreview, a summary. Only BOUND values: an uncontrolled input's value is the\nDOM's and nothing can know it changed.\n\nBoth read a context, so they work below <Form>. For something that is NOT a\ndescendant - a top bar, a sidebar preview - create the store above both and\nhand it in:\n\n\\`\\`\\`tsx\nconst store = useFormStore({ title: '' })\n\n<TopBar store={store} /> // outside the form\n<Form action={save} store={store}>…</Form>\n\\`\\`\\`\n\nuseFormStore is the values and nothing else - no submit, no errors. Creating it\ndoes not subscribe to it, so the holder does not re-render per keystroke and\ntake the subtree with it. useField(name, store) and useFormValues(store) take\none explicitly; without one they read the context.\n\nA submit from outside the form is html, not a second api:\n\n\\`\\`\\`tsx\n<Form id=\"bug-report\" action={reportBug}>…</Form>\n<Button type=\"submit\" form=\"bug-report\">Submit</Button>\n\\`\\`\\`\n\nThere is no useForm hook. <Form> is the whole surface.\n\nFields are real \\`name\\` attributes rather than controlled state, so the form\nreads a native FormData and any component rendering a real control works.\n\nIt works before hydration. The action is on the form element as well as in the\nsubmit handler, so the markup is submittable on its own - the handler calls\npreventDefault() first and React does not run a form action for a cancelled\nsubmit, so exactly one path runs.\n\n**shadcn/ui works as-is.** Input, Textarea, Button and Label are styled native\nelements, so \\`name\\` does what it always does. Select, Checkbox, Switch and\nRadioGroup are Radix underneath and render a hidden native control whenever\ngiven a \\`name\\` - omit it and they are invisible to the form, which is the\nonly thing to remember.\n\nDo NOT use shadcn's own Form/FormField/FormControl with this. Those wrap\nreact-hook-form, a different system for the same job. One or the other.`,\n },\n {\n topic: 'prefetch',\n summary: 'Making a navigation feel instant',\n body: `\\`<Link>\\` prefetches on hover by default. Usually there is nothing to do.\n\n\\`\\`\\`tsx\nimport Link from '@rsc-kit/core/Link'\n\n<Link href=\"/orders\">Orders</Link>\n<Link href=\"/orders\" prefetch={false}>Orders</Link> // opt out\n<Link href=\"/orders\" cacheFor={30_000}>Orders</Link> // hold the payload longer\n\\`\\`\\`\n\n\\`href\\` is typed to the routes the build found, so a link to a page that no\nlonger exists stops compiling. Cast with \\`as Href\\` only when the destination\nis genuinely computed.\n\nTo prefetch from code — a row about to be clicked, a wizard's next step:\n\n\\`\\`\\`ts\nimport { prefetch } from '@rsc-kit/core/navigate'\n\nprefetch('/orders/42')\n\\`\\`\\`\n\nWhat is prefetched is the RSC payload, not the html, so it is small and it warms\nthe same cache the navigation will read.`,\n },\n {\n topic: 'validation',\n summary: 'Checking input — forms, actions, urls and request bodies',\n body: `One contract everywhere: any Standard Schema (Zod, Valibot, ArkType).\n\n**Actions** validate on arrival and RETURN their failures, because React strips\na thrown message in production:\n\n\\`\\`\\`ts\nexport const createPost = client.input(schema).handler(async ({ input, ctx }) => …)\n\\`\\`\\`\n\n**Urls** validate by exporting a schema beside the page or route:\n\n\\`\\`\\`ts\nexport const params = z.object({ slug: z.string().min(1) })\nexport const searchParams = z.object({ page: z.coerce.number().int().min(1).default(1) })\n\\`\\`\\`\n\nValues arrive parsed and typed — \\`?page=3\\` is the number 3, a missing one is\nthe default. Never hand-parse \\`Number(searchParams.get('page'))\\`.\n\nThe same schema types every LINK to that page. Write search params as an\nobject, never as a string:\n\n\\`\\`\\`tsx\n<Link href=\"/search\" search={{ q: 'shoes', page: 2 }}>…</Link> // typed by the page's schema\nvisit(href('/search', { q: 'shoes' })) // same check, as a string\n\\`\\`\\`\n\nA key the page never reads, or a number written as text, does not compile;\na key the page requires is required on the link. A page with no schema takes\nany scalars. Do NOT build \\`?q=\\${q}\\` by hand when the page has a schema.\n\n**Api route bodies** the same way:\n\n\\`\\`\\`ts\nexport const body = z.object({ title: z.string().min(1) })\n\nexport async function POST(request: Request, { body }) {\n const { title } = await body\n}\n\\`\\`\\`\n\nThe failures answer differently on purpose:\n\n bad params 404 — the url does not describe a page\n bad searchParams the error boundary (400 for an api route)\n bad body 422, the status an action already uses\n\nA bad query is deliberately NOT a 404, or one bad link makes a real page look\ndeleted.`,\n },\n {\n topic: 'action-client',\n summary: 'Middleware for server actions, so a check cannot be forgotten',\n body: `\\`\\`\\`ts title=\"src/server/client.ts\"\n'use server'\nimport { createActionClient } from '@rsc-kit/core/action'\n\nexport const client = createActionClient({ onError: report })\n .use(async ({ next }) => {\n const user = await currentUser()\n\n if (!user) throw new ServerAuthenticationError()\n\n return next({ ctx: { user } })\n })\n\\`\\`\\`\n\n\\`\\`\\`ts title=\"src/server/posts.ts\"\n'use server'\nimport { client } from './client'\n\nexport const createPost = client.input(schema).handler(async ({ input, ctx }) => …)\nexport const getPosts = client.query(async ({ ctx }) => …)\n\\`\\`\\`\n\n\\`.handler()\\` is a mutation (POST). \\`.query()\\` is a read (GET). Both run the\nchain, so \\`ctx.user\\` is typed and non-null inside them.\n\nFor a failure the schema cannot know - an account not found, a slug taken -\nthe handler is given \\`fieldErrors\\`, typed to its own input so a field the\nschema does not have is a compile error:\n\n\\`\\`\\`ts\n.handler(async ({ input, fieldErrors }) => {\n if (!account) return fieldErrors({ email: 'Account not found' })\n})\n\\`\\`\\`\n\nWRITE return fieldErrors(...). It throws either way, but TypeScript cannot see\na never-return through a destructured argument, so without the return the\nvalue you checked stays possibly-undefined on the next line. It lands in\nvalidationErrors on that field, the same place a schema refusal does. This is\nnext-safe-action's returnValidationErrors with no schema argument and no\n_errors nesting.\n\nA plain \"use server\" function with no action client imports the same thing,\nuntyped, from '@rsc-kit/core/action' - the engine converts the throw into the\nreturned { validationErrors } on the way out. Same rule: return fieldErrors(...).\n\nThe point is not convenience. An action cannot be added without the check,\nbecause there is no other constructor to reach for.\n\nStack clients for a narrower rule:\n\n\\`\\`\\`ts\nexport const admin = client.use(async ({ ctx, next }) => {\n if (!ctx.user.isAdmin) throw new ServerAuthorizationError()\n return next({ ctx })\n})\n\\`\\`\\`\n\nRoute \\`middleware.ts\\` does NOT run for actions — an action renders no route.\nThat is why the check goes here.\n\nAn action body - arguments and any uploaded files, read whole - is capped at\n8 MB; over it the answer is 413 before a byte is kept. rscKit({ maxActionBody })\nraises it. For large files, mint a pre-signed url and upload straight to storage.`,\n },\n {\n topic: 'data',\n summary: 'Loading data, streaming it, and when the browser needs to refetch',\n body: `**In a server component, just await it.** No loader, no getServerSideProps.\n\n\\`\\`\\`tsx\nexport default async function Page() {\n const posts = await db.posts.all()\n}\n\\`\\`\\`\n\n**Better: do not await.** Pass the promise down and let a client component\nresolve it — the shell paints at once and the rows stream into the same\nresponse, with no request from the browser:\n\n\\`\\`\\`tsx\nexport default function Page() {\n const posts = getPosts() // not awaited\n\n return (\n <Suspense fallback={<Skeleton />}>\n <List posts={posts} /> {/* 'use client': use(posts) */}\n </Suspense>\n )\n}\n\\`\\`\\`\n\nReach for this first. It is the thing RSC is for.\n\n**When the BROWSER decides to refetch** — a filter, a poll, a refresh — that is\na cache library's job and this package does not ship one:\n\n\\`\\`\\`tsx\nuseQuery({ queryKey: ['posts', kind], queryFn: () => fetchQuery(getPosts, [kind]) })\nuseSWR(['posts', kind], () => fetchQuery(getPosts, [kind]))\n\\`\\`\\`\n\n\\`fetchQuery\\` sends the read as a GET and goes to the server every time, which\nis what a fetcher needs — staleness and revalidation belong to the library\nholding the answer. Do not add a cache on top of it.\n\nKeep the arrow: TanStack calls a bare \\`queryFn\\` with its own context, and a\nserver function serialises whatever it is handed.`,\n },\n {\n topic: 'suspense',\n summary: 'Where boundaries go, and why the build cares',\n body: `A boundary is what lets a page be stored with a hole in it rather than not\nstored at all.\n\n\\`\\`\\`tsx\n<Suspense fallback={<Skeleton />}>\n <Slow />\n</Suspense>\n\\`\\`\\`\n\nOr a \\`loading.tsx\\` beside the page, which is the same thing for the whole\nroute.\n\nThe build renders every page. Whatever has not resolved when the budget expires\nbecomes the hole; everything above it is stored and served instantly. So a page\nwith no boundary above its slow part cannot be stored at all — the build says\nso:\n\n ƒ /orders\n blocks before anything can paint. Add a loading.tsx beside it, or put a\n <Suspense> above the waiting, and it has a skeleton to store.\n\nA boundary does NOT fix a frozen \\`Date.now()\\`. Prerendering renders straight\nthrough a component that never awaits, so the value is captured exactly as\nbefore. A boundary becomes a hole only when something inside it waits.`,\n },\n {\n topic: 'offline',\n summary: 'Service worker, and what it does and does not cache',\n body: `\\`\\`\\`ts title=\"vite.config.ts\"\nrscKit({ offline: true })\n\\`\\`\\`\n\nThe build writes a service worker that precaches the client bundle and caches\npages at runtime — a document fetch warms its payload, a payload fetch warms its\ndocument, so a page reached by a link still works when reloaded offline.\n\nPages the build stored whole are served from the cache FIRST, because they\ncannot change until a deploy and a deploy sweeps the cache. Everything else is\nnetwork-first with the cache as fallback.\n\nNothing marked \\`no-store\\` is ever kept — which is how a guarded page and a\nsession-reading query stay out of a cache that has no notion of who asked.\n\nIn a component:\n\n\\`\\`\\`tsx\nimport { useOnline } from '@rsc-kit/core/useOnline'\n\nconst online = useOnline()\n\\`\\`\\`\n\nThere is no push and no background sync. Push needs a subscription endpoint and\na sender; background sync needs idempotent replay. Both are the app's decisions.`,\n },\n {\n topic: 'pwa',\n summary: 'Making the app installable',\n body: `A manifest file beside the routes:\n\n\\`\\`\\`ts title=\"src/app/manifest.ts\"\nimport type { WebManifest } from '@rsc-kit/core/manifest-file'\n\nexport default {\n name: 'Orders',\n shortName: 'Orders',\n themeColor: '#0b0b0c',\n backgroundColor: '#ffffff',\n} satisfies WebManifest\n\\`\\`\\`\n\nRead at build time, so it must be an object literal — not computed, not\nimported from elsewhere.\n\n**Icons need no listing.** Put them in \\`src/app/\\` and the build finds them:\n\n favicon.ico served at /favicon.ico\n icon-192.png <link rel=\"icon\">, and the manifest's icons\n icon-512.png\n apple-icon.png <link rel=\"apple-touch-icon\">\n opengraph-image.png <meta property=\"og:image\">\n twitter-image.png <meta name=\"twitter:image\">\n\nSizes are read from the filename. The build says whether it worked:\n\n [rsc-kit] manifest: Orders is installable\n [rsc-kit] manifest: no icons, so no browser will offer to install this.\n\nThere is no layout to edit — React hoists the tags into <head>.\n\nPair it with \\`offline: true\\`. They are separate options because they are\nseparate decisions.\n\n**Push and background sync** need listeners the generated worker does not have,\nso it imports yours from \\`src/app/sw.js\\` — plain javascript, evaluated by the\nbrowser with no build step in front of it:\n\n\\`\\`\\`js\nself.addEventListener('push', (event) => {\n const payload = event.data ? event.data.json() : {}\n\n event.waitUntil(self.registration.showNotification(payload.title, { body: payload.body }))\n})\n\\`\\`\\`\n\nThe rest is the web api and \\`web-push\\`, not this package: VAPID keys, a\nsubscribe call behind a button, the subscription stored by a server action\nagainst a USER rather than a session, and a sender that deletes an endpoint on\n404 or 410 rather than retrying a dead one forever.\n\nFor background sync, make the endpoint idempotent. The browser decides when a\nsync runs and may run it more than once — a request that reached the server\nwhose response did not arrive is retried, and if that posts a message twice the\nperson sent it twice.`,\n },\n {\n topic: 'no-javascript',\n summary: 'The default is none; \"use client\" is how a page asks for it',\n body: `There is NO JavaScript on a stored page until something on it needs some.\nA route that freezes whole, renders none of the app's client components, and\nhas no server action in its tree ships nothing - no React, no router. The\nbuild says so:\n\n ○ /about no js\n no client components, so ships no javascript; stylesheet inlined\n\n\"use client\" IS the opt-in. Put a client component on the page - a counter, a\n<Link>, an update prompt - and it has the runtime, because there is now\nsomething for the runtime to do. There is NO switch in either direction:\nnothing can need the runtime without a client component or an action in the\ntree, and a page that must stay this way is an assertion on build-report.json\n(its clientJs is null), not a setting.\n\nThe check reads the rendered tree, so a <Link> in a shared layout counts -\npages under a layout with a nav keep the runtime; a route group with its own\nplain layout drops it. Navigation into such a page from a Link elsewhere\nstill works: its flight payload is written with the wrappers.\n\nA page without the runtime also gets its stylesheet inlined when small\n(rscKit({ inlineStylesheets }) to change), and still registers the service\nworker with one inlined line.\n\nDo NOT restructure an app to chase this, and do NOT look for export const\nclientJs - it does not exist. The size column says what each route costs; a\npage that is 82 kB because of one <Link> is fine.`,\n },\n {\n topic: 'api-routes',\n summary: 'HTTP endpoints beside the pages',\n body: `\\`src/app/**/route.ts\\`, one export per method:\n\n\\`\\`\\`ts title=\"src/app/api/posts/[id]/route.ts\"\nexport const params = z.object({ id: z.coerce.number().int() })\nexport const body = z.object({ title: z.string().min(1) })\n\nexport async function GET(request: Request, { params }) {\n const { id } = await params\n\n return Response.json(await findPost(id))\n}\n\nexport async function POST(request: Request, { params, body }) {\n const { title } = await body\n\n return Response.json(await createPost(title), { status: 201 })\n}\n\\`\\`\\`\n\nA real \\`Request\\` in, a real \\`Response\\` out. \\`params\\`, \\`searchParams\\` and\n\\`body\\` are awaited, the same way a page's props are.\n\nFetch one through \\`apiUrl\\` and the path is checked against the routes the\nbuild found:\n\n import { apiUrl } from '@rsc-kit/core/routes'\n await fetch(apiUrl('/api/posts/' + id))\n\nPages and api routes are separate unions: Link refuses an api url, apiUrl\nrefuses a page. It checks the PATH, not the response type - for types across\nthe boundary use a server action or a query, where the return type is the\nfunction's because it is the same function.\n\nThey run their directory's \\`middleware.ts\\`, so an endpoint under a guarded\npath is guarded.\n\nA \\`GET\\` that reads nothing from the request is answered from disk. Awaiting\n\\`searchParams\\` says the answer depends on the query; never touching it means\nthe stored answer is served for any query at all.\n\nExporting a \\`body\\` schema consumes the stream, so \\`request.json()\\` inside the\nhandler will find it already read. Use the parsed value.\n\nA GET that reads nothing is stored - and two more things keep one per request:\na Set-Cookie on its Response (an answer for one visitor; classified dynamic,\nwhatever it read), and a body it froze from Date.now() or Math.random() is\nstored WITH a warning on the route's line. Read the request (await\nconnection()) to run it on demand.`,\n },\n {\n topic: 'authorization',\n summary: 'Guarding pages, actions, api routes and queries',\n body: `Each entry point defends itself. There is no single place that covers all of\nthem, and believing otherwise is how a hole is left.\n\n**A page or an api route**: \\`middleware.ts\\` in its directory guards everything\nat or below it.\n\n\\`\\`\\`ts title=\"src/app/admin/middleware.ts\"\nimport { redirect } from '@rsc-kit/core/redirect'\n\nexport default async function guard() {\n if (!(await currentUser())) redirect('/login')\n}\n\\`\\`\\`\n\n**An action or a query**: middleware does NOT run — they render no route. Build\nthem from an action client so the check cannot be forgotten. See the\n\\`action-client\\` topic.\n\n**Authorise on identity, not arguments.** \\`deletePost(id)\\` that trusts the id\nis the whole of an IDOR: the caller chooses the id, so check the row belongs to\n\\`ctx.user\\`.\n\nA guarded page can still be frozen at build time — the guard is a serving\ndecision, not a build one. Its response is marked private so no cache keeps it.`,\n },\n {\n topic: 'dynamic',\n summary: 'Why a page is not static, and how to choose',\n body: `A page is stored at build time unless it reads the request. Reading it is what\nopts out, and the accessors are async:\n\n\\`\\`\\`ts\nimport { cookies, headers, searchParams, connection } from '@rsc-kit/core/request'\n\nconst theme = (await cookies()).get('theme')\nawait connection() // \"render this per visitor\", said deliberately\n\\`\\`\\`\n\nA page's \\`params\\` and \\`searchParams\\` props are promises for the same reason.\n\nThe build says which call did it, per route:\n\n ◐ /locale 85 kB\n dynamic — called cookies(), headers()\n\nThat is usually correct — a page whose content depends on who is asking cannot\nbe one stored file. Change it only when the read was accidental.\n\nFor a parameterised route, \\`generateStaticParams\\` turns one shell into a page\nper url:\n\n\\`\\`\\`ts\nexport async function generateStaticParams() {\n return (await db.posts.all()).map((p) => ({ slug: p.slug }))\n}\n\\`\\`\\`\n\nA value that must differ per visitor but needs no server — a clock,\nlocalStorage, a map — belongs in the browser only:\n\n\\`\\`\\`tsx\n'use client'\nimport { browser } from 'react-dom'\n\nfunction Clock() {\n use(browser('the time is the visitor\\\\'s, not the build machine\\\\'s'))\n}\n\\`\\`\\`\n\nIt needs a Suspense boundary, and the page stays frozen.\n\nThere is NO app-wide switch to turn prerendering off (no rscKit({ prerender })\nand no export const dynamic). A page that must render per request says\nawait connection() in the page; the build names any page it could not render\nat build time, and that is the page to mark. \"All of it per request\" is\nawait connection() in the root layout with a root loading.tsx - every page a\nstored fallback with the rest streamed, five times slower on a page that could\nhave been stored (bench/ in the repo). Do not reach for it.`,\n },\n {\n topic: 'metadata',\n summary: 'Titles, share cards, and the one setting production needs',\n body: `\\`\\`\\`tsx\nexport const metadata: Metadata = {\n title: 'Orders',\n openGraph: { title: 'Orders', description: '…', images: '/cover.png' },\n}\n\\`\\`\\`\n\nA layout takes a title TEMPLATE - { template: '%s · Site', default: 'Site' } -\nand layouts merge outward-in, so site-wide values go on the root layout once.\n\n**Set metadataBase on the root layout. It is not optional in production.**\n\n\\`\\`\\`tsx\nmetadataBase: new URL('https://example.com')\n\\`\\`\\`\n\nA share-card scraper needs an ABSOLUTE image url and Facebook, Slack and\nLinkedIn refuse a relative one silently - the link unfurls with no image and\nnothing says why. metadataBase makes every relative url, image and icon\nabsolute. Same name as Next, so a port carries it across.\n\nUse the structured objects, not the flat 'og:title' spellings: openGraph and\ntwitter are typed, an image can be { url, width, height, alt }, and it is the\nshape a Next app already has. og: renders as property=, twitter: as name= -\nwhat each scraper reads.\n\nAn opengraph-image.png in app/ is found by name and needs no listing; it still\nneeds metadataBase to go out absolute.`,\n },\n {\n topic: 'fonts',\n summary: 'Self-hosted fonts from npm, and porting next/font',\n body: `There is no font loader. Install the font from Fontsource, import its\ncss, name it in a variable:\n\n\\`\\`\\`css\n@import '@fontsource-variable/fraunces/full.css';\n@import '@fontsource-variable/geist';\n\n:root {\n --font-display: 'Fraunces Variable', ui-serif, Georgia, serif;\n --font-sans: 'Geist Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,\n 'Helvetica Neue', Arial, sans-serif,\n 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';\n}\n\\`\\`\\`\n\nPut the font in FRONT of a full stack, not in place of one. A bare\n'Geist Variable', sans-serif drops the emoji fonts - Geist has no emoji glyphs,\nand with nothing named after it some systems draw a box - and drops the\nmetrics-matched fallback that makes the swap moment smaller. Those are\nTailwind's own defaults; shadcn's generated line loses both.\n\nVite hashes the woff2 files and serves them with the other assets. Nothing is\nfetched from Google at runtime and nothing is downloaded at build - the files\nare in node_modules.\n\nPorting next/font: every option was something Fontsource already did.\nsubsets -> every subset ships behind a unicode-range and the browser fetches\nonly what the page uses. style: ['italic'] -> full-italic.css. axes -> full.css\nhas every axis; standard.css is weight only. display: 'swap' -> already in\nevery rule. className={font.variable} -> nothing, the variable is on :root.\n\nThe one line next/font added that you add yourself is the preload:\n\n\\`\\`\\`tsx\nimport fraunces from '@fontsource-variable/fraunces/files/fraunces-latin-full-normal.woff2?url'\n<link rel=\"preload\" href={fraunces} as=\"font\" type=\"font/woff2\" crossOrigin=\"anonymous\" />\n\\`\\`\\`\n\n?url is Vite's and gives the hashed path. Preload the one file the first paint\nneeds; preloading all of them defeats the subsetting.\n\nDo NOT reach for next/font, @next/font or a Google Fonts link tag.\n\nGETTING TO 100 ON A PHONE (do it last, once the design is settled). Fontsource's\nstylesheet declares every subset of every weight with font-display: swap; a\nthrottled phone audit sees the largest text repaint when the font lands, and\nscores 99. Three moves:\n1. Own the @font-face rules, latin only: import the files with ?url, declare\n them yourself with the latin unicode-range.\n2. font-display: optional for the body faces - the text paints once, in the\n fallback on a cold slow load, in the web font when cached. Keep swap for the\n headline face, which is preloaded and arrives with the document.\n3. One weight per family; variable only where the axes are used. Declare the\n range you ask for (font-weight: 400 500) so nothing requests a missing file.\nPut the preloads before the <style> with the faces. Measured on rsc-kit.dev:\nSpeed Index 1.7s to 0.9s, 99 to a steady 100, fonts 114 kB to 74 kB.\nFull guide: read_guide({ slug: 'fonts' }).`,\n },\n {\n topic: 'from-next',\n summary: 'Porting a Next.js app - what carries over, what to rename, what is different on purpose',\n body: `The app/ conventions are the same: layout, page, loading, error, not-found,\nroute.ts, [slug], [...path], (group), @slot, (.)intercept. \"use client\" and\n\"use server\" are React's. Copy src/app first, fix imports second.\n\nIMPORTS\n next/link -> @rsc-kit/core/Link (href typed; search typed by the page's schema)\n useRouter().push / .replace -> visit(url) / visit(url, { replace: true }) from @rsc-kit/core/router\n useRouter().refresh() -> refresh() from @rsc-kit/core/router, or revalidate() in the action\n usePathname / useSearchParams-> @rsc-kit/core/usePathname, @rsc-kit/core/useSearchParams (nuqs: @rsc-kit/core/nuqs)\n useParams() -> the page's params prop, passed down\n cookies(), headers() -> same names, from @rsc-kit/core/request\n redirect() / notFound() -> @rsc-kit/core/redirect / @rsc-kit/core/not-found\n revalidatePath/Tag -> revalidate('tag') on a section() - targeted, rides back with the action\n Metadata -> @rsc-kit/core/metadata (metadataBase, openGraph, twitter, icons as-is)\n next/font -> Fontsource (how_to fonts)\n next/image -> unpic or vite-imagetools (how_to images)\n next/script -> a <script> tag (how_to scripts)\n NEXT_PUBLIC_* -> VITE_* via import.meta.env; server vars stay process.env\n next-safe-action -> createActionClient() (how_to action-client); returnValidationErrors -> return fieldErrors({...})\n\nDIFFERENT ON PURPOSE\n- No export const dynamic / revalidate = 60. A page is frozen unless it READS\n the request; await connection() is the explicit mark. No time-based ISR.\n- middleware.ts is per directory, on the server, full API; not one edge file.\n It does not run for actions - the check goes in the action.\n- Actions return failures ({ validationErrors }, { serverError }), not throw.\n- No image optimizer, no opengraph-image.tsx - put opengraph-image.png in src/app.\n- Tests need no browser: createTestApp() is the deployed handler.\n- A component library (base-ui, Radix) imports as it did, from server\n components too. A shadcn-style components/ui/ folder keeps \"use client\" at\n the top of each file, as shipped; without it the server evaluates the\n library's internals for nothing.\n\nORDER: scaffold -> copy src/app -> fix imports -> typecheck -> build and READ\nthe output (a cookies() in a layout makes everything dynamic; the build says\nso) -> decide each action the build lists as running no middleware -> check.\n\nFull guide: read_guide({ slug: 'coming-from-next' }).`,\n },\n {\n topic: 'images',\n summary: 'Responsive images with no optimizer - unpic for a CDN, imagetools for files in the repo',\n body: `There is NO image component and NO image server. Do not add next/image or\nwrite an optimizer route. next/image is a srcset-writing component plus a\nresize-on-request process; the first is a library, the second belongs to the\nCDN.\n\nAn image on a CDN (Cloudinary, imgix, Cloudflare Images, Bunny, Vercel,\nNetlify, ...): @unpic/react. Plain component, works in a server component,\nships no javascript, detects the CDN from the url:\n\n\\`\\`\\`tsx\nimport { Image } from '@unpic/react'\n\n<Image src=\"https://res.cloudinary.com/demo/image/upload/sample.jpg\" layout=\"constrained\" width={800} height={600} alt=\"...\" />\n\\`\\`\\`\n\nA file in the repo, a handful of them: vite-imagetools, resized ONCE at build\ntime. Add imagetools() to the vite plugins, then:\n\n\\`\\`\\`tsx\nimport hero from '../hero.png?w=400;800;1200&format=webp&as=srcset'\nimport heroSrc from '../hero.png?w=800&format=webp'\n\n<img srcSet={hero} src={heroSrc} sizes=\"(min-width: 800px) 800px, 100vw\" width={800} height={600} alt=\"...\" />\n\\`\\`\\`\n\nHundreds of files in the repo: that is a CDN's job; move them and use unpic.\nAn icon or a logo: a plain <img>, or inline the svg.\n\nFull guide: read_guide({ slug: 'images' }).`,\n },\n {\n topic: 'scripts',\n summary: 'Third-party scripts - analytics, tag managers - without a Script component',\n body: `Write the script tag. React 19 does what Next's Script component existed for.\n\nAn external script with async, rendered from a server component, is HOISTED\ninto head and DEDUPLICATED by React - the same src in three components is one\ntag. That is afterInteractive:\n\n\\`\\`\\`tsx\n<script async src=\"https://www.clarity.ms/tag/abc123\" />\n\\`\\`\\`\n\nAn inline snippet renders where it is written and runs during parse, before\nhydration - the earlier moment, which is what an analytics snippet wants:\n\n\\`\\`\\`tsx\n<script id=\"ms-clarity\" dangerouslySetInnerHTML={{ __html: '...' }} />\n\\`\\`\\`\n\nPut site-wide scripts in the ROOT LAYOUT, which renders once and is kept\nacross navigations.\n\nThere is no Script component to import. The only case needing one - a script\nthat touches DOM React rendered, or an onLoad callback - is a client component\nwith useEffect that creates the tag. Ten lines of the user's own.`,\n },\n {\n topic: 'testing',\n summary: 'Unit-testing actions, queries and routes; the whole app without a port',\n body: `Almost everything is a function. Any test runner works.\n\n**Actions, queries, api routes: import and call.** \"use server\" is a string in\na test file, so the function is importable. An action built on the action\nclient runs its whole middleware chain when called and RETURNS its failures:\n\n\\`\\`\\`ts\nconst result = await createPost({ title: '' })\nexpect(result.validationErrors).toEqual({ title: ['too short'] })\n\\`\\`\\`\n\nAn api route takes a Request and the context the engine gives it - params is a\nPROMISE:\n\n\\`\\`\\`ts\nconst res = await GET(new Request('https://app.test/api/x'), { params: Promise.resolve({ id: '1' }) })\n\\`\\`\\`\n\n**Anything reading cookies() or headers():** open the request scope yourself.\n\n\\`\\`\\`ts\nimport { withRequest } from '@rsc-kit/core/request'\nawait withRequest(new Request('https://app.test/', { headers: { Cookie: 'session=abc' } }), currentUser)\n\\`\\`\\`\n\n**The whole app as Request -> Response, no port:**\n\n\\`\\`\\`ts\nimport { createTestApp } from '@rsc-kit/core/testing'\nconst app = await createTestApp()\nconst res = await app.fetch('/admin', { redirect: 'manual' }) // real router, real middleware\n\\`\\`\\`\n\nIt builds when the source is newer than the last build - the first run pays,\nthe rest do not. This is where a guard that never ran or a 404 that came back\n200 shows up.\n\n**What still needs a browser:** a server action called OVER THE WIRE (the id is\nReact's and private), hydration, navigation. Playwright against vite preview.\nThat limit is narrower than Next's: the action's logic is a unit test here.\n\n**What to write when you add something.** Before running check, not after:\n\n- A guarded route (middleware.ts, or a page reading the session): a stranger\n is turned away, and someone signed in gets 200.\n \\`\\`\\`ts\n expect((await app.fetch('/admin', { redirect: 'manual' })).status).toBe(302)\n expect((await app.fetch('/admin', { headers: { Cookie: 'session=ada' } })).status).toBe(200)\n \\`\\`\\`\n- An action: its refusal, by calling it. Bad input answers validationErrors;\n a stranger answers serverError (or throws ServerAuthenticationError if you\n built it without the client).\n \\`\\`\\`ts\n expect((await createPost({ title: '' })).validationErrors).toBeDefined()\n \\`\\`\\`\n- An action that takes an id: someone else's id is refused. This is the IDOR\n test and the one most often missing.\n- A query: the shape of its answer, and what a filter changes.\n- An api route: status, content-type, and the 4xx it answers to a bad body.\n- A page that should stay static: assert on the build report - no test, a CI\n check that build-report.json still says frozen for it.\n\nDo NOT start a dev server, spawn a process or pick a port in a test. Do NOT\nadd a second runner. The one in tests/ goes through the real build already.`,\n },\n]\n\n/** Every topic, with one line each — what a caller reads before choosing. */\nexport function listTopics(): string {\n return [\n 'Topics. Ask for one with how_to({ topic }).',\n '',\n ...RECIPES.map((r) => `${r.topic.padEnd(16)} ${r.summary}`),\n ].join('\\n')\n}\n\n/** One recipe, or the list plus a nudge when the topic is not one. */\nexport function howTo(topic: string): string {\n const wanted = topic.trim().toLowerCase().replace(/[\\s_]+/g, '-')\n const found =\n RECIPES.find((r) => r.topic === wanted) ??\n // A near miss is common and worth answering rather than refusing: someone\n // asks for \"form\" or \"queries\" and means the obvious thing.\n RECIPES.find((r) => r.topic.startsWith(wanted) || wanted.startsWith(r.topic)) ??\n RECIPES.find((r) => r.summary.toLowerCase().includes(wanted))\n\n if (!found) return `No topic \"${topic}\".\\n\\n${listTopics()}`\n\n return `# ${found.topic} — ${found.summary}\\n\\n${found.body}`\n}\n\n/** For tests, so a recipe cannot be added without being reachable. */\nexport const TOPICS = RECIPES.map((r) => r.topic)\n"]}
|
|
1
|
+
{"version":3,"file":"recipes.js","sourceRoot":"","sources":["../src/recipes.ts"],"names":[],"mappings":"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","sourcesContent":["// How to build the things this framework has, in the shape that works.\n//\n// The other half of this server, and the more useful one. Introspection answers\n// \"what did my build do\"; this answers \"how do I do X here\", which is the\n// question an agent actually has — and the one it otherwise answers from Next\n// and React habits that produce code which looks right and is not.\n//\n// Long-form on purpose. AGENTS.md has to be short enough to sit in context for\n// every turn, so it can only say the rule. These are fetched when the topic\n// comes up, so they can afford the working example and the caveat under it.\n//\n// Every snippet here is the recommended spelling from the guides, not a\n// paraphrase. When a guide changes, this changes with it — a recipe that has\n// drifted is worse than no recipe, because it is followed with confidence.\n\nexport interface Recipe {\n topic: string\n summary: string\n body: string\n}\n\nconst RECIPES: Recipe[] = [\n {\n topic: 'forms',\n summary: 'Submitting to a server action, with pending state and field errors',\n body: `Use <Form>. It takes the server action itself, not a url.\n\n\\`\\`\\`tsx\n'use client'\nimport Form from '@rsc-kit/core/Form'\nimport { createPost } from '../actions'\n\nexport function NewPost() {\n return (\n <Form action={createPost} schema={schema}>\n {({ pending, errors }) => (\n <>\n <input name=\"title\" />\n {errors.title?.[0] && <p>{errors.title[0]}</p>}\n <button disabled={pending}>Save</button>\n </>\n )}\n </Form>\n )\n}\n\\`\\`\\`\n\nPassing \\`schema\\` validates in the browser BEFORE the action is called, so a\nmistake costs no round trip. It is a courtesy, never a control: the action is a\npublic endpoint reachable without your form, so the server must check too.\n\nA schema on the server (\\`client.input(schema)\\`) does NOT give you client-side\nvalidation. Pass it to the form as well — the same schema is fine.\n\nValues are uncontrolled, so an initial one is React's own \\`defaultValue\\`. A\nrefused submit keeps what was typed, because the DOM kept it.\n\nA repeated name is an array. With one selected it is a string, which no\nz.array() accepts - so for anything that is a list by nature end the name in\n\\`[]\\` and it is always an array, brackets dropped from the key:\n\n\\`\\`\\`tsx\n<input type=\"checkbox\" name=\"tags[]\" value=\"react\" /> // -> { tags: ['react'] }\n\\`\\`\\`\n\nNames that describe a shape build it: \\`address.city\\` nests, and\n\\`items[0].name\\` (or \\`items[0][name]\\`) makes an array of objects. That is\nthe shape the schema was written against, and errors come back keyed the same\nway because Standard Schema issue paths join with dots too.\n\nFor a control with no native element behind it - a rich editor, a Radix select -\nor a value read as it is typed, bind it with \\`field()\\`. It is the same four\nprops react-hook-form's Controller gives:\n\n\\`\\`\\`tsx\n<Form action={save} defaultValues={{ body: '' }}>\n {({ field }) => (\n <>\n <Editor {...field('body')} />\n <span>{field('body').value.length}/100</span>\n </>\n )}\n</Form>\n\\`\\`\\`\n\nonChange takes a DOM event OR a bare value, so native inputs and Radix\ncomponents both work. A bound field is still an ordinary named input, so it\narrives in FormData with the rest - nothing merges.\n\n\\`fieldState(name)\\` is the other half: { touched, invalid, errors }. Two\nobjects rather than one because touched and invalid are not DOM attributes and\nspreading them would warn on every field.\n\n\\`\\`\\`tsx\nconst title = fieldState('title')\n<Field data-invalid={title.invalid}>\n <Input {...field('title')} aria-invalid={title.invalid} />\n <FieldError errors={title.errors.map((message) => ({ message }))} />\n</Field>\n\\`\\`\\`\n\nA field is checked when it is LEFT, not as it is typed, and it works on\nuncontrolled fields too - the form listens for focusout rather than each field\nlistening for blur.\n\nThere is no per-field render prop component here, and that is deliberate.\nTanStack Form is controlled-first, so it needs one - without per-field\nsubscriptions a keystroke re-renders every field. react-hook-form is\nuncontrolled-first like this, and its Controller scopes the re-render of a\ncontrolled field to itself.\n\nfield() is a function call instead, which keeps the markup flat and means a\nbound field re-renders the form rather than only itself. Right for the one or\ntwo controlled fields a form usually has.\n\nWhen it is not, put the field in its own component and use \\`useField\\` there -\nit re-renders that component and nothing else, which is what Controller achieves\nwith a render prop:\n\n\\`\\`\\`tsx\nfunction Title() {\n const { invalid, errors, ...bound } = useField('title')\n\n return <Input {...bound} aria-invalid={invalid} />\n}\n\\`\\`\\`\n\n\\`useFormValues()\\` reads every bound value from anywhere inside the form - a\npreview, a summary. Only BOUND values: an uncontrolled input's value is the\nDOM's and nothing can know it changed.\n\nBoth read a context, so they work below <Form>. For something that is NOT a\ndescendant - a top bar, a sidebar preview - create the store above both and\nhand it in:\n\n\\`\\`\\`tsx\nconst store = useFormStore({ title: '' })\n\n<TopBar store={store} /> // outside the form\n<Form action={save} store={store}>…</Form>\n\\`\\`\\`\n\nuseFormStore is the values and nothing else - no submit, no errors. Creating it\ndoes not subscribe to it, so the holder does not re-render per keystroke and\ntake the subtree with it. useField(name, store) and useFormValues(store) take\none explicitly; without one they read the context.\n\nA submit from outside the form is html, not a second api:\n\n\\`\\`\\`tsx\n<Form id=\"bug-report\" action={reportBug}>…</Form>\n<Button type=\"submit\" form=\"bug-report\">Submit</Button>\n\\`\\`\\`\n\nThere is no useForm hook. <Form> is the whole surface.\n\nFields are real \\`name\\` attributes rather than controlled state, so the form\nreads a native FormData and any component rendering a real control works.\n\nIt works before hydration. The action is on the form element as well as in the\nsubmit handler, so the markup is submittable on its own - the handler calls\npreventDefault() first and React does not run a form action for a cancelled\nsubmit, so exactly one path runs.\n\n**shadcn/ui works as-is.** Input, Textarea, Button and Label are styled native\nelements, so \\`name\\` does what it always does. Select, Checkbox, Switch and\nRadioGroup are Radix underneath and render a hidden native control whenever\ngiven a \\`name\\` - omit it and they are invisible to the form, which is the\nonly thing to remember.\n\nDo NOT use shadcn's own Form/FormField/FormControl with this. Those wrap\nreact-hook-form, a different system for the same job. One or the other.`,\n },\n {\n topic: 'prefetch',\n summary: 'Making a navigation feel instant',\n body: `\\`<Link>\\` prefetches on hover by default. Usually there is nothing to do.\n\n\\`\\`\\`tsx\nimport Link from '@rsc-kit/core/Link'\n\n<Link href=\"/orders\">Orders</Link>\n<Link href=\"/orders\" prefetch={false}>Orders</Link> // opt out\n<Link href=\"/orders\" cacheFor={30_000}>Orders</Link> // hold the payload longer\n\\`\\`\\`\n\n\\`href\\` is typed to the routes the build found, so a link to a page that no\nlonger exists stops compiling. Cast with \\`as Href\\` only when the destination\nis genuinely computed.\n\nTo prefetch from code — a row about to be clicked, a wizard's next step:\n\n\\`\\`\\`ts\nimport { prefetch } from '@rsc-kit/core/navigate'\n\nprefetch('/orders/42')\n\\`\\`\\`\n\nWhat is prefetched is the RSC payload, not the html, so it is small and it warms\nthe same cache the navigation will read.`,\n },\n {\n topic: 'validation',\n summary: 'Checking input — forms, actions, urls and request bodies',\n body: `One contract everywhere: any Standard Schema (Zod, Valibot, ArkType).\n\n**Actions** validate on arrival and RETURN their failures, because React strips\na thrown message in production:\n\n\\`\\`\\`ts\nexport const createPost = client.input(schema).handler(async ({ input, ctx }) => …)\n\\`\\`\\`\n\n**Urls** validate by exporting a schema beside the page or route:\n\n\\`\\`\\`ts\nexport const params = z.object({ slug: z.string().min(1) })\nexport const searchParams = z.object({ page: z.coerce.number().int().min(1).default(1) })\n\\`\\`\\`\n\nValues arrive parsed and typed — \\`?page=3\\` is the number 3, a missing one is\nthe default. Never hand-parse \\`Number(searchParams.get('page'))\\`.\n\nThe same schema types every LINK to that page. Write search params as an\nobject, never as a string:\n\n\\`\\`\\`tsx\n<Link href=\"/search\" search={{ q: 'shoes', page: 2 }}>…</Link> // typed by the page's schema\nvisit(href('/search', { q: 'shoes' })) // same check, as a string\n\\`\\`\\`\n\nA key the page never reads, or a number written as text, does not compile;\na key the page requires is required on the link. A page with no schema takes\nany scalars. Do NOT build \\`?q=\\${q}\\` by hand when the page has a schema.\n\n**Api route bodies** the same way:\n\n\\`\\`\\`ts\nexport const body = z.object({ title: z.string().min(1) })\n\nexport async function POST(request: Request, { body }) {\n const { title } = await body\n}\n\\`\\`\\`\n\nThe failures answer differently on purpose:\n\n bad params 404 — the url does not describe a page\n bad searchParams the error boundary (400 for an api route)\n bad body 422, the status an action already uses\n\nA bad query is deliberately NOT a 404, or one bad link makes a real page look\ndeleted.`,\n },\n {\n topic: 'action-client',\n summary: 'Middleware for server actions, so a check cannot be forgotten',\n body: `\\`\\`\\`ts title=\"src/server/client.ts\"\n'use server'\nimport { createActionClient } from '@rsc-kit/core/action'\n\nexport const client = createActionClient({ onError: report })\n .use(async ({ next }) => {\n const user = await currentUser()\n\n if (!user) throw new ServerAuthenticationError()\n\n return next({ ctx: { user } })\n })\n\\`\\`\\`\n\n\\`\\`\\`ts title=\"src/server/posts.ts\"\n'use server'\nimport { client } from './client'\n\nexport const createPost = client.input(schema).handler(async ({ input, ctx }) => …)\nexport const getPosts = client.query(async ({ ctx }) => …)\n\\`\\`\\`\n\n\\`.handler()\\` is a mutation (POST). \\`.query()\\` is a read (GET). Both run the\nchain, so \\`ctx.user\\` is typed and non-null inside them.\n\nFor a failure the schema cannot know - an account not found, a slug taken -\nthe handler is given \\`fieldErrors\\`, typed to its own input so a field the\nschema does not have is a compile error:\n\n\\`\\`\\`ts\n.handler(async ({ input, fieldErrors }) => {\n if (!account) return fieldErrors({ email: 'Account not found' })\n})\n\\`\\`\\`\n\nWRITE return fieldErrors(...). It throws either way, but TypeScript cannot see\na never-return through a destructured argument, so without the return the\nvalue you checked stays possibly-undefined on the next line. It lands in\nvalidationErrors on that field, the same place a schema refusal does. This is\nnext-safe-action's returnValidationErrors with no schema argument and no\n_errors nesting.\n\nA plain \"use server\" function with no action client imports the same thing,\nuntyped, from '@rsc-kit/core/action' - the engine converts the throw into the\nreturned { validationErrors } on the way out. Same rule: return fieldErrors(...).\n\nThe point is not convenience. An action cannot be added without the check,\nbecause there is no other constructor to reach for.\n\nStack clients for a narrower rule:\n\n\\`\\`\\`ts\nexport const admin = client.use(async ({ ctx, next }) => {\n if (!ctx.user.isAdmin) throw new ServerAuthorizationError()\n return next({ ctx })\n})\n\\`\\`\\`\n\nRoute \\`middleware.ts\\` does NOT run for actions — an action renders no route.\nThat is why the check goes here.\n\nAn action body - arguments and any uploaded files, read whole - is capped at\n8 MB; over it the answer is 413 before a byte is kept. rscKit({ maxActionBody })\nraises it. For large files, mint a pre-signed url and upload straight to storage.`,\n },\n {\n topic: 'data',\n summary: 'Loading data, streaming it, and when the browser needs to refetch',\n body: `**In a server component, just await it.** No loader, no getServerSideProps.\n\n\\`\\`\\`tsx\nexport default async function Page() {\n const posts = await db.posts.all()\n}\n\\`\\`\\`\n\n**Better: do not await.** Pass the promise down and let a client component\nresolve it — the shell paints at once and the rows stream into the same\nresponse, with no request from the browser:\n\n\\`\\`\\`tsx\nexport default function Page() {\n const posts = getPosts() // not awaited\n\n return (\n <Suspense fallback={<Skeleton />}>\n <List posts={posts} /> {/* 'use client': use(posts) */}\n </Suspense>\n )\n}\n\\`\\`\\`\n\nReach for this first. It is the thing RSC is for.\n\n**When the BROWSER decides to refetch** — a filter, a poll, a refresh — that is\na cache library's job and this package does not ship one:\n\n\\`\\`\\`tsx\nuseQuery({ queryKey: ['posts', kind], queryFn: () => fetchQuery(getPosts, [kind]) })\nuseSWR(['posts', kind], () => fetchQuery(getPosts, [kind]))\n\\`\\`\\`\n\n\\`fetchQuery\\` sends the read as a GET and goes to the server every time, which\nis what a fetcher needs — staleness and revalidation belong to the library\nholding the answer. Do not add a cache on top of it.\n\nKeep the arrow: TanStack calls a bare \\`queryFn\\` with its own context, and a\nserver function serialises whatever it is handed.`,\n },\n {\n topic: 'suspense',\n summary: 'Where boundaries go, and why the build cares',\n body: `A boundary is what lets a page be stored with a hole in it rather than not\nstored at all.\n\n\\`\\`\\`tsx\n<Suspense fallback={<Skeleton />}>\n <Slow />\n</Suspense>\n\\`\\`\\`\n\nOr a \\`loading.tsx\\` beside the page, which is the same thing for the whole\nroute.\n\nThe build renders every page. Whatever has not resolved when the budget expires\nbecomes the hole; everything above it is stored and served instantly. So a page\nwith no boundary above its slow part cannot be stored at all — the build says\nso:\n\n ƒ /orders\n blocks before anything can paint. Add a loading.tsx beside it, or put a\n <Suspense> above the waiting, and it has a skeleton to store.\n\nA boundary does NOT fix a frozen \\`Date.now()\\`. Prerendering renders straight\nthrough a component that never awaits, so the value is captured exactly as\nbefore. A boundary becomes a hole only when something inside it waits.`,\n },\n {\n topic: 'offline',\n summary: 'Service worker, and what it does and does not cache',\n body: `\\`\\`\\`ts title=\"vite.config.ts\"\nrscKit({ offline: true })\n\\`\\`\\`\n\nThe build writes a service worker that precaches the client bundle and caches\npages at runtime — a document fetch warms its payload, a payload fetch warms its\ndocument, so a page reached by a link still works when reloaded offline.\n\nPages the build stored whole are served from the cache FIRST, because they\ncannot change until a deploy and a deploy sweeps the cache. Everything else is\nnetwork-first with the cache as fallback.\n\nNothing marked \\`no-store\\` is ever kept — which is how a guarded page and a\nsession-reading query stay out of a cache that has no notion of who asked.\n\nIn a component:\n\n\\`\\`\\`tsx\nimport { useOnline } from '@rsc-kit/core/useOnline'\n\nconst online = useOnline()\n\\`\\`\\`\n\nThere is no push and no background sync. Push needs a subscription endpoint and\na sender; background sync needs idempotent replay. Both are the app's decisions.`,\n },\n {\n topic: 'pwa',\n summary: 'Making the app installable',\n body: `A manifest file beside the routes:\n\n\\`\\`\\`ts title=\"src/app/manifest.ts\"\nimport type { WebManifest } from '@rsc-kit/core/manifest-file'\n\nexport default {\n name: 'Orders',\n shortName: 'Orders',\n themeColor: '#0b0b0c',\n backgroundColor: '#ffffff',\n} satisfies WebManifest\n\\`\\`\\`\n\nRead at build time, so it must be an object literal — not computed, not\nimported from elsewhere.\n\n**Icons need no listing.** Put them in \\`src/app/\\` and the build finds them:\n\n favicon.ico served at /favicon.ico\n icon-192.png <link rel=\"icon\">, and the manifest's icons\n icon-512.png\n apple-icon.png <link rel=\"apple-touch-icon\">\n opengraph-image.png <meta property=\"og:image\">\n twitter-image.png <meta name=\"twitter:image\">\n\nSizes are read from the filename. The build says whether it worked:\n\n [rsc-kit] manifest: Orders is installable\n [rsc-kit] manifest: no icons, so no browser will offer to install this.\n\nThere is no layout to edit — React hoists the tags into <head>.\n\nPair it with \\`offline: true\\`. They are separate options because they are\nseparate decisions.\n\n**Push and background sync** need listeners the generated worker does not have,\nso it imports yours from \\`src/app/sw.js\\` — plain javascript, evaluated by the\nbrowser with no build step in front of it:\n\n\\`\\`\\`js\nself.addEventListener('push', (event) => {\n const payload = event.data ? event.data.json() : {}\n\n event.waitUntil(self.registration.showNotification(payload.title, { body: payload.body }))\n})\n\\`\\`\\`\n\nThe rest is the web api and \\`web-push\\`, not this package: VAPID keys, a\nsubscribe call behind a button, the subscription stored by a server action\nagainst a USER rather than a session, and a sender that deletes an endpoint on\n404 or 410 rather than retrying a dead one forever.\n\nFor background sync, make the endpoint idempotent. The browser decides when a\nsync runs and may run it more than once — a request that reached the server\nwhose response did not arrive is retried, and if that posts a message twice the\nperson sent it twice.`,\n },\n {\n topic: 'no-javascript',\n summary: 'The default is none; \"use client\" is how a page asks for it',\n body: `There is NO JavaScript on a stored page until something on it needs some.\nA route that freezes whole, renders none of the app's client components, and\nhas no server action in its tree ships nothing - no React, no router. The\nbuild says so:\n\n ○ /about no js\n no client components, so ships no javascript; stylesheet inlined\n\n\"use client\" IS the opt-in. Put a client component on the page - a counter, a\n<Link>, an update prompt - and it has the runtime, because there is now\nsomething for the runtime to do. There is NO switch in either direction:\nnothing can need the runtime without a client component or an action in the\ntree, and a page that must stay this way is an assertion on build-report.json\n(its clientJs is null), not a setting.\n\nThe check reads the rendered tree, so a <Link> in a shared layout counts -\npages under a layout with a nav keep the runtime; a route group with its own\nplain layout drops it. Navigation into such a page from a Link elsewhere\nstill works: its flight payload is written with the wrappers.\n\nA page without the runtime also gets its stylesheet inlined when small\n(rscKit({ inlineStylesheets }) to change), and still registers the service\nworker with one inlined line.\n\nDo NOT restructure an app to chase this, and do NOT look for export const\nclientJs - it does not exist. The size column says what each route costs; a\npage that is 82 kB because of one <Link> is fine.`,\n },\n {\n topic: 'api-routes',\n summary: 'HTTP endpoints beside the pages',\n body: `\\`src/app/**/route.ts\\`, one export per method:\n\n\\`\\`\\`ts title=\"src/app/api/posts/[id]/route.ts\"\nexport const params = z.object({ id: z.coerce.number().int() })\nexport const body = z.object({ title: z.string().min(1) })\n\nexport async function GET(request: Request, { params }) {\n const { id } = await params\n\n return Response.json(await findPost(id))\n}\n\nexport async function POST(request: Request, { params, body }) {\n const { title } = await body\n\n return Response.json(await createPost(title), { status: 201 })\n}\n\\`\\`\\`\n\nA real \\`Request\\` in, a real \\`Response\\` out. \\`params\\`, \\`searchParams\\` and\n\\`body\\` are awaited, the same way a page's props are.\n\nFetch one through \\`apiUrl\\` and the path is checked against the routes the\nbuild found:\n\n import { apiUrl } from '@rsc-kit/core/routes'\n await fetch(apiUrl('/api/posts/' + id))\n\nPages and api routes are separate unions: Link refuses an api url, apiUrl\nrefuses a page. It checks the PATH, not the response type - for types across\nthe boundary use a server action or a query, where the return type is the\nfunction's because it is the same function.\n\nThey run their directory's \\`middleware.ts\\`, so an endpoint under a guarded\npath is guarded.\n\nA \\`GET\\` that reads nothing from the request is answered from disk. Awaiting\n\\`searchParams\\` says the answer depends on the query; never touching it means\nthe stored answer is served for any query at all.\n\nExporting a \\`body\\` schema consumes the stream, so \\`request.json()\\` inside the\nhandler will find it already read. Use the parsed value.\n\nA GET that reads nothing is stored - and two more things keep one per request:\na Set-Cookie on its Response (an answer for one visitor; classified dynamic,\nwhatever it read), and a body it froze from Date.now() or Math.random() is\nstored WITH a warning on the route's line. Read the request (await\nconnection()) to run it on demand.`,\n },\n {\n topic: 'authorization',\n summary: 'Guarding pages, actions, api routes and queries',\n body: `Each entry point defends itself. There is no single place that covers all of\nthem, and believing otherwise is how a hole is left.\n\n**A page or an api route**: \\`middleware.ts\\` in its directory guards everything\nat or below it.\n\n\\`\\`\\`ts title=\"src/app/admin/middleware.ts\"\nimport { redirect } from '@rsc-kit/core/redirect'\n\nexport default async function guard() {\n if (!(await currentUser())) redirect('/login')\n}\n\\`\\`\\`\n\n**An action or a query**: middleware does NOT run — they render no route. Build\nthem from an action client so the check cannot be forgotten. See the\n\\`action-client\\` topic.\n\n**Authorise on identity, not arguments.** \\`deletePost(id)\\` that trusts the id\nis the whole of an IDOR: the caller chooses the id, so check the row belongs to\n\\`ctx.user\\`.\n\nA guarded page can still be frozen at build time — the guard is a serving\ndecision, not a build one. Its response is marked private so no cache keeps it.`,\n },\n {\n topic: 'dynamic',\n summary: 'Why a page is not static, and how to choose',\n body: `A page is stored at build time unless it reads the request. Reading it is what\nopts out, and the accessors are async:\n\n\\`\\`\\`ts\nimport { cookies, headers, searchParams, connection } from '@rsc-kit/core/request'\n\nconst theme = (await cookies()).get('theme')\nawait connection() // \"render this per visitor\", said deliberately\n\\`\\`\\`\n\nA page's \\`params\\` and \\`searchParams\\` props are promises for the same reason.\n\nThe build says which call did it, per route:\n\n ◐ /locale 85 kB\n cookies(), headers() stream per request; the rest is stored\n\nThat is usually correct — a page whose content depends on who is asking cannot\nbe one stored file. Change it only when the read was accidental.\n\nFor a parameterised route, \\`generateStaticParams\\` turns one shell into a page\nper url:\n\n\\`\\`\\`ts\nexport async function generateStaticParams() {\n return (await db.posts.all()).map((p) => ({ slug: p.slug }))\n}\n\\`\\`\\`\n\nA value that must differ per visitor but needs no server — a clock,\nlocalStorage, a map — belongs in the browser only:\n\n\\`\\`\\`tsx\n'use client'\nimport { browser } from 'react-dom'\n\nfunction Clock() {\n use(browser('the time is the visitor\\\\'s, not the build machine\\\\'s'))\n}\n\\`\\`\\`\n\nIt needs a Suspense boundary, and the page stays frozen.\n\nThere is NO app-wide switch to turn prerendering off (no rscKit({ prerender })\nand no export const dynamic). A page that must render per request says\nawait connection() in the page; the build names any page it could not render\nat build time, and that is the page to mark. \"All of it per request\" is\nawait connection() in the root layout with a root loading.tsx - every page a\nstored fallback with the rest streamed, five times slower on a page that could\nhave been stored (bench/ in the repo). Do not reach for it.`,\n },\n {\n topic: 'metadata',\n summary: 'Titles, share cards, and the one setting production needs',\n body: `\\`\\`\\`tsx\nexport const metadata: Metadata = {\n title: 'Orders',\n openGraph: { title: 'Orders', description: '…', images: '/cover.png' },\n}\n\\`\\`\\`\n\nA layout takes a title TEMPLATE - { template: '%s · Site', default: 'Site' } -\nand layouts merge outward-in, so site-wide values go on the root layout once.\n\n**Set metadataBase on the root layout. It is not optional in production.**\n\n\\`\\`\\`tsx\nmetadataBase: new URL('https://example.com')\n\\`\\`\\`\n\nA share-card scraper needs an ABSOLUTE image url and Facebook, Slack and\nLinkedIn refuse a relative one silently - the link unfurls with no image and\nnothing says why. metadataBase makes every relative url, image and icon\nabsolute. Same name as Next, so a port carries it across.\n\nUse the structured objects, not the flat 'og:title' spellings: openGraph and\ntwitter are typed, an image can be { url, width, height, alt }, and it is the\nshape a Next app already has. og: renders as property=, twitter: as name= -\nwhat each scraper reads.\n\nAn opengraph-image.png in app/ is found by name and needs no listing; it still\nneeds metadataBase to go out absolute.`,\n },\n {\n topic: 'fonts',\n summary: 'Self-hosted fonts from npm, and porting next/font',\n body: `There is no font loader. Install the font from Fontsource, import its\ncss, name it in a variable:\n\n\\`\\`\\`css\n@import '@fontsource-variable/fraunces/full.css';\n@import '@fontsource-variable/geist';\n\n:root {\n --font-display: 'Fraunces Variable', ui-serif, Georgia, serif;\n --font-sans: 'Geist Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,\n 'Helvetica Neue', Arial, sans-serif,\n 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';\n}\n\\`\\`\\`\n\nPut the font in FRONT of a full stack, not in place of one. A bare\n'Geist Variable', sans-serif drops the emoji fonts - Geist has no emoji glyphs,\nand with nothing named after it some systems draw a box - and drops the\nmetrics-matched fallback that makes the swap moment smaller. Those are\nTailwind's own defaults; shadcn's generated line loses both.\n\nVite hashes the woff2 files and serves them with the other assets. Nothing is\nfetched from Google at runtime and nothing is downloaded at build - the files\nare in node_modules.\n\nPorting next/font: every option was something Fontsource already did.\nsubsets -> every subset ships behind a unicode-range and the browser fetches\nonly what the page uses. style: ['italic'] -> full-italic.css. axes -> full.css\nhas every axis; standard.css is weight only. display: 'swap' -> already in\nevery rule. className={font.variable} -> nothing, the variable is on :root.\n\nThe one line next/font added that you add yourself is the preload:\n\n\\`\\`\\`tsx\nimport fraunces from '@fontsource-variable/fraunces/files/fraunces-latin-full-normal.woff2?url'\n<link rel=\"preload\" href={fraunces} as=\"font\" type=\"font/woff2\" crossOrigin=\"anonymous\" />\n\\`\\`\\`\n\n?url is Vite's and gives the hashed path. Preload the one file the first paint\nneeds; preloading all of them defeats the subsetting.\n\nDo NOT reach for next/font, @next/font or a Google Fonts link tag.\n\nGETTING TO 100 ON A PHONE (do it last, once the design is settled). Fontsource's\nstylesheet declares every subset of every weight with font-display: swap; a\nthrottled phone audit sees the largest text repaint when the font lands, and\nscores 99. Three moves:\n1. Own the @font-face rules, latin only: import the files with ?url, declare\n them yourself with the latin unicode-range.\n2. font-display: optional for the body faces - the text paints once, in the\n fallback on a cold slow load, in the web font when cached. Keep swap for the\n headline face, which is preloaded and arrives with the document.\n3. One weight per family; variable only where the axes are used. Declare the\n range you ask for (font-weight: 400 500) so nothing requests a missing file.\nPut the preloads before the <style> with the faces. Measured on rsc-kit.dev:\nSpeed Index 1.7s to 0.9s, 99 to a steady 100, fonts 114 kB to 74 kB.\nFull guide: read_guide({ slug: 'fonts' }).`,\n },\n {\n topic: 'from-next',\n summary: 'Porting a Next.js app - what carries over, what to rename, what is different on purpose',\n body: `The app/ conventions are the same: layout, page, loading, error, not-found,\nroute.ts, [slug], [...path], (group), @slot, (.)intercept. \"use client\" and\n\"use server\" are React's. Copy src/app first, fix imports second.\n\nIMPORTS\n next/link -> @rsc-kit/core/Link (href typed; search typed by the page's schema)\n useRouter().push / .replace -> visit(url) / visit(url, { replace: true }) from @rsc-kit/core/router\n useRouter().refresh() -> refresh() from @rsc-kit/core/router, or revalidate() in the action\n usePathname / useSearchParams-> @rsc-kit/core/usePathname, @rsc-kit/core/useSearchParams (nuqs: @rsc-kit/core/nuqs)\n useParams() -> the page's params prop, passed down\n cookies(), headers() -> same names, from @rsc-kit/core/request\n redirect() / notFound() -> @rsc-kit/core/redirect / @rsc-kit/core/not-found\n revalidatePath/Tag -> revalidate('tag') on a section() - targeted, rides back with the action\n Metadata -> @rsc-kit/core/metadata (metadataBase, openGraph, twitter, icons as-is)\n app/robots.ts, app/sitemap.ts -> the same files and shapes; app/llms.ts beside them (how_to seo-files)\n next/font -> Fontsource (how_to fonts)\n next/image -> unpic or vite-imagetools (how_to images)\n next/script -> a <script> tag (how_to scripts)\n NEXT_PUBLIC_* -> VITE_* via import.meta.env; server vars stay process.env\n next-safe-action -> createActionClient() (how_to action-client); returnValidationErrors -> return fieldErrors({...})\n cache from 'react' -> cache from @rsc-kit/core/cache: React's dedupes only inside a render; this one\n spans the request (guards, actions, api routes). The build names files still on React's\n @react-email/render, renderToString in an action -> the same call, in a module that starts with \"use ssr\"\n (how_to emails). Next gets away with it only for externalised packages; here it is explicit\n\nDIFFERENT ON PURPOSE\n- No export const dynamic / revalidate = 60. A page is frozen unless it READS\n the request; await connection() is the explicit mark. No time-based ISR.\n- middleware.ts is per directory, on the server, full API; not one edge file.\n It does not run for actions - the check goes in the action.\n- Actions return failures ({ validationErrors }, { serverError }), not throw.\n- No image optimizer, no opengraph-image.tsx - put opengraph-image.png in src/app.\n- Tests need no browser: createTestApp() is the deployed handler.\n- A component library (base-ui, Radix) imports as it did, from server\n components too. A shadcn-style components/ui/ folder keeps \"use client\" at\n the top of each file, as shipped; without it the server evaluates the\n library's internals for nothing.\n\nORDER: scaffold -> copy src/app -> fix imports -> build (it typechecks first,\nso a Link to a route that does not exist fails here) and READ the output: a\nroute that is not ○ names what streams and from which component (a cookies()\nin a layout reaches every page; the build says so) -> decide each action the\nbuild lists as running no middleware -> check.\n\nFull guide: read_guide({ slug: 'coming-from-next' }).`,\n },\n {\n topic: 'emails',\n summary: 'Render React to HTML on the server - an email, a PDF, a feed - from an action or a route, with \"use ssr\"',\n body: `@react-email/render, renderToString, anything on react-dom/server, called from\na server action or a route, fails: \"react-dom/server is not supported in React\nServer Components\". React means it: where server components render, react is\nthe server-only build - the renderer needs the client build's internals, and\nthe components it would render import that same react (no useState, no\nuseContext). No alias fixes it. The rendering has to run in the ssr\nenvironment, the one that turns pages into HTML for the browser.\n\nPut the rendering - the template AND the call that renders it - in a module\nthat starts with \"use ssr\". Everything else imports it normally:\n\n\\`\\`\\`tsx\n// src/lib/email/render.tsx\n\"use ssr\";\nimport { render } from '@react-email/render'\nimport { OtpEmail } from './otp-email'\n\nexport async function renderOtpEmail(code: string) {\n const email = <OtpEmail code={code} />\n const [html, text] = await Promise.all([render(email), render(email, { plainText: true })])\n return { html, text }\n}\n\\`\\`\\`\n\n\\`\\`\\`ts\n// src/lib/email/send-otp.ts - a plain server module, called from the action\nimport { renderOtpEmail } from './render'\nexport async function sendOtpEmail(to: string, code: string) {\n const { html, text } = await renderOtpEmail(code)\n await transporter.sendMail({ to, subject: 'Your code', html, text })\n}\n\\`\\`\\`\n\nWhere server components render, the build replaces the module with async\nproxies of its exports that call across - what \"use client\" does for a\ncomponent, in the other direction. Same process; dev and build; nothing to\nconfigure.\n\nRULES\n- Exports are async functions. The call crosses environments, so the answer is\n a promise. A sync function, a value, a class, export { } or export * is\n refused at build with its name. Types are fine.\n- Pass DATA across, not elements: renderOtpEmail(code), never\n render(<OtpEmail/>) from the caller. An element built on the calling side\n carries components from that side's react, and they render with no hooks.\n- The module's imports are the ssr side's: @react-email/components,\n react-dom/server, a PDF or Markdown renderer. Keep the module to rendering;\n the database call belongs on the calling side.\n\nImported react-dom/server directly (through a library, usually)? It now throws\nthe fix in its message, naming the app file that pulled it in, and the build\nwarns once with the same. Do NOT alias react-dom/server, externalise react, or\nmove the action out of the app - the directive is the whole fix.\n\nFull guide: read_guide({ slug: 'emails' }).`,\n },\n {\n topic: 'seo-files',\n summary: 'robots.txt, sitemap.xml and llms.txt from a file beside the root layout - the shapes Next uses, stored at build when they can be',\n body: `Files beside the root layout, named for what they answer:\n src/app/robots.ts -> /robots.txt default export returns MetadataRoute.Robots\n src/app/sitemap.ts -> /sitemap.xml default export returns MetadataRoute.Sitemap (an array)\n src/app/llms.ts -> /llms.txt default export returns MetadataRoute.Llms\n src/app/llms-full.ts -> /llms-full.txt default export returns a string\nThe types: import type { MetadataRoute } from '@rsc-kit/core/metadata'. The\nsame names and shapes as Next's app/robots.ts and app/sitemap.ts; copy them.\n\n\\`\\`\\`ts\n// src/app/robots.ts\nexport default function robots(): MetadataRoute.Robots {\n return { rules: [{ userAgent: '*', allow: '/', disallow: ['/api/'] }], sitemap: '/sitemap.xml' }\n}\n// src/app/sitemap.ts\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const posts = await db.post.findMany()\n return [{ url: '/', priority: 1 }, ...posts.map((p) => ({ url: \\`/blog/\\${p.slug}\\`, lastModified: p.updatedAt }))]\n}\n// src/app/llms.ts\nexport default function llms(): MetadataRoute.Llms {\n return { title: 'Acme', summary: 'What it is.', sections: [{ title: 'Pages', links: [{ title: 'Pricing', url: '/pricing' }] }] }\n}\n\\`\\`\\`\n\nA relative url is made absolute with the root layout's metadataBase; without\none it is a build error. Any of them may return a string, served as written.\n\nEach becomes an api route, so: one that reads nothing per request (the\ndatabase is fine) is stored at build and served from the file; one that reads\ncookies() or awaits connection() runs per request. No middleware runs for\nthem - a root guard must not 401 the crawler. The url is typed\n(route('/sitemap.xml')).\n\nA file as written beside the root layout is served at the root as it is:\nrobots.txt, sitemap.xml, sitemap-*.xml, llms.txt, llms-full.txt, humans.txt,\nsecurity.txt, ads.txt. A file and a function for the same url is a build\nerror. Do NOT put these in public/ and do NOT write a route.ts for them.\n\nFull guide: read_guide({ slug: 'seo-files' }).`,\n },\n {\n topic: 'images',\n summary: 'Responsive images with no optimizer - unpic for a CDN, imagetools for files in the repo',\n body: `There is NO image component and NO image server. Do not add next/image or\nwrite an optimizer route. next/image is a srcset-writing component plus a\nresize-on-request process; the first is a library, the second belongs to the\nCDN.\n\nAn image on a CDN (Cloudinary, imgix, Cloudflare Images, Bunny, Vercel,\nNetlify, ...): @unpic/react. Plain component, works in a server component,\nships no javascript, detects the CDN from the url:\n\n\\`\\`\\`tsx\nimport { Image } from '@unpic/react'\n\n<Image src=\"https://res.cloudinary.com/demo/image/upload/sample.jpg\" layout=\"constrained\" width={800} height={600} alt=\"...\" />\n\\`\\`\\`\n\nA file in the repo, a handful of them: vite-imagetools, resized ONCE at build\ntime. Add imagetools() to the vite plugins, then:\n\n\\`\\`\\`tsx\nimport hero from '../hero.png?w=400;800;1200&format=webp&as=srcset'\nimport heroSrc from '../hero.png?w=800&format=webp'\n\n<img srcSet={hero} src={heroSrc} sizes=\"(min-width: 800px) 800px, 100vw\" width={800} height={600} alt=\"...\" />\n\\`\\`\\`\n\nDeclare the query tails in src/images.d.ts, or the build's typecheck stops on\nthe imports (a pattern may hold ONE *, so '*?*' matches nothing):\n\n declare module '*&as=srcset' { const srcset: string; export default srcset }\n declare module '*&format=webp' { const url: string; export default url }\n\nHundreds of files in the repo: that is a CDN's job; move them and use unpic.\nAn icon or a logo: a plain <img>, or inline the svg.\n\nFull guide: read_guide({ slug: 'images' }).`,\n },\n {\n topic: 'scripts',\n summary: 'Third-party scripts - analytics, tag managers - without a Script component',\n body: `Write the script tag. React 19 does what Next's Script component existed for.\n\nAn external script with async, rendered from a server component, is HOISTED\ninto head and DEDUPLICATED by React - the same src in three components is one\ntag. That is afterInteractive:\n\n\\`\\`\\`tsx\n<script async src=\"https://www.clarity.ms/tag/abc123\" />\n\\`\\`\\`\n\nAn inline snippet renders where it is written and runs during parse, before\nhydration - the earlier moment, which is what an analytics snippet wants:\n\n\\`\\`\\`tsx\n<script id=\"ms-clarity\" dangerouslySetInnerHTML={{ __html: '...' }} />\n\\`\\`\\`\n\nPut site-wide scripts in the ROOT LAYOUT, which renders once and is kept\nacross navigations.\n\nThere is no Script component to import. The only case needing one - a script\nthat touches DOM React rendered, or an onLoad callback - is a client component\nwith useEffect that creates the tag. Ten lines of the user's own.`,\n },\n {\n topic: 'testing',\n summary: 'Unit-testing actions, queries and routes; the whole app without a port',\n body: `Almost everything is a function. Any test runner works.\n\n**Actions, queries, api routes: import and call.** \"use server\" is a string in\na test file, so the function is importable. An action built on the action\nclient runs its whole middleware chain when called and RETURNS its failures:\n\n\\`\\`\\`ts\nconst result = await createPost({ title: '' })\nexpect(result.validationErrors).toEqual({ title: ['too short'] })\n\\`\\`\\`\n\nAn api route takes a Request and the context the engine gives it - params is a\nPROMISE:\n\n\\`\\`\\`ts\nconst res = await GET(new Request('https://app.test/api/x'), { params: Promise.resolve({ id: '1' }) })\n\\`\\`\\`\n\n**Anything reading cookies() or headers():** open the request scope yourself.\n\n\\`\\`\\`ts\nimport { withRequest } from '@rsc-kit/core/request'\nawait withRequest(new Request('https://app.test/', { headers: { Cookie: 'session=abc' } }), currentUser)\n\\`\\`\\`\n\n**The whole app as Request -> Response, no port:**\n\n\\`\\`\\`ts\nimport { createTestApp } from '@rsc-kit/core/testing'\nconst app = await createTestApp()\nconst res = await app.fetch('/admin', { redirect: 'manual' }) // real router, real middleware\n\\`\\`\\`\n\nIt builds when the source is newer than the last build - the first run pays,\nthe rest do not. This is where a guard that never ran or a 404 that came back\n200 shows up.\n\n**What still needs a browser:** a server action called OVER THE WIRE (the id is\nReact's and private), hydration, navigation. Playwright against vite preview.\nThat limit is narrower than Next's: the action's logic is a unit test here.\n\n**What to write when you add something.** Before running check, not after:\n\n- A guarded route (middleware.ts, or a page reading the session): a stranger\n is turned away, and someone signed in gets 200.\n \\`\\`\\`ts\n expect((await app.fetch('/admin', { redirect: 'manual' })).status).toBe(302)\n expect((await app.fetch('/admin', { headers: { Cookie: 'session=ada' } })).status).toBe(200)\n \\`\\`\\`\n- An action: its refusal, by calling it. Bad input answers validationErrors;\n a stranger answers serverError (or throws ServerAuthenticationError if you\n built it without the client).\n \\`\\`\\`ts\n expect((await createPost({ title: '' })).validationErrors).toBeDefined()\n \\`\\`\\`\n- An action that takes an id: someone else's id is refused. This is the IDOR\n test and the one most often missing.\n- A query: the shape of its answer, and what a filter changes.\n- An api route: status, content-type, and the 4xx it answers to a bad body.\n- A page that should stay static: assert on the build report - no test, a CI\n check that build-report.json still says frozen for it.\n\nDo NOT start a dev server, spawn a process or pick a port in a test. Do NOT\nadd a second runner. The one in tests/ goes through the real build already.`,\n },\n]\n\n/** Every topic, with one line each — what a caller reads before choosing. */\nexport function listTopics(): string {\n return [\n 'Topics. Ask for one with how_to({ topic }).',\n '',\n ...RECIPES.map((r) => `${r.topic.padEnd(16)} ${r.summary}`),\n ].join('\\n')\n}\n\n/** One recipe, or the list plus a nudge when the topic is not one. */\nexport function howTo(topic: string): string {\n const wanted = topic.trim().toLowerCase().replace(/[\\s_]+/g, '-')\n const found =\n RECIPES.find((r) => r.topic === wanted) ??\n // A near miss is common and worth answering rather than refusing: someone\n // asks for \"form\" or \"queries\" and means the obvious thing.\n RECIPES.find((r) => r.topic.startsWith(wanted) || wanted.startsWith(r.topic)) ??\n RECIPES.find((r) => r.summary.toLowerCase().includes(wanted))\n\n if (!found) return `No topic \"${topic}\".\\n\\n${listTopics()}`\n\n return `# ${found.topic} — ${found.summary}\\n\\n${found.body}`\n}\n\n/** For tests, so a recipe cannot be added without being reachable. */\nexport const TOPICS = RECIPES.map((r) => r.topic)\n"]}
|
package/dist/report.d.ts
CHANGED
|
@@ -31,6 +31,18 @@ export interface BuildReport {
|
|
|
31
31
|
apis: ReportedApiRoute[];
|
|
32
32
|
/** Absent from reports written before actions were audited. */
|
|
33
33
|
actions?: ReportedAction[];
|
|
34
|
+
/**
|
|
35
|
+
* Server files importing `cache` from React, relative to the source dir.
|
|
36
|
+
* React's memoises only inside a component render; in a guard, an action
|
|
37
|
+
* or an api route it calls straight through. Absent from older reports.
|
|
38
|
+
*/
|
|
39
|
+
reactCache?: string[];
|
|
40
|
+
/** Server files importing a client library: the packages, and who imports the file. Absent from older reports. */
|
|
41
|
+
clientImports?: {
|
|
42
|
+
file: string;
|
|
43
|
+
packages: string[];
|
|
44
|
+
from: string | null;
|
|
45
|
+
}[];
|
|
34
46
|
totals: {
|
|
35
47
|
static: number;
|
|
36
48
|
partial: number;
|
package/dist/report.js
CHANGED
|
@@ -8,26 +8,26 @@
|
|
|
8
8
|
// The consequence to be honest about: an answer is only as fresh as the last
|
|
9
9
|
// build. So every answer says when it was built, and a missing report says
|
|
10
10
|
// "run a build" rather than "there are no routes".
|
|
11
|
-
import { existsSync, readFileSync, statSync } from
|
|
12
|
-
import { join, resolve } from
|
|
11
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
13
|
/** Where a build leaves its report, in the order worth looking. */
|
|
14
|
-
const LIKELY = [
|
|
14
|
+
const LIKELY = [".rsc", "build", "dist", ".output"];
|
|
15
15
|
export class NoReport extends Error {
|
|
16
16
|
constructor(root) {
|
|
17
17
|
super(`No build report under ${root}. This server answers from what the last build ` +
|
|
18
|
-
|
|
19
|
-
this.name =
|
|
18
|
+
"decided, so there has to have been one — run the build and ask again.");
|
|
19
|
+
this.name = "NoReport";
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
/** The report, and how old it is. */
|
|
23
23
|
export function loadReport(root) {
|
|
24
24
|
const base = resolve(root);
|
|
25
25
|
for (const dir of LIKELY) {
|
|
26
|
-
const file = join(base, dir,
|
|
26
|
+
const file = join(base, dir, "build-report.json");
|
|
27
27
|
if (!existsSync(file))
|
|
28
28
|
continue;
|
|
29
29
|
return {
|
|
30
|
-
report: JSON.parse(readFileSync(file,
|
|
30
|
+
report: JSON.parse(readFileSync(file, "utf-8")),
|
|
31
31
|
// The file's own mtime rather than a timestamp inside it: a stamp written
|
|
32
32
|
// into the file changes the file on every build even when nothing else
|
|
33
33
|
// did, which defeats every cache keyed on its contents.
|
|
@@ -39,16 +39,18 @@ export function loadReport(root) {
|
|
|
39
39
|
}
|
|
40
40
|
/** What each classification means, in one line, for an answer that has to stand alone. */
|
|
41
41
|
export const MEANING = {
|
|
42
|
-
frozen:
|
|
43
|
-
shell:
|
|
44
|
-
blocked:
|
|
45
|
-
dynamic:
|
|
46
|
-
error:
|
|
42
|
+
frozen: "stored whole at build time and served as a file",
|
|
43
|
+
shell: "a stored shell, with the rest rendered per request",
|
|
44
|
+
blocked: "REFUSED — nothing could paint before it read the request, so the build did not finish",
|
|
45
|
+
dynamic: "answered per request",
|
|
46
|
+
error: "failed to render",
|
|
47
47
|
};
|
|
48
48
|
/** A route by url, tolerating a trailing slash either way. */
|
|
49
49
|
export function routeFor(report, url) {
|
|
50
|
-
const wanted = (url.split(
|
|
51
|
-
const matches = (candidate) => (candidate.replace(/\/+$/,
|
|
52
|
-
return (report.routes.find((r) => matches(r.url)) ??
|
|
50
|
+
const wanted = (url.split("?")[0].replace(/\/+$/, "") || "/").toLowerCase();
|
|
51
|
+
const matches = (candidate) => (candidate.replace(/\/+$/, "") || "/").toLowerCase() === wanted;
|
|
52
|
+
return (report.routes.find((r) => matches(r.url)) ??
|
|
53
|
+
report.apis.find((a) => matches(a.url)) ??
|
|
54
|
+
null);
|
|
53
55
|
}
|
|
54
56
|
//# sourceMappingURL=report.js.map
|
package/dist/report.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,qCAAqC;AACrC,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,6EAA6E;AAC7E,yEAAyE;AACzE,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,mDAAmD;AAEnD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,qCAAqC;AACrC,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,6EAA6E;AAC7E,yEAAyE;AACzE,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,mDAAmD;AAEnD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiD1C,mEAAmE;AACnE,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;AAEpD,MAAM,OAAO,QAAS,SAAQ,KAAK;IACjC,YAAY,IAAY;QACtB,KAAK,CACH,yBAAyB,IAAI,iDAAiD;YAC5E,uEAAuE,CAC1E,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AAED,qCAAqC;AACrC,MAAM,UAAU,UAAU,CAAC,IAAY;IAKrC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,mBAAmB,CAAC,CAAC;QAElD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAS;QAEhC,OAAO;YACL,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAgB;YAC9D,0EAA0E;YAC1E,uEAAuE;YACvE,wDAAwD;YACxD,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK;YAC7B,IAAI,EAAE,IAAI;SACX,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,0FAA0F;AAC1F,MAAM,CAAC,MAAM,OAAO,GAA2B;IAC7C,MAAM,EAAE,iDAAiD;IACzD,KAAK,EAAE,oDAAoD;IAC3D,OAAO,EACL,uFAAuF;IACzF,OAAO,EAAE,sBAAsB;IAC/B,KAAK,EAAE,kBAAkB;CAC1B,CAAC;AAEF,8DAA8D;AAC9D,MAAM,UAAU,QAAQ,CACtB,MAAmB,EACnB,GAAW;IAEX,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAC5E,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAE,EAAE,CACpC,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;IAElE,OAAO,CACL,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,CACL,CAAC;AACJ,CAAC","sourcesContent":["// Reading what the build wrote down.\n//\n// Everything this server answers comes from one file: `build-report.json`, in\n// the build's own output directory. Nothing here runs a build, imports the\n// app, or re-derives the route tree — the build already decided all of this,\n// and a second implementation would be a second thing that can be wrong.\n//\n// The consequence to be honest about: an answer is only as fresh as the last\n// build. So every answer says when it was built, and a missing report says\n// \"run a build\" rather than \"there are no routes\".\n\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\n\nexport interface ReportedRoute {\n url: string;\n component: string;\n type: string;\n reason: string | null;\n warning: string | null;\n /** How it was stored, when that is worth knowing - \"no client components, so ships no javascript\". */\n note?: string | null;\n clientJs: number | null;\n}\n\nexport interface ReportedApiRoute {\n url: string;\n name: string;\n type: string;\n reason: string | null;\n /** Stored, and froze a value that will not be the same tomorrow. Absent from older reports. */\n warning?: string | null;\n}\n\nexport interface ReportedAction {\n id: string;\n name: string;\n file: string;\n /** Built by createActionClient, so its middleware ran. */\n client: boolean;\n /** A read (GET) rather than a mutation. */\n query: boolean;\n}\n\nexport interface BuildReport {\n version: number;\n routes: ReportedRoute[];\n apis: ReportedApiRoute[];\n /** Absent from reports written before actions were audited. */\n actions?: ReportedAction[];\n /**\n * Server files importing `cache` from React, relative to the source dir.\n * React's memoises only inside a component render; in a guard, an action\n * or an api route it calls straight through. Absent from older reports.\n */\n reactCache?: string[];\n /** Server files importing a client library: the packages, and who imports the file. Absent from older reports. */\n clientImports?: { file: string; packages: string[]; from: string | null }[];\n totals: { static: number; partial: number; dynamic: number; failed: number };\n}\n\n/** Where a build leaves its report, in the order worth looking. */\nconst LIKELY = [\".rsc\", \"build\", \"dist\", \".output\"];\n\nexport class NoReport extends Error {\n constructor(root: string) {\n super(\n `No build report under ${root}. This server answers from what the last build ` +\n \"decided, so there has to have been one — run the build and ask again.\",\n );\n this.name = \"NoReport\";\n }\n}\n\n/** The report, and how old it is. */\nexport function loadReport(root: string): {\n report: BuildReport;\n builtAt: Date;\n from: string;\n} {\n const base = resolve(root);\n\n for (const dir of LIKELY) {\n const file = join(base, dir, \"build-report.json\");\n\n if (!existsSync(file)) continue;\n\n return {\n report: JSON.parse(readFileSync(file, \"utf-8\")) as BuildReport,\n // The file's own mtime rather than a timestamp inside it: a stamp written\n // into the file changes the file on every build even when nothing else\n // did, which defeats every cache keyed on its contents.\n builtAt: statSync(file).mtime,\n from: file,\n };\n }\n\n throw new NoReport(base);\n}\n\n/** What each classification means, in one line, for an answer that has to stand alone. */\nexport const MEANING: Record<string, string> = {\n frozen: \"stored whole at build time and served as a file\",\n shell: \"a stored shell, with the rest rendered per request\",\n blocked:\n \"REFUSED — nothing could paint before it read the request, so the build did not finish\",\n dynamic: \"answered per request\",\n error: \"failed to render\",\n};\n\n/** A route by url, tolerating a trailing slash either way. */\nexport function routeFor(\n report: BuildReport,\n url: string,\n): ReportedRoute | ReportedApiRoute | null {\n const wanted = (url.split(\"?\")[0].replace(/\\/+$/, \"\") || \"/\").toLowerCase();\n const matches = (candidate: string) =>\n (candidate.replace(/\\/+$/, \"\") || \"/\").toLowerCase() === wanted;\n\n return (\n report.routes.find((r) => matches(r.url)) ??\n report.apis.find((a) => matches(a.url)) ??\n null\n );\n}\n"]}
|
|
@@ -45,6 +45,9 @@ it, the server evaluates the library's internals for nothing on every render.
|
|
|
45
45
|
| `NEXT_PUBLIC_*` | `VITE_*`, read through `import.meta.env`; everything else stays `process.env` on the server |
|
|
46
46
|
| `next.config.js` | `vite.config.ts` — Tailwind, aliases and plugins are Vite's |
|
|
47
47
|
| `next-safe-action` | `createActionClient()` — same shape, [below](#actions) |
|
|
48
|
+
| `app/robots.ts`, `app/sitemap.ts` | the same files, the same shapes — [robots, sitemap and llms.txt](/guides/seo-files); `app/llms.ts` beside them |
|
|
49
|
+
| `@react-email/render` in a server action | the same call, in a module that starts with `"use ssr"` — [emails](/guides/emails). Next fails the same way where it renders server components; the directive is how this one moves it |
|
|
50
|
+
| `cache` from `react` | `cache` from `@rsc-kit/core/cache` — React's memoises only inside a component render; this one spans the request, so a guard, the layout and the action share one call. The build names server files still importing React's |
|
|
48
51
|
|
|
49
52
|
## Different on purpose
|
|
50
53
|
|
|
@@ -59,7 +62,7 @@ which, and why:
|
|
|
59
62
|
```
|
|
60
63
|
○ /about no js
|
|
61
64
|
◐ /orders 85 kB
|
|
62
|
-
|
|
65
|
+
cookies() in RootLayout streams per request; the rest is stored
|
|
63
66
|
```
|
|
64
67
|
|
|
65
68
|
`await connection()` is the one explicit mark, for a page that must render per
|
|
@@ -145,10 +148,12 @@ in Next; see [Testing](/guides/testing).
|
|
|
145
148
|
## The porting order that worked
|
|
146
149
|
|
|
147
150
|
1. `bun create rsc-kit@latest` and copy `src/app` over the scaffold's.
|
|
148
|
-
2. Fix imports from the table. `bun run typecheck` finds the rest
|
|
151
|
+
2. Fix imports from the table. `bun run typecheck` finds the rest — and the
|
|
152
|
+
build runs the same check first, as `next build` does, so a link to a
|
|
153
|
+
route that does not exist cannot ship.
|
|
149
154
|
3. `bun run build` and **read the output**: every route that is not `○` says
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
what streams and from which component. Most surprises are a `cookies()`
|
|
156
|
+
in a layout reaching every page — the build says so under the summary.
|
|
152
157
|
4. Actions not built from a client are listed. Decide for each.
|
|
153
158
|
5. `bun run check`. Then a browser, for the parts that are a browser's.
|
|
154
159
|
|
package/guides/emails.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Emails and other HTML
|
|
2
|
+
|
|
3
|
+
> Rendering React to HTML on the server — an email, a PDF, a feed — from a server action or a route, with "use ssr".
|
|
4
|
+
|
|
5
|
+
An email template is a React component, and `@react-email/render` (or
|
|
6
|
+
`renderToString` from `react-dom/server`) turns it into HTML. Call that from
|
|
7
|
+
a server action and this comes back:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Error: react-dom/server is not supported in React Server Components.
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
React means it. Where server components render, `react` is a server-only
|
|
14
|
+
build: the renderer needs the client build's internals, and the components it
|
|
15
|
+
would render import that same `react` — no `useState`, no `useContext`. Nothing
|
|
16
|
+
that renders React to HTML can run there, and no alias fixes that. It has to
|
|
17
|
+
run in the **ssr** environment, which every app here already has: the one that
|
|
18
|
+
turns your pages into HTML for the browser.
|
|
19
|
+
|
|
20
|
+
## `"use ssr"`
|
|
21
|
+
|
|
22
|
+
Put the rendering — the template and the call that renders it — in a module
|
|
23
|
+
that starts with the directive:
|
|
24
|
+
|
|
25
|
+
```tsx title="src/lib/email/render.tsx"
|
|
26
|
+
"use ssr";
|
|
27
|
+
|
|
28
|
+
import { render } from '@react-email/render';
|
|
29
|
+
import { OtpEmail } from './otp-email';
|
|
30
|
+
|
|
31
|
+
export async function renderOtpEmail(code: string) {
|
|
32
|
+
const email = <OtpEmail code={code} />;
|
|
33
|
+
|
|
34
|
+
const [html, text] = await Promise.all([
|
|
35
|
+
render(email),
|
|
36
|
+
render(email, { plainText: true }),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
return { html, text };
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Everything else imports it normally:
|
|
44
|
+
|
|
45
|
+
```ts title="src/lib/email/send-otp.ts"
|
|
46
|
+
import { renderOtpEmail } from './render';
|
|
47
|
+
|
|
48
|
+
export async function sendOtpEmail(to: string, code: string) {
|
|
49
|
+
const { html, text } = await renderOtpEmail(code);
|
|
50
|
+
|
|
51
|
+
await transporter.sendMail({ to, subject: 'Your code', html, text });
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The module runs in the ssr environment, in the same process. Where server
|
|
56
|
+
components render, the build replaces it with proxies of its exports that call
|
|
57
|
+
across — the same thing `"use client"` does for a component, in the other
|
|
58
|
+
direction. In development that is a call into the ssr module runner; in a
|
|
59
|
+
build the module is part of the ssr bundle and the proxies import it from
|
|
60
|
+
there. Nothing to configure.
|
|
61
|
+
|
|
62
|
+
## The rules
|
|
63
|
+
|
|
64
|
+
**Exports are async functions.** A call crosses environments, so the answer is
|
|
65
|
+
a promise. A function declared without `async` is refused at build with its
|
|
66
|
+
name; so is a value, a class, `export { … }` or `export *`. Types are fine.
|
|
67
|
+
|
|
68
|
+
**Pass data across, not elements.** The template is created inside the module,
|
|
69
|
+
from the arguments — `renderOtpEmail(code)`, not `render(<OtpEmail />)` from
|
|
70
|
+
the caller. An element built where server components render carries
|
|
71
|
+
components from that side's `react`, and they would render with no hooks.
|
|
72
|
+
|
|
73
|
+
**The module's imports belong to the ssr side.** `@react-email/components`,
|
|
74
|
+
`react-dom/server`, a PDF renderer, a Markdown library that uses React — all
|
|
75
|
+
resolved and bundled for the ssr environment. A database client works there
|
|
76
|
+
too, but belongs on the calling side; keep the module to rendering.
|
|
77
|
+
|
|
78
|
+
## If you import it directly anyway
|
|
79
|
+
|
|
80
|
+
`react-dom/server` imported where server components render — through a
|
|
81
|
+
library, usually — now fails with the fix in the message rather than React's
|
|
82
|
+
refusal:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
react-dom/server cannot run where server components render … Imported by
|
|
86
|
+
src/lib/nodemailer.ts. Put the rendering — the template and the call — in a
|
|
87
|
+
module that starts with "use ssr" …
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A build prints the same once, as a warning, naming the file.
|
package/guides/errors.md
CHANGED
|
@@ -55,7 +55,11 @@ Log the digest where you log the error, and the two line up.
|
|
|
55
55
|
|
|
56
56
|
## When nothing answers the url
|
|
57
57
|
|
|
58
|
-
`src/app/not-found.tsx` is rendered for any url no route matches
|
|
58
|
+
`src/app/not-found.tsx` is rendered for any url no route matches — and a
|
|
59
|
+
client-side navigation to one renders it in place, layout kept, url changed,
|
|
60
|
+
status still 404, the way Next.js does once the page is overridden. Without
|
|
61
|
+
the file, a navigation to a missing url is a full document load of a plain
|
|
62
|
+
"Not found":
|
|
59
63
|
|
|
60
64
|
```tsx title="src/app/not-found.tsx"
|
|
61
65
|
import Link from '@rsc-kit/core/Link'
|
package/guides/images.md
CHANGED
|
@@ -62,12 +62,18 @@ build what resizing costs, once per image per width, and nothing at request
|
|
|
62
62
|
time. It is opt-in for that reason: a hundred hero images at four widths is a
|
|
63
63
|
noticeable build, and most of them belong on a CDN.
|
|
64
64
|
|
|
65
|
-
Declare the
|
|
65
|
+
Declare the queries so TypeScript stops asking — and the build's typecheck
|
|
66
|
+
lets them through:
|
|
66
67
|
|
|
67
68
|
```ts title="src/images.d.ts"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// A pattern may hold one `*`, so each matches on the tail of the query.
|
|
70
|
+
declare module '*&as=srcset' {
|
|
71
|
+
const srcset: string;
|
|
72
|
+
export default srcset;
|
|
73
|
+
}
|
|
74
|
+
declare module '*&format=webp' {
|
|
75
|
+
const url: string;
|
|
76
|
+
export default url;
|
|
71
77
|
}
|
|
72
78
|
```
|
|
73
79
|
|
package/guides/index.json
CHANGED
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"title": "Serving shells from a CDN",
|
|
30
30
|
"description": "Putting build-time shells on the edge, and what rsc-kit does not do."
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
"slug": "emails",
|
|
34
|
+
"title": "Emails and other HTML",
|
|
35
|
+
"description": "Rendering React to HTML on the server — an email, a PDF, a feed — from a server action or a route, with \"use ssr\"."
|
|
36
|
+
},
|
|
32
37
|
{
|
|
33
38
|
"slug": "errors",
|
|
34
39
|
"title": "Errors and 404s",
|
|
@@ -144,6 +149,11 @@
|
|
|
144
149
|
"title": "Sections",
|
|
145
150
|
"description": "Refreshing one region of a page without re-rendering the rest."
|
|
146
151
|
},
|
|
152
|
+
{
|
|
153
|
+
"slug": "seo-files",
|
|
154
|
+
"title": "robots, sitemap and llms.txt",
|
|
155
|
+
"description": "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."
|
|
156
|
+
},
|
|
147
157
|
{
|
|
148
158
|
"slug": "server-actions",
|
|
149
159
|
"title": "Server actions",
|
package/guides/installation.md
CHANGED
|
@@ -43,10 +43,10 @@ needs, not ones this package pins for you.
|
|
|
43
43
|
## Configure Vite
|
|
44
44
|
|
|
45
45
|
```ts title="vite.config.ts"
|
|
46
|
-
import { defineConfig } from
|
|
47
|
-
import react from
|
|
48
|
-
import { nitro } from
|
|
49
|
-
import { rscKit } from
|
|
46
|
+
import { defineConfig } from "vite";
|
|
47
|
+
import react from "@vitejs/plugin-react";
|
|
48
|
+
import { nitro } from "nitro/vite";
|
|
49
|
+
import { rscKit } from "@rsc-kit/core/vite";
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* The full route tree, built the way every scaffolded app is built.
|
|
@@ -56,16 +56,15 @@ import { rscKit } from '@rsc-kit/core/vite'
|
|
|
56
56
|
*/
|
|
57
57
|
export default defineConfig({
|
|
58
58
|
plugins: [
|
|
59
|
-
nitro({ preset:
|
|
59
|
+
nitro({ preset: "bun", serveStatic: "inline" }),
|
|
60
60
|
rscKit({
|
|
61
|
-
sourceDir:
|
|
62
|
-
outDir:
|
|
63
|
-
viewTransitions: true,
|
|
61
|
+
sourceDir: "src",
|
|
62
|
+
outDir: "build",
|
|
64
63
|
offline: true,
|
|
65
64
|
}),
|
|
66
65
|
react(),
|
|
67
66
|
],
|
|
68
|
-
})
|
|
67
|
+
});
|
|
69
68
|
```
|
|
70
69
|
|
|
71
70
|
`sourceDir` is where your route tree lives. The defaults are plain Vite ones —
|
package/guides/mcp.md
CHANGED
|
@@ -59,7 +59,7 @@ The reason a page is dynamic is **recorded, not guessed**:
|
|
|
59
59
|
Rendered by app/locale/page.
|
|
60
60
|
Ships 85 kB of javascript, gzipped.
|
|
61
61
|
|
|
62
|
-
Why it is not stored whole:
|
|
62
|
+
Why it is not stored whole: cookies(), headers() stream per request; the rest is stored
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
That matters more than it sounds. Asked to "make this page faster", an agent
|
package/guides/metadata.md
CHANGED
|
@@ -117,7 +117,7 @@ one above it. A page that exports no title gets the `default`.
|
|
|
117
117
|
| `description` | `<meta name="description">` |
|
|
118
118
|
| `keywords` | `<meta name="keywords">`, joined with commas if you pass an array |
|
|
119
119
|
| `author` | `<meta name="author">` |
|
|
120
|
-
| `robots` | `<meta name="robots">` |
|
|
120
|
+
| `robots` | `<meta name="robots">` — a string, or the object Next takes: `{ index: false, follow: false }` is `noindex, nofollow`; `googleBot` is `<meta name="googlebot">` |
|
|
121
121
|
| `metadataBase` | nothing itself — it is what makes every relative url below absolute |
|
|
122
122
|
| `openGraph` | `<meta property="og:…">`, one per field; each image brings its size and alt |
|
|
123
123
|
| `twitter` | `<meta name="twitter:…">` |
|
package/guides/offline.md
CHANGED
|
@@ -74,6 +74,14 @@ own error page: no script runs, so nothing held in JavaScript is reachable.
|
|
|
74
74
|
|
|
75
75
|
A service worker is the only thing that survives that.
|
|
76
76
|
|
|
77
|
+
Nothing of it runs in development. A worker is registered per origin, and one
|
|
78
|
+
a production run left on `localhost:3000` would otherwise sit in front of the
|
|
79
|
+
dev server on the same port — a stored page is cache-first, so a refresh would
|
|
80
|
+
bring back yesterday's document and a `504 Outdated Optimize Dep` for each of
|
|
81
|
+
its modules. The dev server answers `/sw.js` with a worker that removes the old
|
|
82
|
+
one, its caches, and reloads the pages it held; the browser asks for that file
|
|
83
|
+
on every navigation, so it happens on the first load.
|
|
84
|
+
|
|
77
85
|
### What is cached, and when
|
|
78
86
|
|
|
79
87
|
| | |
|