@rsc-kit/mcp 0.19.0 → 0.20.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/recipes.js +18 -3
- package/dist/recipes.js.map +1 -1
- package/guides/authorization.md +20 -0
- package/guides/deployment.md +15 -0
- package/guides/installation.md +1 -1
- package/guides/quick-start.md +3 -1
- package/guides/redirects.md +7 -3
- package/guides/routing.md +4 -4
- package/guides/typed-routes.md +1 -2
- package/package.json +1 -1
package/dist/recipes.js
CHANGED
|
@@ -185,7 +185,7 @@ import Link from '@rsc-kit/core/Link'
|
|
|
185
185
|
\`\`\`
|
|
186
186
|
|
|
187
187
|
\`href\` is typed to the routes the build found, so a link to a page that no
|
|
188
|
-
longer exists stops compiling. Cast with \`as
|
|
188
|
+
longer exists stops compiling. Cast with \`as Route\` only when the destination
|
|
189
189
|
is genuinely computed.
|
|
190
190
|
|
|
191
191
|
To prefetch from code — a row about to be clicked, a wizard's next step:
|
|
@@ -410,7 +410,15 @@ so:
|
|
|
410
410
|
|
|
411
411
|
A boundary does NOT fix a frozen \`Date.now()\`. Prerendering renders straight
|
|
412
412
|
through a component that never awaits, so the value is captured exactly as
|
|
413
|
-
before. A boundary becomes a hole only when something inside it waits
|
|
413
|
+
before. A boundary becomes a hole only when something inside it waits.
|
|
414
|
+
|
|
415
|
+
redirect() and notFound() inside a boundary still work: the shell has gone
|
|
416
|
+
out, so the redirect travels in the row's error digest and the browser
|
|
417
|
+
performs it as a navigation, layouts kept. An error.tsx on the route never
|
|
418
|
+
sees it - a redirect is the page's answer, not a failure - and the same holds
|
|
419
|
+
for a component under its own <Suspense> and for a parallel route slot. Only
|
|
420
|
+
an authorization check should NOT be there: the layouts above already
|
|
421
|
+
rendered. See the redirects guide.`,
|
|
414
422
|
},
|
|
415
423
|
{
|
|
416
424
|
topic: 'offline',
|
|
@@ -623,6 +631,10 @@ export default async function guard() {
|
|
|
623
631
|
}
|
|
624
632
|
\`\`\`
|
|
625
633
|
|
|
634
|
+
Several checks in one directory: export default [signedIn, verified, admin] -
|
|
635
|
+
run in order, stopping at the first refusal; reuse a check by importing it
|
|
636
|
+
from one place. Directories still compose outermost first.
|
|
637
|
+
|
|
626
638
|
**An action or a query**: middleware does NOT run — they render no route. Build
|
|
627
639
|
them from an action client so the check cannot be forgotten. See the
|
|
628
640
|
\`action-client\` topic.
|
|
@@ -1285,7 +1297,10 @@ Single binary (Bun): bun build --compile .output/server/compile.mjs
|
|
|
1285
1297
|
--outfile dist/app (the scaffold's "compile" script). compile.mjs is written
|
|
1286
1298
|
by the build and embeds the frozen pages; with serveStatic: 'inline' in the
|
|
1287
1299
|
Nitro plugin the assets (and their .br/.gz) are inside too. Ship dist/app
|
|
1288
|
-
alone - a Dockerfile copies nothing else, not .output/public.
|
|
1300
|
+
alone - a Dockerfile copies nothing else, not .output/public. A production
|
|
1301
|
+
app ported from Next measured the binary image at 50.12 MiB against the
|
|
1302
|
+
Next image's 104.56 MiB, the docker build at 2m45s against 5m13s, and
|
|
1303
|
+
Lighthouse at 99 mobile / 100 desktop - nothing tuned for the numbers.
|
|
1289
1304
|
|
|
1290
1305
|
Nothing is sent raw: a built bun/node server gzips what it answers
|
|
1291
1306
|
(documents, streams flushed per chunk, payloads, stored pages, api routes)
|
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. Uncontrolled by default - no useState per field',\n body: `THE RULE: forms are UNCONTROLLED. Inputs keep their value in the DOM,\nan initial value is defaultValue, the action reads FormData. Do NOT write\nuseState + value/onChange per input, and do NOT reach for TanStack Form.\nControl ONE field only when the UI must react as the user types (a character\ncount, a live preview, a dependent select) - bind it with useField, which\nscopes the re-render to that field. Everything else stays uncontrolled.\n\nUse <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, another page, a refresh —\ncall fetchQuery. It is a plain async function that returns the typed answer;\nNO library is needed:\n\n\\`\\`\\`tsx\nconst [listings, setListings] = useState(initial) // the server-rendered value\nconst [pending, start] = useTransition()\nconst show = (kind) => start(async () => setListings(await fetchQuery(getListings, [kind])))\n\\`\\`\\`\nWorks in an onClick, onSubmit, useEffect - anywhere in the browser.\n\n**When you want CACHING** (stale-while-revalidate, dedupe, offline), hand the\nsame call to the library that holds the answer; this package ships none:\n\\`\\`\\`tsx\nuseQuery({ queryKey: ['posts', kind], queryFn: () => fetchQuery(getPosts, [kind]) })\nuseSWR(['posts', kind], () => fetchQuery(getPosts, [kind]))\n\\`\\`\\`\nfetchQuery sends the read as a GET and goes to the server every time, which\nis what a fetcher needs. It will never cache, dedupe or batch: a client\ncache is the library's job, and a batch would lose the per-read cache key a\nGET has. The ladder, most reads stopping on the first rung:\n 1. the value now: fetchQuery + setState\n 2. survive a reload / let a CDN serve it: query(fn, { cache: 'public', maxAge })\n - HTTP caching, no code in the page\n 3. staleness, background refresh, optimistic updates, shared across\n components: TanStack or SWR with fetchQuery as the fetcher\nDo not add a cache on top of it, and do not install TanStack for a single\nbutton that reads once.\n\nKeep the arrow: TanStack calls a bare \\`queryFn\\` with its own context, and a\nserver function serialises whatever it is handed.\n\nA value that keeps CHANGING while someone watches - polling, SSE, realtime -\nis how_to live-data, not this.`,\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\"\nimport type { RouteContext } from '@rsc-kit/core/route-schema'\n\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 }: RouteContext<typeof params>) {\n const { id } = await params\n\n return Response.json(await findPost(id))\n}\n\nexport async function POST(request: Request, { params, body }: RouteContext<typeof params, never, typeof body>) {\n const { title } = await body\n\n return Response.json(await createPost(title), { status: 201 })\n}\n\\`\\`\\`\n\nTYPE THE CONTEXT: RouteContext<'/api/items/[id]'> types params from the\nroute's segments (the pattern is checked against the routes the build found);\nRouteContext<typeof params> from a schema. Every field is a PROMISE - a sync\nparams.id is a compile error, never a route that 404s. Do NOT hand-write\n{ params: { id: string } }.\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\nA route.ts is a Route too (type Route from '@rsc-kit/core/routes' covers pages\nAND route.ts files, as Next's does): Link, visit and redirect accept it. The\nclient treats a link to a route as an anchor - never prefetched, a full\nnavigation, not a payload fetch. ApiRoute is the narrower union for apiUrl:\napiUrl refuses a page, because fetching one gets html. It checks the PATH, not\nthe response type - for types across the boundary use a server action or a\nquery, where the return type is the function'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. NEVER read the query with\nnew URL(request.url).searchParams (the Next way): the build cannot see that\nread, and reading request.url at all makes the route dynamic (the table says\n\"reads the request - url\"). A webhook verification handshake (hub.mode,\nhub.challenge) reads the awaited searchParams. A GET that answers 4xx/5xx to\nthe build is never stored either (the table says \"answered 403 to the build\").\n\nredirect() thrown from a handler is the route's answer: a real 3xx Location\nfor whoever asked (a signed-url export, a moved endpoint); notFound() is its\n404. A guard's redirect above a route.ts is a refusal: a browser that\nnavigated there gets the Location, code that fetched it gets 401 +\nX-RSC-Redirect (fetch would follow a Location and hand back the login page).\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')?.value // { name, value } | undefined, as in Next\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.\nTHE RULE: a font never blocks the page - text paints in the fallback before\nthe web font arrives. Three ways to break it, all avoided: a fonts.googleapis\n<link> (render-blocking CSS from a cold origin - self-host via Fontsource\ninstead), font-display: block or unset (invisible text for up to 3s - every\nrule says swap or optional), preloading every file (preload only what the\nfirst paint needs).\n\nAFTER THE ANSWER: after(() => sendEmail(user)) from @rsc-kit/core/request\nqueues work to run once the response is on its way - from an action, a\ncomponent, middleware or an api route. Do NOT use a detached promise: on a\nWorker the isolate dies with the response unless work is handed to\nwaitUntil, which after() does; on a process it runs detached. Rejections\nare logged, never surfaced.\n\nWRITE THE SCHEMA FOR THE SHAPE IT WANTS. The form is read the way the schema\nmeans it, on both sides (Zod 4 / ArkType describe themselves as JSON Schema;\nValibot not yet - its values arrive as strings):\n notify: z.boolean() // unchecked posts nothing -> false; \"on\" -> true\n limit: z.number().int() // \"5\" -> 5. NOT z.coerce.number()\n tags: z.array(z.string()) // one -> ['a'], none ticked -> []\n policy: z.string().optional() // hidden behind a switch -> absent when off\n + .refine((s) => !s.notify || s.policy) for \"required when the switch is on\"\nNested names nest: fields[0][name] / fields[0].name -> { fields: [{ name }] };\nauth[kind] picks a discriminated union's branch. No per-checkbox transform,\nno checkbox() helper. The action decodes the same object the form validated.\nOffline (rscKit({ offline: true })): the precache is what boots the app - js,\ncss, fonts, manifest, icons, / and /offline with their boot payloads; images,\nwasm and the share card are cached on first use. A frozen page never visited\nfalls back to /offline like any other navigation. Do not add a second service\nworker or a precache list; app/sw.js is importScripts'd into this one.\n\nAn action that redirect()s RESOLVES with { redirected: '/where' } on the\nclient once the navigation starts - it does not throw, so a plain\nstartTransition(async () => await logOut()) needs no catch (a rejection\nthere unmounts the root). The result is always an object (exactly one of\ndata / validationErrors / serverError / redirected is set), so reading any\nfield of it is safe. Do NOT wrap actions in a hook to catch\nServerRedirectError; nothing throws.\n\nBefore hydration a submit is a native POST to the page's url (React's hidden\n$ACTION_ fields); the host runs the action and re-renders the page with the\nresult seated in the <Form> that posted - a refusal shows on its fields\nwithout javascript; a redirect() is followed, a cookie lands. Nothing to\nconfigure; <Form> uses useActionState under a wrapper so the action keeps\nits (formData) signature.\nA blank control is absent for any optional field (z.email().optional()\naccepts it; an optional union is not read as its first branch) and \"\" for a\nrequired one (z.string().min(1) refuses it). A leaf\nwith no JSON Schema (z.date()) arrives as posted; its siblings still coerce.\n<Form ref={...}> is fine: the caller's ref is filled beside the form's own.\nRender props also carry dirty (anything differs from mount, uncontrolled\nfields included; baseline moves on a successful submit; reset() clears it):\n{({ dirty, reset }) => <Button disabled={!dirty}>Save</Button>} - the RHF\nisDirty gate for Save/Discard.\n\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('name') on a section() - targeted, rides back with the action; the\n name is typed to the sections and slots the build found\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 middleware.ts subdomain rewrite -> nothing: a host is a route segment (how_to domains)\n flags/next (Vercel Flags SDK) -> unchanged: the build aliases next/headers to @rsc-kit/core/request\n (how_to feature-flags); precompute() does not carry over\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_* -> PUBLIC_* in src/env.ts (how_to env); server vars typed there too\n import 'server-only' -> keep it (the build honours it). Under bun test the real package throws on\n import, so the scaffold's tests/preload.ts stubs it: bunfig.toml\n [test] preload = [\"./tests/preload.ts\"], mock.module('server-only', () => ({})).\n A project without those two files adds them before unit-testing an action.\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 (the build warns naming the app file and the package; the stub throws when called)\n (how_to emails). Next gets away with it only for externalised packages; here it is explicit\n import type { Route } from 'next' -> import type { Route } from '@rsc-kit/core/routes' (pages AND route.ts; a link to a route.ts is an anchor, never prefetched)\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. It builds\n with the project's own build script on the runtime the tests run under, and\n answers files the build wrote to .output/public (assets, sw.js, the\n manifest, icons) as production does - app.fetch('/sw.js') is a real test.\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\nSCAFFOLD FLAGS: --host=bun|node|worker --validation=zod|valibot|arktype|none\n--env/--no-env (typed env vars via @t3-oss/env-core in src/env.ts, in the\nchosen library; server vars never reach the browser, PUBLIC_ prefix for ones\nthat may). Pick the library the Next app already uses.\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\nCONVERT THE FORMS AND ACTIONS - do not carry them. useActionState +\nuseFormStatus, react-hook-form, TanStack Form and useState-per-input all\nstill COMPILE here, which is why a port leaves them. Each becomes\n<Form action={…} schema={…}> (how_to forms) and a createActionClient()\nhandler (how_to action-client). Remove the form library when the last form\nis converted. A port that keeps two form systems has ported nothing.\n\nFull guide: read_guide({ slug: 'coming-from-next' }).`,\n },\n {\n topic: 'feature-flags',\n summary: \"Vercel's Flags SDK (flags/next) runs unchanged: next/headers is answered by headers()/cookies() here\",\n body: `bun add flags. Then flags/next as written for Next:\n\n import { flag, dedupe } from 'flags/next'\n const visitor = dedupe(async ({ cookies, headers }) => ({ id: cookies.get('visitor')?.value ?? 'anon' }))\n export const showBanner = flag<boolean, { id: string }>({ key: 'show-banner', identify: visitor, decide: ({ entities }) => entities?.id === 'ada' })\n\n // page.tsx (server component)\n const on = await showBanner()\n\nThe build aliases next/headers to @rsc-kit/core/request - same names, same\nshapes (cookies().get(name)?.value), one object per request, which the SDK's\ndedupe keys on. Nothing to configure, no shim to write.\n\nA flag reads the request, so the page renders per visitor: put a <Suspense>\nor loading.tsx above the read and the build stores the rest as a shell (the\ntable says \"headers() in run, cookies() in run stream per request\").\n\nDiscovery endpoint: a route.ts -\n export const GET = createFlagsDiscoveryEndpoint(async () => getProviderData(flags))\n export const openapi = false\nprecompute() does NOT carry over (it rewrites urls in Next middleware); read\nthe flag in the page.`,\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\nNO sitemap.ts? The build writes /sitemap.xml itself: every stored page and\nevery generateStaticParams url, lastModified = the build, minus guarded\nroutes (middleware.ts above them), failed pages and not-found. Needs the root\nlayout's metadataBase. Write sitemap.ts only when you need urls the build\ncannot see or per-url changeFrequency/priority.\n\nHOW FRESH - the function decides, the same rule every route follows:\n write nothing -> the build's own sitemap, stored; fresh every deploy\n sitemap.ts that reads the database -> stored at build (○); fresh every deploy\n sitemap.ts that awaits connection() -> rendered per request (ƒ); fresh every crawl\n\\`\\`\\`ts\nexport default async function sitemap() {\n await connection() // from '@rsc-kit/core/request' - per request, like a page\n return (await db.post.findMany()).map((p) => ({ url: \\`/blog/\\${p.slug}\\`, lastModified: p.updatedAt }))\n}\n\\`\\`\\`\nReading the database at build is fine; the REQUEST makes it dynamic, not the\ndata. Same for robots.ts and llms.ts. 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 middleware.ts for them.\n\nFull guide: read_guide({ slug: 'seo-files' }).`,\n },\n {\n topic: 'domains',\n summary: 'Subdomains and custom domains as route segments - admin.example.com reaches app/admin, a tenant host binds [domain], no rewrite',\n body: `A request from a host that is not the site's own is matched with the host\nin FRONT of the path. The site's own hosts: the root layout's metadataBase,\nwww. of it, and rscKit({ hosts: [...] }) - which is ONLY for a name that is\nneither the apex nor a subdomain of it (a staging/internal name, a second\nbrand domain); a normal setup needs no config beyond metadataBase. localhost\nand ips are always own.\n\n example.com/admin -> /admin app/admin/page.tsx\n admin.example.com/ -> /admin the same file (subdomain of an own host = its label)\n acme.example.com/settings -> /acme/settings app/[domain]/settings/page.tsx, domain \"acme\"\n acme.com/settings -> /acme.com/settings the same file, domain \"acme.com\" (other host = whole host)\n\nThe visitor's url is untouched; only the match changes. A top-level [domain]\nbinds ONLY from a host, never from a path: example.com/nope is a 404, not a\ntenant called \"nope\". Otherwise [domain] is an ordinary dynamic segment: params.domain in every page/layout under it, typed\nroute('/[domain]/settings', { domain }), loading/error files as usual. A\ndirectory named for a host (app/admin/) wins over [domain].\n\n\\`\\`\\`tsx\n// src/app/[domain]/layout.tsx\nexport default async function TenantLayout({ params, children }) {\n const { domain } = await params\n const tenant = await tenantByDomain(domain) // \"acme\" or \"acme.com\", as stored\n if (!tenant) notFound()\n return <TenantProvider tenant={tenant}>{children}</TenantProvider>\n}\n// src/app/[domain]/page.tsx - domains in a database: list them, they are stored at build\nexport async function generateStaticParams() {\n return (await db.tenant.findMany()).map((t) => ({ domain: t.domain }))\n}\n\\`\\`\\`\n\nOnly when a route could answer it (a top-level [domain] directory, or one\nnamed for the host); otherwise the host is the site's own. Keep metadataBase as\nthe production host: localhost and ips are always own, so dev routes by path.\nTo try a tenant locally: curl -H 'X-Forwarded-Host: acme.example.com'\nhttp://localhost:3000/ (or /etc/hosts). Behind a proxy the\nhost is X-Forwarded-Host, then Host. Not for a static export (a file server\nsees no host). Do NOT write a middleware rewrite, do NOT\nread the host in every page - the segment already is the host. The root layout\nneeds metadataBase (or rscKit({ hosts })) or every host is the site's own.\n\nFull guide: read_guide({ slug: 'domains' }).`,\n },\n {\n topic: 'identify',\n summary: 'What a response says about itself - X-RSC-Kit (how it was served, always) and X-Powered-By + a generator tag (what built it, off with identify: false)',\n body: `Every response carries X-RSC-Kit: stored | rendered | shell - a page from a\nfile the build wrote, rendered for this visitor, or a stored shell with its\nholes rendered now. The header to read when a page is slower than expected\n(like X-Nextjs-Cache); a CDN rule or health check can key on it. Names no\nproduct; always sent.\n\nBy default a response also says what built it: X-Powered-By: rsc-kit and\n<meta name=\"generator\" content=\"rsc-kit\"> in every document (BuiltWith,\nWappalyzer). The NAME only, never the version - a version in every response\nis what a vulnerability scanner filters on.\n\nrscKit({ identify: false }) turns off the name (header and tag) for a policy\nthat strips framework identifiers; X-RSC-Kit stays. Do not strip X-RSC-Kit\nat the proxy - it is what tells you whether a stored page was served.\n\nFull guide: read_guide({ slug: 'response-headers' }).`,\n },\n {\n topic: 'backend',\n summary: 'BAP (Backend-Answered Pages): a Laravel, Go or other backend behind the renderer - rpc() reaches it, middleware.ts names its middleware, app/Rsc/Actions are its server actions',\n body: `The model is a BAP - Backend-Answered Pages: a page rendered in front of\nthe backend rather than by it (MPA: backend renders; SPA: browser renders and\ncalls an API; BAP: a renderer on the server renders and calls the backend\nover loopback). The backend is the part that is not a page - models, session,\nauth, policies, jobs - answering one private endpoint, and it keeps every\nroute of its own. The whole model, and how to build for it:\nread_guide({ slug: 'backend-answered-pages' }).\n\nGo: in a Go module, rsc-kit init sees go.mod, writes the JS half and .env\n(RSC_BACKEND + a generated secret) and prints the Go wiring; go get\ngithub.com/rsc-kit/go. A new app: bun create rsc-kit --backend=<url>.\n\nA backend in another language answers that ONE endpoint, POST /__rsc/host-call,\nand the renderer wires itself from two variables in .env: RSC_BACKEND (a\nLaravel app's APP_URL counts) and RSC_HOST_CALL_SECRET. Both or neither.\n\nLaravel: composer require rsc-kit/laravel, then php artisan rsc:install. It\nruns rsc-kit init, which writes ONE vite.config.ts (laravel-vite-plugin is\nmoved aside - the renderer owns the frontend). Source is resources/js (the route tree is resources/js/app).\n\nrpc() is a global the renderer installs in its own process (never imported,\nnever in the browser bundle): one POST to the backend's host-call endpoint\nwith { function, args }, the secret and the visitor's cookie; the answer is\nthe return value as JSON, typed by rpc<T>(). Refusals arrive as their kind\n(422/401/403/redirect), never a 500; sibling calls in one tick are batched\nand each resolves the moment the backend answers it.\nA BAP server bundle carries no database driver, ORM or auth library - the\nbackend owns those.\n\nReach PHP from a server component - rpc() is a global, typed in\n.rsc-kit/rsc-env.d.ts, server render only:\n\n // app/Rsc/Orders.php: public function recent(int $limit): array\n const orders = await rpc<Order[]>('Orders.recent', 5)\n\nThe call runs AS THE VISITOR (their cookie is forwarded; auth()->user() is\nthem). Refuse with attributes: #[Authenticated], #[Can('update', Order::class)],\n#[Middleware('throttle:60,1')]. A ValidationException lands on the form as\nvalidationErrors; Authentication/Authorization exceptions answer 401/403.\n\nGuard a route in Laravel's vocabulary, no route declared in PHP:\n\n // resources/js/app/admin/middleware.ts\n export const middleware = ['auth', 'verified', 'can:update,post']\n\nMake one: php artisan make:rsc-action Orders --method=cancel --auth\n--can=update,Order --middleware=throttle:60,1 --revalidate=orders (--rpc for\nan rpc() class under app/Rsc; no --method = invokable). Do NOT hand-write the\nattributes from memory; the command writes the ones the registry reads.\nServer actions are classes in app/Rsc/Actions; \\`php artisan\nrsc:action-manifest\\` (already in the dev/build scripts) writes the map and\nthe build writes server-actions.generated.ts beside the app - import\nordersCancel from it in a client component. Rsc::revalidate('orders') in the\naction returns the re-rendered region with the answer.\n\nPer url: if the React tree has it, React renders it, otherwise Laravel does\n- including / : the welcome route in routes/web.php answers nothing while\nresources/js/app/page.tsx exists (the package registers the tree's urls from\nbootstrap/rsc/vite/routes.json after routes/web.php). Do NOT tell the user\nto delete the welcome route to make the page show; do not add Laravel routes\nfor React pages.\n\nphp artisan serve is one worker, which deadlocks the proxy - unless\nPHP_CLI_SERVER_WORKERS=4 in .env AND serve --no-reload (Laravel ignores\nthe variable otherwise). Herd, Valet, FPM, Octane are fine as they are. Production: put the renderer in front (bun\n.output/server/index.mjs with the app's .env), restrict /__rsc/host-call at\nthe web server.\n\nAny other language implements the same endpoint - request { function, args },\nreply { result | validationErrors | unauthenticated | unauthorized | redirect\n| error, revalidate }, answers '__rsc.middleware' with true or a refusal, and\na batch { calls: [...] } as application/x-ndjson - one line per call AS IT\nFINISHES, { index, status, ...reply }, in any order, flushed each time\n(X-Accel-Buffering: no) - or, less good, one JSON { replies: [...] } in\norder (every call then waits for the slowest). Calls issued in the same\nrender tick travel as one batch, so parallel reads are one backend request\nand a fast read still resolves while a slow sibling runs; the renderer falls\nback to single calls for a backend without batches.\n\nFull guides: read_guide({ slug: 'backend-answered-pages' }), read_guide({ slug: 'laravel' }), read_guide({ slug: 'go' }), read_guide({ slug: 'your-own-backend' }).`,\n },\n {\n topic: 'startup',\n summary: 'Once-per-process setup - src/instrumentation.ts is imported before any page and its register() awaited before the first request',\n body: `Setup that belongs to the process - validating env, configuring a shared\npackage, warming a connection - goes in src/instrumentation.ts. Do NOT import\na bootstrap module from pages to get the same effect; it depends on nobody\nforgetting, and the failure is a page throwing \"not configured\" for whoever\nreaches it first.\n\n // src/instrumentation.ts\n import './env' // refuses at import -> server fails at startup\n export async function register() { // optional; the first render waits for it\n await db.connect()\n }\n\nThe generated entry imports this file FIRST, so a package configured here is\nconfigured before any page module evaluates. register() is awaited by every\nentry point (server, dev, prerender, middleware, actions, api routes), once\nper process. On a server it runs at startup and a failure exits the process;\non a Worker it runs at the isolate's first request.\n\nWorker rule: read bindings INSIDE register(), not at the top of the module -\nprocess.env is empty until the first request arrives.\n\nA scaffolded app with env validation already has this file importing ./env.\nBuild machines without production variables: SKIP_ENV_VALIDATION=1.\n\nNever NODE_ENV in a .env: Vite sets it (development under vite, production\nunder vite build) and a .env line overrides it for the build, which then\ncompiles pages against React's dev JSX runtime (jsxDEV) and every route\nfails with React's opaque \"message omitted in production builds\". The build\nrefuses this and names the file:line. A plugin cannot override it (Vite\napplies the .env value after plugins run), so remove the line - other tools\nthat want it keep it in their own .env.\n\nFull guide: read_guide({ slug: 'instrumentation' }).`,\n },\n {\n topic: 'bun',\n summary: 'Running on Bun - Vite on Bun\\'s runtime (bun --bun vite), native deps external (serverExternalPackages), and the gotchas that are Bun\\'s not ours',\n body: `The vite bin has a node shebang: \\`bun run dev\\` alone starts Vite - dev\nserver, build, prerender - under NODE, and an app importing 'bun' or\n'bun:sqlite' fails at first render with \"Cannot find package 'bun'\". Scripts\non a Bun app: dev \"bun --bun vite\", build \"bun --bun vite build\". createTestApp\nruns the project's build script on the runtime the tests use.\n\nNative dependencies (sharp, bcrypt, better-sqlite3, @prisma/client, puppeteer,\n...) are external to the server bundles by default; Nitro traces them into\n.output/server/node_modules with their binaries. Add one:\nrscKit({ serverExternalPackages: ['@acme/native'] }). Same as Next's option.\n\nThe other direction: a dependency with \"use client\" files that does NOT\ndeclare react as a peerDependency (generated wrappers, workspace packages\nwith react under dependencies) would be left external by plugin-rsc and its\ndirective never read - hooks then run on the server. The build detects direct\ndependencies in that state and bundles them, printing\n\"[rsc-kit] bundling <pkg>: it has \"use client\" files but does not declare\nreact as a peer dependency\". Nothing to configure; fix the package's\npeerDependencies when it is yours.\n\nBun's, not the framework's: bun test loads the package .env (use\n--env-file=/dev/null to isolate); Stripe's constructEvent throws on Bun\n(no sync WebCrypto) - use constructEventAsync; Bun's pg puts SQLSTATE in\nerrno where Node's pg uses code.\n\nNever NODE_ENV in a .env (the build refuses it, naming the line). Build\nmachines without secrets: SKIP_ENV_VALIDATION=1.\n\nreflect-metadata (tsyringe, typeorm, inversify - often under\n@simplewebauthn/server): nothing to import; when the graph has it the build\nloads it in a Nitro plugin ahead of the app, so the \"tsyringe requires a\nreflect polyfill\" boot error does not happen in a directory or a binary.\n\nBuild, then start - NEVER run vite build while bun/node .output/server/index.mjs\nis serving from that .output: services load lazily and a failed import of a\nhalf-written chunk is cached by the runtime (ENOENT 500s until restart).\n\nSingle binary (Bun): bun build --compile .output/server/compile.mjs\n--outfile dist/app (the scaffold's \"compile\" script). compile.mjs is written\nby the build and embeds the frozen pages; with serveStatic: 'inline' in the\nNitro plugin the assets (and their .br/.gz) are inside too. Ship dist/app\nalone - a Dockerfile copies nothing else, not .output/public.\n\nNothing is sent raw: a built bun/node server gzips what it answers\n(documents, streams flushed per chunk, payloads, stored pages, api routes)\nfor a request that accepts it, and the build writes .br/.gz beside every\npublic asset, served by Nitro. Nothing to configure; a Worker leaves it to\nthe platform. Off: compress: false on the handler / compressPublicAssets:\nfalse in Nitro config; Cache-Control: no-transform exempts one answer. Do\nNOT add a compression middleware or precompress assets yourself.\n\nFull guide: read_guide({ slug: 'bun' }).`,\n },\n {\n topic: 'openapi',\n summary: 'An OpenAPI document derived from route.ts files - rscKit({ openapi }) - and Scalar\\'s page over it, mounted as a route',\n body: `Do NOT hand-write an OpenAPI spec. rscKit({ openapi: true }) in\nvite.config.ts answers /openapi.json, derived from every route.ts: the\ndirectory is the path ([id] -> {id}), each method export an operation,\nparams/searchParams/body schemas the parameters and request body (Zod 4 and\nArkType describe themselves as JSON Schema; Valibot not yet), a middleware.ts\nabove a route a security requirement + 401/403. Stored at build, no middleware.\n\nDocument-level parts go on the option:\n rscKit({ openapi: { info, servers, security, components: { securitySchemes } } })\nWhat a route says about itself, beside its handler:\n export const openapi = { summary, tags, responses: { 200: {...} }, POST: { summary } }\n export const openapi = false // leave this route out (the reference page, a webhook)\n export const openapi = { DELETE: false } // one method out; HEAD/OPTIONS never documented\nWebhook-heavy app: rscKit({ openapi: { include: 'declared' } }) documents only\nroutes that export openapi, so callbacks need no opt-out line.\nResponse bodies are declared in openapi.responses until a typed helper exists.\n\nThe page: Scalar's own package, one route, nothing shipped by the engine:\n // src/app/reference/route.ts\n import { ApiReference } from '@scalar/nextjs-api-reference'\n export const GET = ApiReference({ url: '/openapi.json' })\n export const openapi = false\n\nPorting a spec file: delete its paths (they are the routes now, and body\nvalidates at runtime), move info/servers/security to the option, move a\nroute's summary/tags/responses to its openapi export.\n\nFull guide: read_guide({ slug: 'openapi' }).`,\n },\n {\n topic: 'env',\n summary: 'Typed environment variables - src/env.ts with @t3-oss/env-core in the app\\'s validation library; refused at startup by name. Never NODE_ENV in .env',\n body: `A scaffolded app has src/env.ts when it said yes to typed environment\nvariables (create-rsc-kit --env, with --validation=zod|valibot|arktype). To\nadd it to an app without one: install @t3-oss/env-core and write the same file.\n\n\\`\\`\\`ts\n// src/env.ts\nimport * as z from 'zod' // or valibot / arktype - any Standard Schema library\nimport { createEnv } from '@t3-oss/env-core'\n\nexport const env = createEnv({\n server: {\n NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),\n DATABASE_URL: z.url(),\n SESSION_SECRET: z.string().min(1),\n },\n clientPrefix: 'PUBLIC_',\n client: { PUBLIC_SITE_URL: z.url() },\n // No bare process.env: a \"use client\" file importing this for a PUBLIC_\n // value has no process, and the spread throws before the first render.\n runtimeEnv: { ...(typeof process === 'undefined' ? {} : process.env), ...import.meta.env },\n emptyStringAsUndefined: true,\n skipValidation: typeof process !== 'undefined' && !!process.env.SKIP_ENV_VALIDATION,\n})\n\\`\\`\\`\n\nRead env.DATABASE_URL, never process.env.DATABASE_URL: the first is typed and\nwas checked at startup (a missing or malformed one fails then, with its name),\nthe second is string | undefined. A server variable never reaches the browser;\na browser-readable one MUST start with PUBLIC_ and is read from import.meta.env\n(Vite; the engine registers PUBLIC_ beside VITE_ as a client prefix, nothing\nto configure), which is why runtimeEnv merges both. Commit .env.example, not .env.\n\nNext: NEXT_PUBLIC_* becomes PUBLIC_*; @t3-oss/env-nextjs becomes\n@t3-oss/env-core with runtimeEnv as above (env-nextjs's experimental__runtimeEnv\nis not needed).`,\n },\n {\n topic: 'live-data',\n summary: 'A value that keeps changing - realtime, live updates: usePolling over a query, or server-sent events (SSE, streaming from a middleware.ts generator) with useEvents - both feed TanStack, SWR or setState',\n body: `Neither is part of query() - a query answers once and is cacheable.\n\nPOLLING - start here when you have no change feed yet. Reuses the query,\ngoes through its Cache-Control (a CDN collapses many tabs into one origin\nread per interval), pauses when the tab is hidden, never overlaps two reads.\n\\`\\`\\`tsx\nimport { usePolling } from '@rsc-kit/core/usePolling'\nconst { data, status, refresh } = usePolling(() => fetchQuery(getSeats), { every: 2_000 })\n\\`\\`\\`\nUNTIL IT SETTLES - a job that ends. until(data) says the last read; onSettled\nfires once on it. The result is the DATA; what to\ndo on settling is the page's choice:\n // a server-rendered list, some jobs still running: re-render through the server\n usePolling(() => fetchQuery(jobStatus, [id]), { every: 2_000, enabled: !isTerminal(job), until: isTerminal, onSettled: () => refresh('page') })\n // the page that owns the job's state machine: the value in hand\n const { data, status } = usePolling(read, { every: 1_500, until: isTerminal, onSettled: (f) => dispatch(f.status) })\nSettled = stopped until refresh() or the inputs change. status: 'reading' |\n'paused' | 'settled' | 'idle'.\n\nSERVER-SENT EVENTS - when something can push. Better per update (bytes only\non change, instant), but holds a connection per open tab (fine on Bun/Node,\na limit on Workers or a small container), is uncacheable, and needs a source\nof change to yield from - a generator that polls the DB itself just moved the\npolling. An ordinary route.ts: beside its pages, runs middleware.ts above it.\n\\`\\`\\`ts\n// src/app/api/orders/[id]/events/route.ts\nimport { events, named } from '@rsc-kit/core/events'\nexport const GET = events(async function* ({ params, signal }) {\n const { id } = await params\n for await (const status of orderStatus(id, { signal })) yield { status }\n // yield named('paid', order, { id: order.id }) names a message / gives an id\n})\n\\`\\`\\`\n\\`\\`\\`tsx\nimport { useEvents } from '@rsc-kit/core/useEvents'\nconst { latest, all, status, close } = useEvents<Status>(\\`/api/orders/\\${id}/events\\`)\n// <Status> is the message type the route yields; a url infers nothing, so\n// without it latest is unknown. Declare the type beside the route, import both sides.\n\\`\\`\\`\nevents() frames JSON, sends a keepalive, sets text/event-stream + no-store,\nends the generator on disconnect (signal). EventSource reconnects itself and\nresumes with Last-Event-ID when you yielded ids.\n\nNO LIBRARY NEEDED. Both hooks ARE state: read data (polling) or latest\n(events) and render it. Neither needs TanStack or SWR.\n const { latest } = useEvents<Status>(url); const current = latest ?? initial // the server value until the first message\nWITH A STORE - when the value already lives somewhere, hand every value on so\nthat stays the truth: a useState, a reducer, or a cache library:\n useEvents<Order>(url, { onMessage: setOrder }) // useState\n useEvents<Order>(url, { onMessage: (m) => dispatch({ type: 'update', m }) }) // reducer\n useEvents<Order>(url, { onMessage: (m) => queryClient.setQueryData(['order', id], m) }) // TanStack\n useEvents<Order>(url, { onMessage: (m) => mutate(['order', id], m, false) }) // SWR\n usePolling(read, { every, onData: setSeats })\nSTABLE CALLBACKS: a callback a timer/subscription/listener calls that must see\nthe latest props is useEffectEvent from React (19.2+), never a ref assigned\neach render, and never in a dependency array. Both hooks are built on it.\n\nERRORS: both hooks expose error as state AND fire onError - a failed poll read\n(the next interval still reads) or a dropped stream (EventSource reconnects\nitself). Use onError for a toast/log; do NOT watch error in a useEffect.\nusePolling's onError gets (error, { failures }) - failed reads in a row, reset\nby a success - so toast on the third, not the first.\nfetchQuery(query, args) types args from the query: fetchQuery(getSeats) for a\nquery that takes nothing, fetchQuery(status, [{ id }]) refused if the query's\ninput has no id.\nDo NOT put a stream on query() or on a server action, and do NOT poll from\ninside an events() generator.\n\nFull guides: read_guide({ slug: 'queries' }) for polling, read_guide({ slug:\n'api-routes' }) for the streaming route.`,\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 // The summary is prose, so a multi-word ask is matched with its spaces back.\n RECIPES.find((r) => r.summary.toLowerCase().replace(/[-\\s]+/g, ' ').includes(wanted.replace(/-/g, ' ')))\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. Uncontrolled by default - no useState per field',\n body: `THE RULE: forms are UNCONTROLLED. Inputs keep their value in the DOM,\nan initial value is defaultValue, the action reads FormData. Do NOT write\nuseState + value/onChange per input, and do NOT reach for TanStack Form.\nControl ONE field only when the UI must react as the user types (a character\ncount, a live preview, a dependent select) - bind it with useField, which\nscopes the re-render to that field. Everything else stays uncontrolled.\n\nUse <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 Route\\` 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, another page, a refresh —\ncall fetchQuery. It is a plain async function that returns the typed answer;\nNO library is needed:\n\n\\`\\`\\`tsx\nconst [listings, setListings] = useState(initial) // the server-rendered value\nconst [pending, start] = useTransition()\nconst show = (kind) => start(async () => setListings(await fetchQuery(getListings, [kind])))\n\\`\\`\\`\nWorks in an onClick, onSubmit, useEffect - anywhere in the browser.\n\n**When you want CACHING** (stale-while-revalidate, dedupe, offline), hand the\nsame call to the library that holds the answer; this package ships none:\n\\`\\`\\`tsx\nuseQuery({ queryKey: ['posts', kind], queryFn: () => fetchQuery(getPosts, [kind]) })\nuseSWR(['posts', kind], () => fetchQuery(getPosts, [kind]))\n\\`\\`\\`\nfetchQuery sends the read as a GET and goes to the server every time, which\nis what a fetcher needs. It will never cache, dedupe or batch: a client\ncache is the library's job, and a batch would lose the per-read cache key a\nGET has. The ladder, most reads stopping on the first rung:\n 1. the value now: fetchQuery + setState\n 2. survive a reload / let a CDN serve it: query(fn, { cache: 'public', maxAge })\n - HTTP caching, no code in the page\n 3. staleness, background refresh, optimistic updates, shared across\n components: TanStack or SWR with fetchQuery as the fetcher\nDo not add a cache on top of it, and do not install TanStack for a single\nbutton that reads once.\n\nKeep the arrow: TanStack calls a bare \\`queryFn\\` with its own context, and a\nserver function serialises whatever it is handed.\n\nA value that keeps CHANGING while someone watches - polling, SSE, realtime -\nis how_to live-data, not this.`,\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\nredirect() and notFound() inside a boundary still work: the shell has gone\nout, so the redirect travels in the row's error digest and the browser\nperforms it as a navigation, layouts kept. An error.tsx on the route never\nsees it - a redirect is the page's answer, not a failure - and the same holds\nfor a component under its own <Suspense> and for a parallel route slot. Only\nan authorization check should NOT be there: the layouts above already\nrendered. See the redirects guide.`,\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\"\nimport type { RouteContext } from '@rsc-kit/core/route-schema'\n\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 }: RouteContext<typeof params>) {\n const { id } = await params\n\n return Response.json(await findPost(id))\n}\n\nexport async function POST(request: Request, { params, body }: RouteContext<typeof params, never, typeof body>) {\n const { title } = await body\n\n return Response.json(await createPost(title), { status: 201 })\n}\n\\`\\`\\`\n\nTYPE THE CONTEXT: RouteContext<'/api/items/[id]'> types params from the\nroute's segments (the pattern is checked against the routes the build found);\nRouteContext<typeof params> from a schema. Every field is a PROMISE - a sync\nparams.id is a compile error, never a route that 404s. Do NOT hand-write\n{ params: { id: string } }.\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\nA route.ts is a Route too (type Route from '@rsc-kit/core/routes' covers pages\nAND route.ts files, as Next's does): Link, visit and redirect accept it. The\nclient treats a link to a route as an anchor - never prefetched, a full\nnavigation, not a payload fetch. ApiRoute is the narrower union for apiUrl:\napiUrl refuses a page, because fetching one gets html. It checks the PATH, not\nthe response type - for types across the boundary use a server action or a\nquery, where the return type is the function'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. NEVER read the query with\nnew URL(request.url).searchParams (the Next way): the build cannot see that\nread, and reading request.url at all makes the route dynamic (the table says\n\"reads the request - url\"). A webhook verification handshake (hub.mode,\nhub.challenge) reads the awaited searchParams. A GET that answers 4xx/5xx to\nthe build is never stored either (the table says \"answered 403 to the build\").\n\nredirect() thrown from a handler is the route's answer: a real 3xx Location\nfor whoever asked (a signed-url export, a moved endpoint); notFound() is its\n404. A guard's redirect above a route.ts is a refusal: a browser that\nnavigated there gets the Location, code that fetched it gets 401 +\nX-RSC-Redirect (fetch would follow a Location and hand back the login page).\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\nSeveral checks in one directory: export default [signedIn, verified, admin] -\nrun in order, stopping at the first refusal; reuse a check by importing it\nfrom one place. Directories still compose outermost first.\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')?.value // { name, value } | undefined, as in Next\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.\nTHE RULE: a font never blocks the page - text paints in the fallback before\nthe web font arrives. Three ways to break it, all avoided: a fonts.googleapis\n<link> (render-blocking CSS from a cold origin - self-host via Fontsource\ninstead), font-display: block or unset (invisible text for up to 3s - every\nrule says swap or optional), preloading every file (preload only what the\nfirst paint needs).\n\nAFTER THE ANSWER: after(() => sendEmail(user)) from @rsc-kit/core/request\nqueues work to run once the response is on its way - from an action, a\ncomponent, middleware or an api route. Do NOT use a detached promise: on a\nWorker the isolate dies with the response unless work is handed to\nwaitUntil, which after() does; on a process it runs detached. Rejections\nare logged, never surfaced.\n\nWRITE THE SCHEMA FOR THE SHAPE IT WANTS. The form is read the way the schema\nmeans it, on both sides (Zod 4 / ArkType describe themselves as JSON Schema;\nValibot not yet - its values arrive as strings):\n notify: z.boolean() // unchecked posts nothing -> false; \"on\" -> true\n limit: z.number().int() // \"5\" -> 5. NOT z.coerce.number()\n tags: z.array(z.string()) // one -> ['a'], none ticked -> []\n policy: z.string().optional() // hidden behind a switch -> absent when off\n + .refine((s) => !s.notify || s.policy) for \"required when the switch is on\"\nNested names nest: fields[0][name] / fields[0].name -> { fields: [{ name }] };\nauth[kind] picks a discriminated union's branch. No per-checkbox transform,\nno checkbox() helper. The action decodes the same object the form validated.\nOffline (rscKit({ offline: true })): the precache is what boots the app - js,\ncss, fonts, manifest, icons, / and /offline with their boot payloads; images,\nwasm and the share card are cached on first use. A frozen page never visited\nfalls back to /offline like any other navigation. Do not add a second service\nworker or a precache list; app/sw.js is importScripts'd into this one.\n\nAn action that redirect()s RESOLVES with { redirected: '/where' } on the\nclient once the navigation starts - it does not throw, so a plain\nstartTransition(async () => await logOut()) needs no catch (a rejection\nthere unmounts the root). The result is always an object (exactly one of\ndata / validationErrors / serverError / redirected is set), so reading any\nfield of it is safe. Do NOT wrap actions in a hook to catch\nServerRedirectError; nothing throws.\n\nBefore hydration a submit is a native POST to the page's url (React's hidden\n$ACTION_ fields); the host runs the action and re-renders the page with the\nresult seated in the <Form> that posted - a refusal shows on its fields\nwithout javascript; a redirect() is followed, a cookie lands. Nothing to\nconfigure; <Form> uses useActionState under a wrapper so the action keeps\nits (formData) signature.\nA blank control is absent for any optional field (z.email().optional()\naccepts it; an optional union is not read as its first branch) and \"\" for a\nrequired one (z.string().min(1) refuses it). A leaf\nwith no JSON Schema (z.date()) arrives as posted; its siblings still coerce.\n<Form ref={...}> is fine: the caller's ref is filled beside the form's own.\nRender props also carry dirty (anything differs from mount, uncontrolled\nfields included; baseline moves on a successful submit; reset() clears it):\n{({ dirty, reset }) => <Button disabled={!dirty}>Save</Button>} - the RHF\nisDirty gate for Save/Discard.\n\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('name') on a section() - targeted, rides back with the action; the\n name is typed to the sections and slots the build found\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 middleware.ts subdomain rewrite -> nothing: a host is a route segment (how_to domains)\n flags/next (Vercel Flags SDK) -> unchanged: the build aliases next/headers to @rsc-kit/core/request\n (how_to feature-flags); precompute() does not carry over\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_* -> PUBLIC_* in src/env.ts (how_to env); server vars typed there too\n import 'server-only' -> keep it (the build honours it). Under bun test the real package throws on\n import, so the scaffold's tests/preload.ts stubs it: bunfig.toml\n [test] preload = [\"./tests/preload.ts\"], mock.module('server-only', () => ({})).\n A project without those two files adds them before unit-testing an action.\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 (the build warns naming the app file and the package; the stub throws when called)\n (how_to emails). Next gets away with it only for externalised packages; here it is explicit\n import type { Route } from 'next' -> import type { Route } from '@rsc-kit/core/routes' (pages AND route.ts; a link to a route.ts is an anchor, never prefetched)\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. It builds\n with the project's own build script on the runtime the tests run under, and\n answers files the build wrote to .output/public (assets, sw.js, the\n manifest, icons) as production does - app.fetch('/sw.js') is a real test.\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\nSCAFFOLD FLAGS: --host=bun|node|worker --validation=zod|valibot|arktype|none\n--env/--no-env (typed env vars via @t3-oss/env-core in src/env.ts, in the\nchosen library; server vars never reach the browser, PUBLIC_ prefix for ones\nthat may). Pick the library the Next app already uses.\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\nCONVERT THE FORMS AND ACTIONS - do not carry them. useActionState +\nuseFormStatus, react-hook-form, TanStack Form and useState-per-input all\nstill COMPILE here, which is why a port leaves them. Each becomes\n<Form action={…} schema={…}> (how_to forms) and a createActionClient()\nhandler (how_to action-client). Remove the form library when the last form\nis converted. A port that keeps two form systems has ported nothing.\n\nFull guide: read_guide({ slug: 'coming-from-next' }).`,\n },\n {\n topic: 'feature-flags',\n summary: \"Vercel's Flags SDK (flags/next) runs unchanged: next/headers is answered by headers()/cookies() here\",\n body: `bun add flags. Then flags/next as written for Next:\n\n import { flag, dedupe } from 'flags/next'\n const visitor = dedupe(async ({ cookies, headers }) => ({ id: cookies.get('visitor')?.value ?? 'anon' }))\n export const showBanner = flag<boolean, { id: string }>({ key: 'show-banner', identify: visitor, decide: ({ entities }) => entities?.id === 'ada' })\n\n // page.tsx (server component)\n const on = await showBanner()\n\nThe build aliases next/headers to @rsc-kit/core/request - same names, same\nshapes (cookies().get(name)?.value), one object per request, which the SDK's\ndedupe keys on. Nothing to configure, no shim to write.\n\nA flag reads the request, so the page renders per visitor: put a <Suspense>\nor loading.tsx above the read and the build stores the rest as a shell (the\ntable says \"headers() in run, cookies() in run stream per request\").\n\nDiscovery endpoint: a route.ts -\n export const GET = createFlagsDiscoveryEndpoint(async () => getProviderData(flags))\n export const openapi = false\nprecompute() does NOT carry over (it rewrites urls in Next middleware); read\nthe flag in the page.`,\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\nNO sitemap.ts? The build writes /sitemap.xml itself: every stored page and\nevery generateStaticParams url, lastModified = the build, minus guarded\nroutes (middleware.ts above them), failed pages and not-found. Needs the root\nlayout's metadataBase. Write sitemap.ts only when you need urls the build\ncannot see or per-url changeFrequency/priority.\n\nHOW FRESH - the function decides, the same rule every route follows:\n write nothing -> the build's own sitemap, stored; fresh every deploy\n sitemap.ts that reads the database -> stored at build (○); fresh every deploy\n sitemap.ts that awaits connection() -> rendered per request (ƒ); fresh every crawl\n\\`\\`\\`ts\nexport default async function sitemap() {\n await connection() // from '@rsc-kit/core/request' - per request, like a page\n return (await db.post.findMany()).map((p) => ({ url: \\`/blog/\\${p.slug}\\`, lastModified: p.updatedAt }))\n}\n\\`\\`\\`\nReading the database at build is fine; the REQUEST makes it dynamic, not the\ndata. Same for robots.ts and llms.ts. 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 middleware.ts for them.\n\nFull guide: read_guide({ slug: 'seo-files' }).`,\n },\n {\n topic: 'domains',\n summary: 'Subdomains and custom domains as route segments - admin.example.com reaches app/admin, a tenant host binds [domain], no rewrite',\n body: `A request from a host that is not the site's own is matched with the host\nin FRONT of the path. The site's own hosts: the root layout's metadataBase,\nwww. of it, and rscKit({ hosts: [...] }) - which is ONLY for a name that is\nneither the apex nor a subdomain of it (a staging/internal name, a second\nbrand domain); a normal setup needs no config beyond metadataBase. localhost\nand ips are always own.\n\n example.com/admin -> /admin app/admin/page.tsx\n admin.example.com/ -> /admin the same file (subdomain of an own host = its label)\n acme.example.com/settings -> /acme/settings app/[domain]/settings/page.tsx, domain \"acme\"\n acme.com/settings -> /acme.com/settings the same file, domain \"acme.com\" (other host = whole host)\n\nThe visitor's url is untouched; only the match changes. A top-level [domain]\nbinds ONLY from a host, never from a path: example.com/nope is a 404, not a\ntenant called \"nope\". Otherwise [domain] is an ordinary dynamic segment: params.domain in every page/layout under it, typed\nroute('/[domain]/settings', { domain }), loading/error files as usual. A\ndirectory named for a host (app/admin/) wins over [domain].\n\n\\`\\`\\`tsx\n// src/app/[domain]/layout.tsx\nexport default async function TenantLayout({ params, children }) {\n const { domain } = await params\n const tenant = await tenantByDomain(domain) // \"acme\" or \"acme.com\", as stored\n if (!tenant) notFound()\n return <TenantProvider tenant={tenant}>{children}</TenantProvider>\n}\n// src/app/[domain]/page.tsx - domains in a database: list them, they are stored at build\nexport async function generateStaticParams() {\n return (await db.tenant.findMany()).map((t) => ({ domain: t.domain }))\n}\n\\`\\`\\`\n\nOnly when a route could answer it (a top-level [domain] directory, or one\nnamed for the host); otherwise the host is the site's own. Keep metadataBase as\nthe production host: localhost and ips are always own, so dev routes by path.\nTo try a tenant locally: curl -H 'X-Forwarded-Host: acme.example.com'\nhttp://localhost:3000/ (or /etc/hosts). Behind a proxy the\nhost is X-Forwarded-Host, then Host. Not for a static export (a file server\nsees no host). Do NOT write a middleware rewrite, do NOT\nread the host in every page - the segment already is the host. The root layout\nneeds metadataBase (or rscKit({ hosts })) or every host is the site's own.\n\nFull guide: read_guide({ slug: 'domains' }).`,\n },\n {\n topic: 'identify',\n summary: 'What a response says about itself - X-RSC-Kit (how it was served, always) and X-Powered-By + a generator tag (what built it, off with identify: false)',\n body: `Every response carries X-RSC-Kit: stored | rendered | shell - a page from a\nfile the build wrote, rendered for this visitor, or a stored shell with its\nholes rendered now. The header to read when a page is slower than expected\n(like X-Nextjs-Cache); a CDN rule or health check can key on it. Names no\nproduct; always sent.\n\nBy default a response also says what built it: X-Powered-By: rsc-kit and\n<meta name=\"generator\" content=\"rsc-kit\"> in every document (BuiltWith,\nWappalyzer). The NAME only, never the version - a version in every response\nis what a vulnerability scanner filters on.\n\nrscKit({ identify: false }) turns off the name (header and tag) for a policy\nthat strips framework identifiers; X-RSC-Kit stays. Do not strip X-RSC-Kit\nat the proxy - it is what tells you whether a stored page was served.\n\nFull guide: read_guide({ slug: 'response-headers' }).`,\n },\n {\n topic: 'backend',\n summary: 'BAP (Backend-Answered Pages): a Laravel, Go or other backend behind the renderer - rpc() reaches it, middleware.ts names its middleware, app/Rsc/Actions are its server actions',\n body: `The model is a BAP - Backend-Answered Pages: a page rendered in front of\nthe backend rather than by it (MPA: backend renders; SPA: browser renders and\ncalls an API; BAP: a renderer on the server renders and calls the backend\nover loopback). The backend is the part that is not a page - models, session,\nauth, policies, jobs - answering one private endpoint, and it keeps every\nroute of its own. The whole model, and how to build for it:\nread_guide({ slug: 'backend-answered-pages' }).\n\nGo: in a Go module, rsc-kit init sees go.mod, writes the JS half and .env\n(RSC_BACKEND + a generated secret) and prints the Go wiring; go get\ngithub.com/rsc-kit/go. A new app: bun create rsc-kit --backend=<url>.\n\nA backend in another language answers that ONE endpoint, POST /__rsc/host-call,\nand the renderer wires itself from two variables in .env: RSC_BACKEND (a\nLaravel app's APP_URL counts) and RSC_HOST_CALL_SECRET. Both or neither.\n\nLaravel: composer require rsc-kit/laravel, then php artisan rsc:install. It\nruns rsc-kit init, which writes ONE vite.config.ts (laravel-vite-plugin is\nmoved aside - the renderer owns the frontend). Source is resources/js (the route tree is resources/js/app).\n\nrpc() is a global the renderer installs in its own process (never imported,\nnever in the browser bundle): one POST to the backend's host-call endpoint\nwith { function, args }, the secret and the visitor's cookie; the answer is\nthe return value as JSON, typed by rpc<T>(). Refusals arrive as their kind\n(422/401/403/redirect), never a 500; sibling calls in one tick are batched\nand each resolves the moment the backend answers it.\nA BAP server bundle carries no database driver, ORM or auth library - the\nbackend owns those.\n\nReach PHP from a server component - rpc() is a global, typed in\n.rsc-kit/rsc-env.d.ts, server render only:\n\n // app/Rsc/Orders.php: public function recent(int $limit): array\n const orders = await rpc<Order[]>('Orders.recent', 5)\n\nThe call runs AS THE VISITOR (their cookie is forwarded; auth()->user() is\nthem). Refuse with attributes: #[Authenticated], #[Can('update', Order::class)],\n#[Middleware('throttle:60,1')]. A ValidationException lands on the form as\nvalidationErrors; Authentication/Authorization exceptions answer 401/403.\n\nGuard a route in Laravel's vocabulary, no route declared in PHP:\n\n // resources/js/app/admin/middleware.ts\n export const middleware = ['auth', 'verified', 'can:update,post']\n\nMake one: php artisan make:rsc-action Orders --method=cancel --auth\n--can=update,Order --middleware=throttle:60,1 --revalidate=orders (--rpc for\nan rpc() class under app/Rsc; no --method = invokable). Do NOT hand-write the\nattributes from memory; the command writes the ones the registry reads.\nServer actions are classes in app/Rsc/Actions; \\`php artisan\nrsc:action-manifest\\` (already in the dev/build scripts) writes the map and\nthe build writes server-actions.generated.ts beside the app - import\nordersCancel from it in a client component. Rsc::revalidate('orders') in the\naction returns the re-rendered region with the answer.\n\nPer url: if the React tree has it, React renders it, otherwise Laravel does\n- including / : the welcome route in routes/web.php answers nothing while\nresources/js/app/page.tsx exists (the package registers the tree's urls from\nbootstrap/rsc/vite/routes.json after routes/web.php). Do NOT tell the user\nto delete the welcome route to make the page show; do not add Laravel routes\nfor React pages.\n\nphp artisan serve is one worker, which deadlocks the proxy - unless\nPHP_CLI_SERVER_WORKERS=4 in .env AND serve --no-reload (Laravel ignores\nthe variable otherwise). Herd, Valet, FPM, Octane are fine as they are. Production: put the renderer in front (bun\n.output/server/index.mjs with the app's .env), restrict /__rsc/host-call at\nthe web server.\n\nAny other language implements the same endpoint - request { function, args },\nreply { result | validationErrors | unauthenticated | unauthorized | redirect\n| error, revalidate }, answers '__rsc.middleware' with true or a refusal, and\na batch { calls: [...] } as application/x-ndjson - one line per call AS IT\nFINISHES, { index, status, ...reply }, in any order, flushed each time\n(X-Accel-Buffering: no) - or, less good, one JSON { replies: [...] } in\norder (every call then waits for the slowest). Calls issued in the same\nrender tick travel as one batch, so parallel reads are one backend request\nand a fast read still resolves while a slow sibling runs; the renderer falls\nback to single calls for a backend without batches.\n\nFull guides: read_guide({ slug: 'backend-answered-pages' }), read_guide({ slug: 'laravel' }), read_guide({ slug: 'go' }), read_guide({ slug: 'your-own-backend' }).`,\n },\n {\n topic: 'startup',\n summary: 'Once-per-process setup - src/instrumentation.ts is imported before any page and its register() awaited before the first request',\n body: `Setup that belongs to the process - validating env, configuring a shared\npackage, warming a connection - goes in src/instrumentation.ts. Do NOT import\na bootstrap module from pages to get the same effect; it depends on nobody\nforgetting, and the failure is a page throwing \"not configured\" for whoever\nreaches it first.\n\n // src/instrumentation.ts\n import './env' // refuses at import -> server fails at startup\n export async function register() { // optional; the first render waits for it\n await db.connect()\n }\n\nThe generated entry imports this file FIRST, so a package configured here is\nconfigured before any page module evaluates. register() is awaited by every\nentry point (server, dev, prerender, middleware, actions, api routes), once\nper process. On a server it runs at startup and a failure exits the process;\non a Worker it runs at the isolate's first request.\n\nWorker rule: read bindings INSIDE register(), not at the top of the module -\nprocess.env is empty until the first request arrives.\n\nA scaffolded app with env validation already has this file importing ./env.\nBuild machines without production variables: SKIP_ENV_VALIDATION=1.\n\nNever NODE_ENV in a .env: Vite sets it (development under vite, production\nunder vite build) and a .env line overrides it for the build, which then\ncompiles pages against React's dev JSX runtime (jsxDEV) and every route\nfails with React's opaque \"message omitted in production builds\". The build\nrefuses this and names the file:line. A plugin cannot override it (Vite\napplies the .env value after plugins run), so remove the line - other tools\nthat want it keep it in their own .env.\n\nFull guide: read_guide({ slug: 'instrumentation' }).`,\n },\n {\n topic: 'bun',\n summary: 'Running on Bun - Vite on Bun\\'s runtime (bun --bun vite), native deps external (serverExternalPackages), and the gotchas that are Bun\\'s not ours',\n body: `The vite bin has a node shebang: \\`bun run dev\\` alone starts Vite - dev\nserver, build, prerender - under NODE, and an app importing 'bun' or\n'bun:sqlite' fails at first render with \"Cannot find package 'bun'\". Scripts\non a Bun app: dev \"bun --bun vite\", build \"bun --bun vite build\". createTestApp\nruns the project's build script on the runtime the tests use.\n\nNative dependencies (sharp, bcrypt, better-sqlite3, @prisma/client, puppeteer,\n...) are external to the server bundles by default; Nitro traces them into\n.output/server/node_modules with their binaries. Add one:\nrscKit({ serverExternalPackages: ['@acme/native'] }). Same as Next's option.\n\nThe other direction: a dependency with \"use client\" files that does NOT\ndeclare react as a peerDependency (generated wrappers, workspace packages\nwith react under dependencies) would be left external by plugin-rsc and its\ndirective never read - hooks then run on the server. The build detects direct\ndependencies in that state and bundles them, printing\n\"[rsc-kit] bundling <pkg>: it has \"use client\" files but does not declare\nreact as a peer dependency\". Nothing to configure; fix the package's\npeerDependencies when it is yours.\n\nBun's, not the framework's: bun test loads the package .env (use\n--env-file=/dev/null to isolate); Stripe's constructEvent throws on Bun\n(no sync WebCrypto) - use constructEventAsync; Bun's pg puts SQLSTATE in\nerrno where Node's pg uses code.\n\nNever NODE_ENV in a .env (the build refuses it, naming the line). Build\nmachines without secrets: SKIP_ENV_VALIDATION=1.\n\nreflect-metadata (tsyringe, typeorm, inversify - often under\n@simplewebauthn/server): nothing to import; when the graph has it the build\nloads it in a Nitro plugin ahead of the app, so the \"tsyringe requires a\nreflect polyfill\" boot error does not happen in a directory or a binary.\n\nBuild, then start - NEVER run vite build while bun/node .output/server/index.mjs\nis serving from that .output: services load lazily and a failed import of a\nhalf-written chunk is cached by the runtime (ENOENT 500s until restart).\n\nSingle binary (Bun): bun build --compile .output/server/compile.mjs\n--outfile dist/app (the scaffold's \"compile\" script). compile.mjs is written\nby the build and embeds the frozen pages; with serveStatic: 'inline' in the\nNitro plugin the assets (and their .br/.gz) are inside too. Ship dist/app\nalone - a Dockerfile copies nothing else, not .output/public. A production\napp ported from Next measured the binary image at 50.12 MiB against the\nNext image's 104.56 MiB, the docker build at 2m45s against 5m13s, and\nLighthouse at 99 mobile / 100 desktop - nothing tuned for the numbers.\n\nNothing is sent raw: a built bun/node server gzips what it answers\n(documents, streams flushed per chunk, payloads, stored pages, api routes)\nfor a request that accepts it, and the build writes .br/.gz beside every\npublic asset, served by Nitro. Nothing to configure; a Worker leaves it to\nthe platform. Off: compress: false on the handler / compressPublicAssets:\nfalse in Nitro config; Cache-Control: no-transform exempts one answer. Do\nNOT add a compression middleware or precompress assets yourself.\n\nFull guide: read_guide({ slug: 'bun' }).`,\n },\n {\n topic: 'openapi',\n summary: 'An OpenAPI document derived from route.ts files - rscKit({ openapi }) - and Scalar\\'s page over it, mounted as a route',\n body: `Do NOT hand-write an OpenAPI spec. rscKit({ openapi: true }) in\nvite.config.ts answers /openapi.json, derived from every route.ts: the\ndirectory is the path ([id] -> {id}), each method export an operation,\nparams/searchParams/body schemas the parameters and request body (Zod 4 and\nArkType describe themselves as JSON Schema; Valibot not yet), a middleware.ts\nabove a route a security requirement + 401/403. Stored at build, no middleware.\n\nDocument-level parts go on the option:\n rscKit({ openapi: { info, servers, security, components: { securitySchemes } } })\nWhat a route says about itself, beside its handler:\n export const openapi = { summary, tags, responses: { 200: {...} }, POST: { summary } }\n export const openapi = false // leave this route out (the reference page, a webhook)\n export const openapi = { DELETE: false } // one method out; HEAD/OPTIONS never documented\nWebhook-heavy app: rscKit({ openapi: { include: 'declared' } }) documents only\nroutes that export openapi, so callbacks need no opt-out line.\nResponse bodies are declared in openapi.responses until a typed helper exists.\n\nThe page: Scalar's own package, one route, nothing shipped by the engine:\n // src/app/reference/route.ts\n import { ApiReference } from '@scalar/nextjs-api-reference'\n export const GET = ApiReference({ url: '/openapi.json' })\n export const openapi = false\n\nPorting a spec file: delete its paths (they are the routes now, and body\nvalidates at runtime), move info/servers/security to the option, move a\nroute's summary/tags/responses to its openapi export.\n\nFull guide: read_guide({ slug: 'openapi' }).`,\n },\n {\n topic: 'env',\n summary: 'Typed environment variables - src/env.ts with @t3-oss/env-core in the app\\'s validation library; refused at startup by name. Never NODE_ENV in .env',\n body: `A scaffolded app has src/env.ts when it said yes to typed environment\nvariables (create-rsc-kit --env, with --validation=zod|valibot|arktype). To\nadd it to an app without one: install @t3-oss/env-core and write the same file.\n\n\\`\\`\\`ts\n// src/env.ts\nimport * as z from 'zod' // or valibot / arktype - any Standard Schema library\nimport { createEnv } from '@t3-oss/env-core'\n\nexport const env = createEnv({\n server: {\n NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),\n DATABASE_URL: z.url(),\n SESSION_SECRET: z.string().min(1),\n },\n clientPrefix: 'PUBLIC_',\n client: { PUBLIC_SITE_URL: z.url() },\n // No bare process.env: a \"use client\" file importing this for a PUBLIC_\n // value has no process, and the spread throws before the first render.\n runtimeEnv: { ...(typeof process === 'undefined' ? {} : process.env), ...import.meta.env },\n emptyStringAsUndefined: true,\n skipValidation: typeof process !== 'undefined' && !!process.env.SKIP_ENV_VALIDATION,\n})\n\\`\\`\\`\n\nRead env.DATABASE_URL, never process.env.DATABASE_URL: the first is typed and\nwas checked at startup (a missing or malformed one fails then, with its name),\nthe second is string | undefined. A server variable never reaches the browser;\na browser-readable one MUST start with PUBLIC_ and is read from import.meta.env\n(Vite; the engine registers PUBLIC_ beside VITE_ as a client prefix, nothing\nto configure), which is why runtimeEnv merges both. Commit .env.example, not .env.\n\nNext: NEXT_PUBLIC_* becomes PUBLIC_*; @t3-oss/env-nextjs becomes\n@t3-oss/env-core with runtimeEnv as above (env-nextjs's experimental__runtimeEnv\nis not needed).`,\n },\n {\n topic: 'live-data',\n summary: 'A value that keeps changing - realtime, live updates: usePolling over a query, or server-sent events (SSE, streaming from a middleware.ts generator) with useEvents - both feed TanStack, SWR or setState',\n body: `Neither is part of query() - a query answers once and is cacheable.\n\nPOLLING - start here when you have no change feed yet. Reuses the query,\ngoes through its Cache-Control (a CDN collapses many tabs into one origin\nread per interval), pauses when the tab is hidden, never overlaps two reads.\n\\`\\`\\`tsx\nimport { usePolling } from '@rsc-kit/core/usePolling'\nconst { data, status, refresh } = usePolling(() => fetchQuery(getSeats), { every: 2_000 })\n\\`\\`\\`\nUNTIL IT SETTLES - a job that ends. until(data) says the last read; onSettled\nfires once on it. The result is the DATA; what to\ndo on settling is the page's choice:\n // a server-rendered list, some jobs still running: re-render through the server\n usePolling(() => fetchQuery(jobStatus, [id]), { every: 2_000, enabled: !isTerminal(job), until: isTerminal, onSettled: () => refresh('page') })\n // the page that owns the job's state machine: the value in hand\n const { data, status } = usePolling(read, { every: 1_500, until: isTerminal, onSettled: (f) => dispatch(f.status) })\nSettled = stopped until refresh() or the inputs change. status: 'reading' |\n'paused' | 'settled' | 'idle'.\n\nSERVER-SENT EVENTS - when something can push. Better per update (bytes only\non change, instant), but holds a connection per open tab (fine on Bun/Node,\na limit on Workers or a small container), is uncacheable, and needs a source\nof change to yield from - a generator that polls the DB itself just moved the\npolling. An ordinary route.ts: beside its pages, runs middleware.ts above it.\n\\`\\`\\`ts\n// src/app/api/orders/[id]/events/route.ts\nimport { events, named } from '@rsc-kit/core/events'\nexport const GET = events(async function* ({ params, signal }) {\n const { id } = await params\n for await (const status of orderStatus(id, { signal })) yield { status }\n // yield named('paid', order, { id: order.id }) names a message / gives an id\n})\n\\`\\`\\`\n\\`\\`\\`tsx\nimport { useEvents } from '@rsc-kit/core/useEvents'\nconst { latest, all, status, close } = useEvents<Status>(\\`/api/orders/\\${id}/events\\`)\n// <Status> is the message type the route yields; a url infers nothing, so\n// without it latest is unknown. Declare the type beside the route, import both sides.\n\\`\\`\\`\nevents() frames JSON, sends a keepalive, sets text/event-stream + no-store,\nends the generator on disconnect (signal). EventSource reconnects itself and\nresumes with Last-Event-ID when you yielded ids.\n\nNO LIBRARY NEEDED. Both hooks ARE state: read data (polling) or latest\n(events) and render it. Neither needs TanStack or SWR.\n const { latest } = useEvents<Status>(url); const current = latest ?? initial // the server value until the first message\nWITH A STORE - when the value already lives somewhere, hand every value on so\nthat stays the truth: a useState, a reducer, or a cache library:\n useEvents<Order>(url, { onMessage: setOrder }) // useState\n useEvents<Order>(url, { onMessage: (m) => dispatch({ type: 'update', m }) }) // reducer\n useEvents<Order>(url, { onMessage: (m) => queryClient.setQueryData(['order', id], m) }) // TanStack\n useEvents<Order>(url, { onMessage: (m) => mutate(['order', id], m, false) }) // SWR\n usePolling(read, { every, onData: setSeats })\nSTABLE CALLBACKS: a callback a timer/subscription/listener calls that must see\nthe latest props is useEffectEvent from React (19.2+), never a ref assigned\neach render, and never in a dependency array. Both hooks are built on it.\n\nERRORS: both hooks expose error as state AND fire onError - a failed poll read\n(the next interval still reads) or a dropped stream (EventSource reconnects\nitself). Use onError for a toast/log; do NOT watch error in a useEffect.\nusePolling's onError gets (error, { failures }) - failed reads in a row, reset\nby a success - so toast on the third, not the first.\nfetchQuery(query, args) types args from the query: fetchQuery(getSeats) for a\nquery that takes nothing, fetchQuery(status, [{ id }]) refused if the query's\ninput has no id.\nDo NOT put a stream on query() or on a server action, and do NOT poll from\ninside an events() generator.\n\nFull guides: read_guide({ slug: 'queries' }) for polling, read_guide({ slug:\n'api-routes' }) for the streaming route.`,\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 // The summary is prose, so a multi-word ask is matched with its spaces back.\n RECIPES.find((r) => r.summary.toLowerCase().replace(/[-\\s]+/g, ' ').includes(wanted.replace(/-/g, ' ')))\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/guides/authorization.md
CHANGED
|
@@ -27,6 +27,26 @@ Middleware compose up the tree like layouts — outermost first — and **every*
|
|
|
27
27
|
request runs the whole chain: a full page load, a navigation, a prefetch, a
|
|
28
28
|
revalidation. There is no flag to remember; the file is the declaration.
|
|
29
29
|
|
|
30
|
+
Several checks in one directory are a list, run in the order written and
|
|
31
|
+
stopping at the first refusal — which is how a check written once is reused
|
|
32
|
+
where it is needed:
|
|
33
|
+
|
|
34
|
+
```ts title="src/app/admin/middleware.ts"
|
|
35
|
+
import { signedIn, verified } from '../../guards';
|
|
36
|
+
import { redirect } from '@rsc-kit/core/redirect';
|
|
37
|
+
|
|
38
|
+
export default [
|
|
39
|
+
signedIn,
|
|
40
|
+
verified,
|
|
41
|
+
async () => {
|
|
42
|
+
if (!(await currentUser())?.isAdmin) redirect('/');
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`src/app/middleware.ts` above it still runs first; the list is this
|
|
48
|
+
directory's own part of the chain.
|
|
49
|
+
|
|
30
50
|
<Aside type="caution" title="Do not put the check in a layout">
|
|
31
51
|
It looks like it works, and it does on a full page load. But a navigation
|
|
32
52
|
tells the server which layouts the browser already has, and the server skips
|
package/guides/deployment.md
CHANGED
|
@@ -42,6 +42,21 @@ CMD ["bun", ".output/server/index.mjs"]
|
|
|
42
42
|
The second stage carries `.output` and nothing else. On the docs application
|
|
43
43
|
that is 1.0 MB against 104 MB of `node_modules`.
|
|
44
44
|
|
|
45
|
+
<Aside type="tip" title="What a port measured">
|
|
46
|
+
A production app ported from Next, same pages, same backend, the Bun binary
|
|
47
|
+
image beside the Next one it replaced:
|
|
48
|
+
|
|
49
|
+
| | Next | rsc-kit |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| docker image | 104.56 MiB | 50.12 MiB |
|
|
52
|
+
| docker build | 5 m 13 s | 2 m 45 s |
|
|
53
|
+
| Lighthouse | — | 99 mobile, 100 desktop |
|
|
54
|
+
|
|
55
|
+
Nothing was tuned for the numbers. The image is smaller because the bundle
|
|
56
|
+
carries no `node_modules`; the build is faster because there is one build,
|
|
57
|
+
not a build and a trace.
|
|
58
|
+
</Aside>
|
|
59
|
+
|
|
45
60
|
## Or a platform, without a Dockerfile
|
|
46
61
|
|
|
47
62
|
Change the preset and Nitro produces what that platform expects — a Worker and
|
package/guides/installation.md
CHANGED
|
@@ -172,7 +172,7 @@ runs](/hosts/where-it-runs).
|
|
|
172
172
|
"dev": "bun --bun vite",
|
|
173
173
|
"build": "bun --bun vite build",
|
|
174
174
|
"start": "bun .output/server/index.mjs",
|
|
175
|
-
"compile": "bun --bun vite build && bun build --compile .output/server/
|
|
175
|
+
"compile": "bun --bun vite build && bun build --compile .output/server/compile.mjs --outfile dist/app"
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
```
|
package/guides/quick-start.md
CHANGED
|
@@ -29,7 +29,9 @@ export const env = createEnv({
|
|
|
29
29
|
},
|
|
30
30
|
clientPrefix: 'PUBLIC_',
|
|
31
31
|
client: {},
|
|
32
|
-
|
|
32
|
+
// No bare process.env: a "use client" file importing this for a PUBLIC_
|
|
33
|
+
// value has no process, and the spread would throw before the first render.
|
|
34
|
+
runtimeEnv: { ...(typeof process === 'undefined' ? {} : process.env), ...import.meta.env },
|
|
33
35
|
emptyStringAsUndefined: true,
|
|
34
36
|
})
|
|
35
37
|
```
|
package/guides/redirects.md
CHANGED
|
@@ -55,7 +55,11 @@ windows, and nothing you write chooses between them:
|
|
|
55
55
|
Neither buffers the response. Before anything is written the host is still
|
|
56
56
|
waiting on the shell, so a component that redirects instead of rendering is
|
|
57
57
|
caught there. After that, React already carries an error digest to the client
|
|
58
|
-
and the destination rides along in it
|
|
58
|
+
and the destination rides along in it, and the browser performs it as a
|
|
59
|
+
navigation — the layouts above stay mounted, the url being left is replaced
|
|
60
|
+
rather than kept in history. An `error.tsx` on the route never sees it: a
|
|
61
|
+
redirect is the page's answer, not a failure, whether it is thrown by the
|
|
62
|
+
page, by a component under its own `<Suspense>`, or by a parallel route slot.
|
|
59
63
|
|
|
60
64
|
<Aside type="caution" title="Middleware belongs above the boundaries">
|
|
61
65
|
A `loading.tsx` wraps the whole page in `<Suspense>`. That is usually what you
|
|
@@ -85,10 +89,10 @@ renders, on every path. This is the one built for the job.
|
|
|
85
89
|
layout skips the check:
|
|
86
90
|
|
|
87
91
|
```bash
|
|
88
|
-
curl -H 'X-RSC:
|
|
92
|
+
curl -H 'X-RSC: 1' -H 'X-RSC-Segments: app/layout' /guarded
|
|
89
93
|
# 204, X-RSC-Redirect: /orders ← the middleware ran
|
|
90
94
|
|
|
91
|
-
curl -H 'X-RSC:
|
|
95
|
+
curl -H 'X-RSC: 1' -H 'X-RSC-Segments: app/layout,app/guarded/layout' /guarded
|
|
92
96
|
# 200, and the page's content ← it did not
|
|
93
97
|
```
|
|
94
98
|
|
package/guides/routing.md
CHANGED
|
@@ -232,14 +232,14 @@ fails at the list rather than at the `Link` that renders it:
|
|
|
232
232
|
|
|
233
233
|
```tsx title="src/components/Nav.tsx"
|
|
234
234
|
// `satisfies` rather than a type annotation: an annotation would widen href to
|
|
235
|
-
//
|
|
235
|
+
// Route and lose which one each entry is, while this keeps the literals and
|
|
236
236
|
// still checks them — so a typo fails here, at the list, rather than at the
|
|
237
237
|
// Link that renders it.
|
|
238
238
|
const links = [
|
|
239
239
|
{ href: '/', label: 'Home' },
|
|
240
240
|
{ href: '/dashboard', label: 'Dashboard' },
|
|
241
241
|
{ href: '/posts/hello-world', label: 'A Post' },
|
|
242
|
-
] satisfies { href:
|
|
242
|
+
] satisfies { href: Route; label: string }[]
|
|
243
243
|
```
|
|
244
244
|
|
|
245
245
|
### When the value is not already a url
|
|
@@ -269,9 +269,9 @@ When a destination is computed rather than written, cast it — that is the seam
|
|
|
269
269
|
where you are telling the typechecker something it cannot know:
|
|
270
270
|
|
|
271
271
|
```ts
|
|
272
|
-
import type {
|
|
272
|
+
import type { Route } from '@rsc-kit/core/routes';
|
|
273
273
|
|
|
274
|
-
<Link href={savedPath as
|
|
274
|
+
<Link href={savedPath as Route}>Resume</Link>
|
|
275
275
|
```
|
|
276
276
|
|
|
277
277
|
Two limits. A dynamic segment widens to `${string}`, and a template literal type
|
package/guides/typed-routes.md
CHANGED
|
@@ -141,8 +141,7 @@ await fetch(apiUrl('/api/ordrs')) // does not compile
|
|
|
141
141
|
against — the function is the place.
|
|
142
142
|
|
|
143
143
|
`apiUrl('/orders')` does not compile, because fetching a page gets html where
|
|
144
|
-
json was expected — the one mistake the narrower union is for.
|
|
145
|
-
`ApiHref` are the same two types under their older names.
|
|
144
|
+
json was expected — the one mistake the narrower union is for.
|
|
146
145
|
|
|
147
146
|
:::note[Paths, not response types]
|
|
148
147
|
This checks the **url**. It does not infer what the endpoint returns — that
|
package/package.json
CHANGED