# 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