# Mandu Framework — Full Documentation > The agent-native meta-framework for Bun + React > This file concatenates all docs pages for LLM ingestion. > For the lightweight index, see /llms.txt Generated at: 2026-05-06T10:07:07.652Z Base URL: https://mandujs.com --- # Quickstart URL: https://mandujs.com/docs/start/quickstart ai-hint: Install via the standalone binary (curl/iwr) or `bun install -g @mandujs/cli`, scaffold with `mandu create my-app`, then `bun run dev` on port 3333. Routes live under `app/` as `page.tsx`. Bun >= 1.3.12 is required for the npm path. stable-since: v0.22 description: Ship a Mandu app in 5 minutes — install, scaffold, dev. Two install paths, one prompt your agent can run end-to-end. # Quickstart > **TL;DR** — install Mandu (binary or npm), `mandu create my-app`, `cd my-app && bun run dev`, http://localhost:3333. Five minutes, three commands. ## Why Mandu Other fullstack frameworks were built on the assumption that **a human types every line**. Mandu was designed from day one for **AI agents pouring out code** — Guard enforces architectural boundaries at runtime, contracts derive types/validation/OpenAPI from one file, and 100+ MCP tools talk directly to your editor's agent. That's why five minutes is enough. ## Install Mandu ships in two parallel formats. Pick the one that matches your situation. ### Path A · Standalone binary (no prereqs, ~132 MB) Bun is embedded in the binary, so there's nothing else to install. ```bash # Linux / macOS curl -fsSL https://raw.githubusercontent.com/konamgil/mandu/main/install.sh | sh # Windows (PowerShell) iwr https://raw.githubusercontent.com/konamgil/mandu/main/install.ps1 -useb | iex ``` Verify: `mandu --version`. If "command not found", restart your shell so the new `PATH` entry (`~/.mandu/bin` on Unix, `%LOCALAPPDATA%\Mandu\bin` on Windows) is picked up. ### Path B · npm / Bun package (~5 MB, requires Bun) Smaller download, but you need Bun installed. ```bash bun --version # 1.3.12 or newer required bun install -g @mandujs/cli ``` If you don't have Bun: install from [bun.sh/docs/installation](https://bun.sh/docs/installation). Node.js is **not** supported — Mandu's router, bundler, and test runner all bind to Bun APIs. ## Scaffold ```bash mandu create my-app cd my-app ``` The scaffolder asks two questions: - **Template** — `minimal` / `starter` / `agent-ready`. Pick `starter` if unsure. - **MCP agents** — pre-wire Claude Code · Claude Desktop · Gemini CLI. Add later with `mandu mcp add`. ## Run dev ```bash bun run dev # or, if you only have the standalone binary: `mandu dev` ``` Open http://localhost:3333. If the Mandu mascot waves back, you're up. ## 🤖 Agent Prompt
🤖 Agent Prompt — Bootstrap a new project
``` Bootstrap a new Mandu project. Walk me through: 1. Install Mandu — prefer the standalone binary: - Unix: curl -fsSL https://raw.githubusercontent.com/konamgil/mandu/main/install.sh | sh - Windows: iwr https://raw.githubusercontent.com/konamgil/mandu/main/install.ps1 -useb | iex (Or `bun install -g @mandujs/cli` if Bun is already installed.) 2. `mandu create my-app` (template: `starter`). 3. `cd my-app && bun run dev`. 4. Open http://localhost:3333 and confirm the welcome page renders. Diagnose using these invariants: - routes live at app/**/page.tsx (.tsx only) - .island.tsx with `use client` is required for hydration - dev port is 3333 - the npm path requires Bun >= 1.3.12 (the binary embeds Bun) Once dev is running, show me the project tree (app/, src/client, src/server, src/shared, spec/, content/, mandu.config.ts). ```
Paste this into Claude Code · Cursor · OpenAI Codex · GitHub Copilot · Gemini CLI — any AI agent — and it just works. The page's `ai-hint` and `invariants` frontmatter is picked up automatically. ## What you got After scaffolding: ```text my-app/ ├── app/ │ ├── page.tsx # the "/" route │ ├── layout.tsx # body wrapper (Mandu emits /) │ └── api/ # file-system API routes ├── src/ │ ├── client/ # UI code (FSD: shared/entities/features/widgets) │ ├── server/ # server code (domain/application/infra) │ └── shared/ # contracts and types shared across the boundary ├── spec/ # specs (contracts, decisions, sketches) ├── content/ # MDX content layer └── mandu.config.ts # framework config ``` ## Five things you must not miss - **Bun ≥ 1.3.12** for the npm path (binary embeds Bun, no prereq). - **Routes = `app/**/page.tsx` only.** `.jsx`, `index.tsx`, `.mdx` are ignored. - **Only `.island.tsx`** files hydrate. Plain `.tsx` is server-only. - **Port 3333** is the default — change via `mandu.config.ts` (`server.port`). - **Guard enforces boundaries at runtime** — if `features/*` imports the internal of `entities/*`, the dev console rejects it on the spot. ## Related - [project-structure](./project-structure) — why folders are split this way - [first-route](./first-route) — add your second route - [recipes/create-api](../recipes/create-api) — build an API endpoint (next step) - [architect/guard](../architect/guard) — what Guard catches --- # Start URL: https://mandujs.com/docs/start ai-hint: Start category hub. Entry point: quickstart. Covers install, scaffold, dev server. stable-since: v0.22 description: Install Mandu, scaffold an app, and run it locally in 5 minutes. # Start Welcome. This category gets you from zero to a running Mandu app. ## Pages - [Quickstart](/docs/start/quickstart) — scaffold and run in 5 minutes. More pages land in W2 (first route, data fetching, layouts). ## 🤖 Agent Prompt
🤖 Agent Prompt — Start
``` Apply the guidance from the Mandu docs page at https://mandujs.com/docs/start/index to my project. Summary of the page: Start category hub. Entry point: quickstart. Covers install, scaffold, dev server. Then: 1. Make the change in my codebase consistent with the page. 2. Run `bun run guard` and `bun run check` to verify nothing in src/ or app/ breaks Mandu's invariants. 3. Show me the diff and any guard violations. ```
--- # Architecture Overview URL: https://mandujs.com/docs/architect/overview ai-hint: Mandu rests on three pillars — Prerender, Island, Guard. Prerender is the default render mode, Island is the opt-in client boundary, Guard is the machine-checkable invariant layer. stable-since: v0.22 description: The three pillars that keep Mandu apps — and the agents writing them — from drifting. # Architecture Overview Mandu is built around one belief: **agents will write most of your code, and the framework has to stop them from breaking the house**. Everything else — routing, rendering, bundling — is shaped to support that outcome. Three pillars carry that weight. Each has its own deep dive; this page is a map. ## The three pillars ### 1. Prerender — static by default Every route in Mandu is statically prerendered at build time unless it explicitly opts out. No "getServerSideProps". No ambiguous "will it stream or hydrate?" Every page produces an HTML file, and that HTML is what the user sees first. Why this matters for agents: an agent generating a page doesn't have to decide between five rendering modes. There is one default, and it is the fastest one. ```tsx // app/about/page.tsx — prerendered, zero config export default function About() { return
About Mandu.
; } ``` Opt into dynamic rendering explicitly: ```tsx // app/dashboard/page.tsx export const dynamic = "force-dynamic"; export default async function Dashboard() { const user = await getCurrentUser(); return ; } ``` Deep dive: **[Prerender](/docs/architect/prerender)**. ### 2. Island — interactivity is opt-in Client-side JavaScript ships only from files that end in `.island.tsx`. Everything else — pages, layouts, shared components — is server-rendered HTML with zero hydration cost. ```tsx // src/client/widgets/counter/Counter.island.tsx "use client"; import { useState } from "react"; export function Counter() { const [n, setN] = useState(0); return ; } ``` ```tsx // app/page.tsx — server-rendered, imports an island import { Counter } from "@/client/widgets/counter/Counter.island"; export default function Home() { return (

Welcome

); } ``` The `.island.tsx` suffix is the **only** signal the bundler needs to decide what ships to the browser. No manifest file, no "use client" scattered across the tree — the filename is the contract. Deep dive: **[Island](/docs/architect/island)**. ### 3. Guard — invariants the agent can't break Guard is a static analyzer that runs on every build (and via `bun run guard` on demand). It enforces rules the framework cares about, such as: - `src/server/**` cannot be imported from `src/client/**` or `app/**`. - `page.tsx` files cannot declare `"use client"`. - `.island.tsx` files must declare `"use client"`. - API route files (`app/**/route.ts`) must export a default `Mandu.filling()` handler. When Guard fails, the build fails. The error message points to the exact file and line and explains which invariant was violated. Agents read that message and self-correct. ```text Guard FAIL src/client/widgets/chart/Chart.island.tsx:3 Invariant: no-server-in-client Imported src/server/infra/db.ts from a client island. Fix: move the data fetch into an API route or loader. ``` Deep dive: **[Guard](/docs/architect/guard)**. ## How Mandu differs from Next.js and Vite | Concern | Next.js | Vite (plain) | **Mandu** | |---|---|---|---| | Runtime | Node.js | Node.js | **Bun only** | | Default render mode | Hybrid (RSC + CSR) | CSR | **Prerender** | | Client boundary signal | `"use client"` everywhere | Ad hoc | **`.island.tsx` filename** | | Architecture enforcement | Linter plugin (optional) | None | **Guard (build-time, mandatory)** | | Router | `app/` directory | User-chosen | **`app/**/page.tsx` only** | | Agent integration | External | External | **First-party MCP server** | | Config required | Often (`next.config.js`) | Yes (`vite.config.ts`) | **Optional (`mandu.config.ts`)** | | Build output | `.next/` (mixed) | `dist/` (static) | **`dist/` (static + minimal server)** | The table simplifies — every framework can be bent into other shapes — but the **defaults** are what matter when an agent is generating code without human review. ## How the pillars compose A typical request flow in production: ```text HTTP request │ ▼ ┌──────────────┐ ┌──────────────┐ │ Prerender │──► │ Static HTML │ ← 99% of traffic stops here └──────┬───────┘ └──────────────┘ │ (dynamic routes only) ▼ ┌──────────────┐ ┌──────────────┐ │ Server │──► │ Streamed HTML│ └──────┬───────┘ └──────────────┘ │ ▼ ┌──────────────┐ ┌──────────────┐ │ Island │──► │ Hydrated JS │ ← only where marked └──────────────┘ └──────────────┘ Guard runs at build time across every arrow above. ``` - **Prerender** handles the common case (marketing, docs, dashboards with client-side data). - **Islands** handle interactivity without pulling the whole page into the client bundle. - **Guard** runs across every boundary at build time so none of the above degrades silently when 10 agents commit to the repo in one day. ## When to read which page - You're laying out a new app → **[Routing](/docs/architect/routing)**. - You're deciding where a file belongs → **[Layers](/docs/architect/layers)**. - You're tuning what the browser downloads → **[Island](/docs/architect/island)**. - You're debugging a failed build → **[Guard](/docs/architect/guard)**. --- > **Do not do this** > > - Do not try to make a page "partially interactive" by adding `"use client"` > to it. The right tool is an island. > - Do not disable Guard to unblock a build. Fix the violation; that's the > signal working as intended. > - Do not mirror Next.js patterns blindly. `getServerSideProps`, > `middleware.ts` at the route level, and `_app.tsx` do not exist in Mandu. ## 🤖 Agent Prompt
🤖 Agent Prompt — Architecture Overview
``` Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/overview to my project. Summary of the page: Mandu rests on three pillars — Prerender, Island, Guard. Prerender is the default render mode, Island is the opt-in client boundary, Guard is the machine-checkable invariant layer. Required invariants — must hold after your changes: - Every route is prerendered unless it explicitly opts into dynamic rendering - Interactivity is opt-in via .island.tsx files; nothing ships to the client by default - Guard rules are enforced at build time and fail the build on violation - src/server/** never crosses the client boundary Then: 1. Make the change in my codebase consistent with the page. 2. Run `bun run guard` and `bun run check` to verify nothing in src/ or app/ breaks Mandu's invariants. 3. Show me the diff and any guard violations. ```
--- # Rendering Modes URL: https://mandujs.com/docs/architect/rendering-modes ai-hint: Mandu supports 8 rendering modes. Pages default to prerender + island hydration. Use SSR for user-specific data, streaming SSR for slow pages with Suspense, 'use client' for fully interactive pages, partials for fine-grained hydration. stable-since: v0.22 description: Mandu blends eight rendering strategies — here's when to use each. # Rendering Modes Mandu is not one rendering strategy — it composes eight. Most pages pick a primary mode and mix in islands for interactivity. ## The eight modes | Mode | Runs | Output | Good for | |------|------|--------|----------| | **Prerender** | Build time | Static HTML | Docs, marketing, blog | | **SSR** | Per request | HTML | Auth'd pages, user data | | **Streaming SSR** | Per request | Chunked HTML | Slow dependencies, Suspense | | **Island** | Client, after SSR/prerender | JS bundle mounts on placeholder | Interactive widgets inside static pages | | **Partial** | Client, after SSR/prerender | Smaller than island | Single buttons, form fields | | **`"use client"` page** | Client | Full React app on page | Dashboards, editors, games | | **SPA navigation** | Client, on link click | Patches DOM, no reload | Multi-page feel, single-page speed | | **Edge SSR** | Per request, at edge PoP | HTML (low latency) | Global users, per-region content | ## Decision tree ``` Does the page depend on the request (user, cookies, query)? ├── NO → Prerender (fastest, zero server cost) │ └── YES → How fast does the data resolve? ├── Fast (<200ms) → SSR ├── Slow / parallel → Streaming SSR with Suspense └── Fully interactive? → "use client" page Need interactivity inside a static/SSR page? └── Use an Island (.island.tsx) or Partial ├── Many children interactive → Island └── One button, one form → Partial ``` ## Mixing modes A single Mandu route can use three modes at once: ```tsx // app/dashboard/page.tsx (server component — SSR) import ChartIsland from "./chart.island"; // <-- Island (client-hydrated) import { DeletePartial } from "./delete.partial"; // <-- Partial export default async function Dashboard() { const user = await loadUser(); // Server SSR return (

Welcome, {user.name}

{/* SSR */} {/* Island */} {/* Partial */}
); } ``` The server renders the full tree once (HTML ships to browser), then the client hydrates only the island and partial — the rest stays static. ## Default mode If you write `app/foo/page.tsx` with no annotations: - No dynamic data → **prerendered** at build time - Has `export async function generateStaticParams()` → prerendered per param - Has server-only data fetching in the component → **SSR** per request - File starts with `"use client"` → **full client-rendered** page (no prerender) Mandu picks prerender by default when possible — it's the fastest path. ## Edge vs origin Set `--target=workers | deno | vercel-edge | netlify-edge` at build time (`mandu build --target=workers`). Edge SSR runs the same code as origin SSR but closer to the user. Not every Node API works at the edge — see [Deploy to Cloudflare Workers](/docs/edge/cloudflare-workers) for the compatibility list. ## 🤖 Agent Prompt
🤖 Agent Prompt — Rendering Modes
``` Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/rendering-modes to my project. Summary of the page: Mandu supports 8 rendering modes. Pages default to prerender + island hydration. Use SSR for user-specific data, streaming SSR for slow pages with Suspense, 'use client' for fully interactive pages, partials for fine-grained hydration. Then: 1. Make the change in my codebase consistent with the page. 2. Run `bun run guard` and `bun run check` to verify nothing in src/ or app/ breaks Mandu's invariants. 3. Show me the diff and any guard violations. ```
## Related - [Prerender](/docs/architect/prerender) — the default mode in depth - [SSR and Streaming SSR](/docs/architect/ssr) — runtime server rendering - [Client rendering](/docs/architect/client-rendering) — `"use client"`, islands, partials - [Smooth navigation](/docs/architect/smooth-navigation) — SPA-style transitions --- # Prerender URL: https://mandujs.com/docs/architect/prerender ai-hint: Prerender is the default render mode. Every page route without a dynamic segment is rendered to HTML at build time into .mandu/static/**/index.html. Dynamic routes participate via generateStaticParams(). Crawl mode follows internal links to discover unlisted paths. stable-since: v0.22 description: Static-by-default rendering — how Mandu turns your routes into HTML at build time, and the tiny lever that opts out. # Prerender Prerender is the render mode Mandu assumes you want. Routes become HTML files at build time and the server does the boring work in advance, which is usually the work you wanted anyway. This page documents what actually happens when the prerender engine runs, what the knobs do, and where the artifacts land. ## The default path Every file matching `app/**/page.tsx` is a candidate. If its URL pattern has no dynamic parameters, it is prerendered unconditionally — no opt-in, no config. ```tsx // app/about/page.tsx export default function About() { return
Static. Done.
; } ``` The engine walks the routes manifest, collects patterns where `route.pattern.includes(":")` is false, and renders each one by issuing a `Request` to the fetch handler (source: `bundler/prerender.ts:69-73`). Output for `/about` lands at `.mandu/static/about/index.html`. The root route `/` lands at `.mandu/static/index.html`. The suffix is always `index.html` because clean URLs are non-negotiable (source: `bundler/prerender.ts:166-171`). ## Dynamic routes and generateStaticParams Dynamic routes — those with `[param]`, `[...param]`, or `[[...param]]` — produce a pattern that contains a colon (for example `/blog/:slug`). The engine looks for a named export: ```tsx // app/blog/[slug]/page.tsx export async function generateStaticParams() { const posts = await loadPosts(); return posts.map((p) => ({ slug: p.slug })); } export default function Post({ params }: { params: { slug: string } }) { return
{params.slug}
; } ``` For each param object returned, the engine substitutes the pattern via `resolvePattern()` — catch-all segments preserve slashes and encode each path piece individually (source: `bundler/prerender.ts:145-159`). If `generateStaticParams()` returns anything other than an array, the engine logs a warning and skips that route. If the export is missing, the route is simply not prerendered; it will render dynamically at request time instead. ## Crawl mode — discover without listing You can tell the engine to follow internal links in the HTML it just rendered and prerender those too: ```bash bun run build --crawl ``` Mechanically: after writing an HTML file, the engine pulls every `href` attribute whose value starts with `/`, drops query strings and fragments, filters out static-asset extensions (`.js`, `.css`, `.png`, `.woff2`, and friends), and adds the remaining paths to the render queue (source: `bundler/prerender.ts:174-195`). Crawl mode is idempotent — already-rendered paths skip re-render thanks to the `renderedPaths` set. It is most useful for catalog-style sites where an index page enumerates detail pages and you would rather not maintain two lists. ## Adding explicit routes If a path is neither listed in the manifest nor reachable by crawling (for example, a page you generate via `generateStaticParams` on a dynamic route you also reference elsewhere), pass it in: ```tsx // mandu.config.ts export default { prerender: { routes: ["/sitemap.xml", "/well-known/ai-plugin.json"], }, }; ``` These are merged into `pathsToRender` before any manifest-driven discovery (source: `bundler/prerender.ts:66`). ## Output layout After a successful build: ```text .mandu/static/ ├── index.html ← / ├── about/index.html ← /about ├── blog/ │ ├── index.html ← /blog │ └── hello-world/index.html ← /blog/hello-world (from generateStaticParams) └── docs/ └── architect/ └── prerender/index.html ``` Each page result carries `{ path, size, duration }` so the build reporter can show you which page cost what. Errors are collected per path and surfaced at the end — a failed render does not abort the build, but a non-zero count is treated as a build failure by the orchestrator (source: `bundler/prerender.ts:99-135`). ## Opting out: dynamic rendering The one-line escape hatch: ```tsx // app/dashboard/page.tsx export const dynamic = "force-dynamic"; ``` Routes tagged `force-dynamic` are excluded from the prerender pass and served at request time by the runtime server. Use it when the page's HTML truly depends on the request (authenticated sessions, per-user data, request headers). Do not use it to avoid learning `generateStaticParams`. ## Anti-patterns > **Do not do this.** > > 1. **Calling `fetch` inside the render path to reach your own API.** The > prerender engine runs the same fetch handler; hitting `/api/posts` from > inside a page handler creates a cycle. Read the data source directly. > 2. **Returning a promise from `generateStaticParams` that resolves to an > object (not an array).** The engine warns and skips the route — your page > silently becomes request-time rendered, and you will not notice until a > CDN miss at 3am. > 3. **Relying on `Date.now()` or `process.env.SOMETHING` inside a prerendered > page without a rebuild trigger.** The HTML freezes at build time. If the > value needs to be fresh, the route should be dynamic. ## 🤖 Agent Prompt
🤖 Agent Prompt — Prerender
``` Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/prerender to my project. Summary of the page: Prerender is the default render mode. Every page route without a dynamic segment is rendered to HTML at build time into .mandu/static/**/index.html. Dynamic routes participate via generateStaticParams(). Crawl mode follows internal links to discover unlisted paths. Required invariants — must hold after your changes: - A page route with no dynamic segment is always prerendered - A dynamic page route is prerendered only for params returned by generateStaticParams() - Output lives under .mandu/static//index.html (clean URLs) - generateStaticParams() must return an array; non-array returns are warned and skipped Then: 1. Make the change in my codebase consistent with the page. 2. Run `bun run guard` and `bun run check` to verify nothing in src/ or app/ breaks Mandu's invariants. 3. Show me the diff and any guard violations. ```
--- # SSR & Streaming SSR URL: https://mandujs.com/docs/architect/ssr ai-hint: Mandu SSR renders React components per request via renderToStream. Use for auth'd pages, user-specific content, or data that can't be known at build time. Streaming SSR uses React Suspense to ship HTML as it resolves. stable-since: v0.22 description: Render on the server per request — including chunked Suspense streams. # SSR & Streaming SSR SSR (server-side rendering) runs your components **per request** on the server, producing HTML before the browser ever sees the page. Prerender does the same thing but once, at build time; SSR is the on-demand version. ## When to use SSR Pick SSR over [prerender](/docs/architect/prerender) when: - The page depends on the **current user** (session, cookies, JWT). - Data comes from a **fast mutable source** (primary DB, feature flags). - The URL space is **too large to enumerate** at build time. - You want **fresh** data on every request (no stale build). If none of those apply, prerender is cheaper and faster. ## Basic SSR route Just fetch data in the component. Mandu infers SSR because the data is not available at build time: ```tsx // app/account/page.tsx import { getSession } from "@mandujs/core"; export default async function Account() { const session = await getSession(); if (!session) return ; const user = await db.user.findOne({ id: session.userId }); return ; } ``` No `generateStaticParams`, no `"use client"` — Mandu SSRs this route. ## Streaming SSR For pages that wait on slow data, stream the HTML as it resolves using React Suspense: ```tsx // app/feed/page.tsx import { Suspense } from "react"; export default function Feed() { return (
{/* ships immediately */} }> {/* slow DB query */} }> {/* slow external API */}
); } ``` Mandu uses `renderToStream` under the hood. The browser receives the shell first (Header + skeletons), then each Suspense boundary streams in as its data resolves. Time-to-first-byte stays near-zero even when total page takes seconds. ## Context inside a server component The `getRequestContext()` hook gives you the current request: ```tsx import { getRequestContext } from "@mandujs/core"; export default async function Page() { const ctx = getRequestContext(); const locale = ctx.request.headers.get("Accept-Language") ?? "en"; // ... } ``` Combine with the [Filling API](/docs/architect/overview) for handlers with middleware, guards, and lifecycle hooks. ## SSR at the edge Every SSR route compiles for four edge targets without code changes: ```bash mandu build --target=workers # Cloudflare Workers mandu build --target=deno # Deno Deploy mandu build --target=vercel-edge # Vercel Edge mandu build --target=netlify-edge # Netlify Edge ``` Edge SSR has the same semantics as origin SSR — the only difference is runtime APIs. Node-only packages fail at the edge; see [Edge runtimes](/docs/edge) for the compatibility matrix. ## SSR + Islands SSR and islands compose. The server renders the full tree; islands hydrate the interactive parts: ```tsx // app/article/[id]/page.tsx (SSR) import ReactionsIsland from "./reactions.island"; export default async function Article({ params }: { params: { id: string } }) { const article = await db.articles.findOne({ id: params.id }); return (

{article.title}

); } ``` Only `ReactionsIsland` ships JS to the browser. The rest of the article is static HTML over the wire. ## 🤖 Agent Prompt
🤖 Agent Prompt — SSR & Streaming SSR
``` Apply the guidance from the Mandu docs page at https://mandujs.com/docs/architect/ssr to my project. Summary of the page: Mandu SSR renders React components per request via renderToStream. Use for auth'd pages, user-specific content, or data that can't be known at build time. Streaming SSR uses React Suspense to ship HTML as it resolves. Then: 1. Make the change in my codebase consistent with the page. 2. Run `bun run guard` and `bun run check` to verify nothing in src/ or app/ breaks Mandu's invariants. 3. Show me the diff and any guard violations. ```
## Related - [Rendering modes](/docs/architect/rendering-modes) — the bigger picture - [Prerender](/docs/architect/prerender) — build-time version of SSR - [Client rendering](/docs/architect/client-rendering) — when SSR isn't enough --- # Client Rendering URL: https://mandujs.com/docs/architect/client-rendering ai-hint: Mandu has three client-render mechanisms: 'use client' page (whole page client-rendered), .island.tsx (partial hydration of a component inside a server page), and partial (even smaller than island, for single buttons). Prefer islands for most interactive UI. stable-since: v0.22 description: `"use client"` pages, `*.island.tsx` islands, and partials — when to pick which. # Client Rendering Mandu has three ways to run React on the client. They look similar but serve different purposes — picking the right one keeps pages fast. ## The three mechanisms | Mechanism | Scope | Initial HTML | Bundle size | |-----------|-------|--------------|-------------| | **`"use client"` page** | Whole route | None (CSR shell) | Large | | **Island** (`*.island.tsx`) | One component in a server page | Full page SSR/prerender, island placeholder | Per-island chunk | | **Partial** | One element (button, field) | Server-rendered then hydrates | Smallest | ## `"use client"` page A page file starting with `"use client"` runs **entirely on the client**. Mandu ships a client bundle and the browser boots React to render the whole route. ```tsx // app/editor/page.tsx "use client"; import { useState } from "react"; export default function Editor() { const [text, setText] = useState(""); return