### Community/Examples
# Community Work
- [Waku with yarn pnp](https://github.com/bysxx/waku-with-yarn-pnp) @bysxx
- [Waku with TailwindCSS v4 & Shadcn/ui & SSR pre-hydrated theme logic](https://github.com/JesseKoldewijn/waku-tw4-shadcn-starter) @JesseKoldewijn
---
### Community/Waku En Espanol
---
slug: waku-en-espanol
title: Learn Waku in Spanish
description: Comprehensive guide to learn Waku in Spanish, with real code examples and best practices
author: Ariel GonzAgΓΌer
date: 2026-01-25
tags: [guide, spanish, tutorial, learning]
---
# Learn Waku in Spanish
A comprehensive Spanish-language guide for learning Waku, the minimal React framework. This resource is based on the official documentation and includes practical examples, best practices, and real-world code samples.
## Resource
Visit the guide at: [aprenderwaku.netlify.app](https://aprenderwaku.netlify.app/)
## What's Included
This Spanish learning resource covers:
- **Complete Waku fundamentals** - Introduction to the minimal React framework
- **React Server Components (RSC)** - Understanding server and client components
- **File-based routing** - Pages, layouts, and navigation patterns
- **Data fetching** - Server-side and client-side data patterns
- **Server actions** - Mutations
- **Real code examples** - Practical implementations you can use
- **Best practices** - Production-ready patterns and recommendations
- **Deployment guides** - Deploying to various platforms
## Who Is This For?
This guide is perfect for:
- Spanish-speaking developers learning Waku
- React developers transitioning to server components
- Anyone looking for practical Waku examples in Spanish
- Teams working with Waku in Spanish-speaking regions
## Getting Started
The guide follows the official documentation, starting from basic concepts and progressing to advanced topics. Each section includes:
- Clear explanations in Spanish
- Code examples with comments
- Common patterns and use cases
- Tips and best practices
## Why This Resource?
- **Native Spanish content** - Not just a translation, but content written for Spanish speakers
- **Additional context** - Extra explanations and examples
- **Community perspective** - Real-world experiences and solutions
- **Accessibility** - Making Waku accessible to the Spanish-speaking developer community
## Links
- [Visit the Guide](https://aprenderwaku.netlify.app/)
- [Waku Official Docs](https://waku.gg/)
- [Waku GitHub](https://github.com/wakujs/waku)
---
_This is a community-created resource and is not officially maintained by the Waku team, but it follows the official documentation and best practices._
---
### Guides/Getting Started/Comparison
---
slug: comparison
title: Comparison
description: How Waku compares architecturally with other frameworks.
category: Getting Started
order: 40
---
## How to read this page
Frameworks are best compared by their durable architectural choices, not by feature checklists that change every release. All of the frameworks below are excellent at what they are designed for; the question is which model fits your project. Details reviewed in July 2026 against each framework's documentation. Always check the linked docs for the current state.
## At a glance
| | Waku | [Next.js](https://nextjs.org/docs) | [Astro](https://docs.astro.build) | [React Router](https://reactrouter.com) | [TanStack Start](https://tanstack.com/start/latest) |
| --------------------------------- | ---- | ---------------------------------- | --------------------------------- | --------------------------------------- | --------------------------------------------------- |
| React Server Components | β | β | β | π§ͺ | π§ͺ |
| Static pages with dynamic regions | β | β | β | β | β |
| Framework-managed caching | β | β | β | β | β |
| UI libraries beyond React | β | β | β | β | β |
| Deployment adapters | β | π§ͺ | β | β | β |
β supported, π§ͺ experimental, β not part of the design. React Router is compared in its framework mode; TanStack Start is powered by [TanStack Router](https://tanstack.com/router/latest). The sections below carry the substance the checkmarks cannot.
## Next.js
Next.js optimizes for an integrated application platform: the broadest feature set in the React ecosystem, with the framework owning much of the application lifecycle, including a caching and revalidation model that controls rendering from whole routes down to individual functions. Waku optimizes for the opposite trade: a small framework-owned surface with direct execution semantics, no implicit cache to reason about, and ecosystem libraries for the concerns the framework doesn't own. If you want an integrated platform, Next.js is a strong choice; if you prefer to keep more of those decisions in your own hands, Waku may be the better fit.
## Astro
Astro is optimized for content-first, multi-framework sites: static-first delivery, minimal browser JavaScript, content collections for local data, and islands from any UI library. Its server islands overlap conceptually with Waku's slices. The difference is that Waku is React-native throughout: server components, client components, layouts, pages, and slices belong to one React mental model, so a site that starts mostly static can grow meaningful server-driven application behavior without changing its UI model or adding a second component format.
## React Router
React Router is built for progressive adoption (declarative, data, and framework modes) with mature loader/action conventions, per-route SSR, prerendering, and SPA modes, and a web-standards request/response model. Its React Server Components support is currently experimental. The architectural distinction: React Router begins with routing and layers framework behavior on top; Waku begins with the React server-component model and supplies the routing needed to compose it. Data flows through route loaders there, and through component composition here.
## TanStack Start
TanStack Start, powered by TanStack Router, offers end-to-end type-safe routing, server functions, full-document SSR with streaming, and an explicit, router-centered programming model across multiple deployment targets. It shares Waku's taste for explicitness and portability. The distinction is which abstraction sits at the center: if you want an elaborate type-safe router and server-function model as the dominant abstraction, TanStack may fit you better. Waku is the stronger fit when the abstraction you want is React itself: server components as the architecture, with routing in a supporting role.
## Next Step
[Use Cases](/guides/use-cases) describes the application shapes Waku fits best, and the ones where another framework is the better call.
---
### Guides/Getting Started/Introduction
---
slug: introduction
title: What is Waku?
description: The minimal React framework built around React Server Components.
category: Getting Started
order: 20
---
## The minimal React framework
**Waku** _(wah-ku)_ or **γγ** means "frame" in Japanese, as in framework. It is the minimal React framework, built around React Server Components and server actions.
Minimal is not just about size. It means fewer hidden execution semantics: you can read a Waku route file and predict when its server code runs. Pages are prerendered at build time by default, pages you declare dynamic execute on every request, and Waku does not place an implicit cache in front of your rendering or data fetching.
## What you get
- **File-based routing.** Files in `src/pages` become routes. Layouts, dynamic segments, catch-all routes, and API routes are all part of the same convention.
- **Static and dynamic rendering in one app.** Each page, layout, and slice (an independently rendered fragment of a page) declares its own rendering mode, so a prerendered marketing page and a per-request dashboard live side by side.
- **Server and client components.** Fetch data with `await` directly in server components. Add `'use client'` where you need interactivity.
- **All React.** Waku adds no parallel programming model or component format. Skills learned in Waku are React skills, and React patterns from elsewhere work in Waku.
- **Deployment adapters.** The same application deploys to Node.js, Vercel, Netlify, Cloudflare, AWS Lambda, Deno, or Bun without changing how you write React.
## A route at a glance
```tsx
// ./src/pages/index.tsx
export default async function HomePage() {
const posts = await getPosts();
return (
{posts.map((post) => (
{post.title}
))}
);
}
```
This is a complete Waku page: a server component that fetches its own data. It is prerendered at build time by default; adding a `getConfig` export with `render: 'dynamic'` makes it execute on every request instead.
## Next Step
Curious why Waku is designed this way? Read the [Philosophy](/guides/philosophy).
---
### Guides/Getting Started/Philosophy
---
slug: philosophy
title: Philosophy
description: The principles behind Waku's design.
category: Getting Started
order: 30
---
## Minimal is the feature
Waku keeps its API surface small enough to hold in your head: files in `src/pages` become routes, `getConfig` declares how a route renders, `'use client'` marks the interactive boundary, and server actions handle mutations. Everything else is React.
A small surface is not a limitation. It means less framework-specific knowledge between you and your application, and fewer places where behavior needs explaining.
## Explicit over implicit
Waku's rendering model is a set of declarations, not heuristics:
- **Static is the default.** Pages and layouts are prerendered at build time unless you declare them dynamic. Static output is a build artifact: it stays the same until the next build replaces it. A rebuild is not "cache invalidation"; it is producing a new artifact.
- **Dynamic means every request.** A route declared `render: 'dynamic'` executes on each request and sees current data.
- **No implicit caching.** Waku does not put a cache in front of dynamic rendering or data fetching. What you fetch is what you render. There is no framework revalidation model to learn, because there is nothing to invalidate until you deliberately add a cache yourself.
In short: **static at build time, fresh at request time, cache explicitly.**
The result is a simple render model. Code runs in one of three places, and every piece of the app clearly belongs to one of them:
1. **At build time.** Static pages and layouts execute once during the production build.
2. **On the server, per request.** Dynamic pages and layouts, server actions, and API routes.
3. **In the browser.** Client components (which also render once on the server to produce the initial HTML).
Because the model is small, you rarely have to ask "when does this code actually run?" The answer is in the file you are looking at.
## Extensible without compiler magic
Waku's capabilities grow through libraries, not compiler plugins. The compilers in a Waku app belong to React and Vite; Waku itself avoids adding compile-time magic, so extending it means writing ordinary code.
The best evidence is Waku itself: `waku/router`, with its pages, layouts, and file conventions, is a library built on top of the Waku core. Extensions can take the same shape at any scale: a published package, a private shared library, or a module inside your repository. Because they are ordinary code, they compose, version, and debug like ordinary code.
## Next Step
See how these choices play out against other frameworks in the [Comparison](/guides/comparison).
---
### Guides/Getting Started/Quick Start
---
slug: quick-start
title: Quick Start
description: Scaffold a new Waku project and run it locally in five minutes.
category: Getting Started
order: 10
---
## Prerequisites
- [Node.js](https://nodejs.org) `^26.0.0`, `^24.0.0`, or `^22.15.0`
- Any package manager (the commands below use `npm`)
## Scaffold a project
Run the following command in your terminal:
```sh
npm create waku@latest
```
Follow the CLI prompts. The default project name is `waku-project`. Then install dependencies and start the development server:
```sh
cd waku-project
npm install
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) in your browser. You should see a small demo app with an interactive counter.
## Project structure
The starter contains the following files:
```text
waku-project/
βββ public/ # static assets served as-is
βββ src/
β βββ components/ # reusable React components
β βββ middleware/ # optional server middleware
β βββ pages/ # file-based routes
β β βββ _layout.tsx
β β βββ about.tsx
β β βββ index.tsx
β βββ global.d.ts
β βββ styles.css
βββ package.json
βββ tsconfig.json
βββ waku.config.ts
```
- `src/pages` is the router: each file becomes a route, and `_layout.tsx` wraps the pages next to and below it.
- `src/components` holds regular React components that pages import.
- `src/middleware` contains optional [Hono](https://hono.dev) middleware that the default setup picks up automatically. The starter ships one that removes trailing slashes.
- `public` files are copied to the site root unchanged, such as images and fonts.
- `waku.config.ts` configures Waku and Vite. The starter enables Tailwind CSS and React Compiler plugins.
While the development server is running, Waku also generates `src/pages.gen.ts` containing route types for type-safe links. It is regenerated automatically whenever your pages change, so you never edit it by hand.
## Make your first edit
Open `src/pages/index.tsx`:
```tsx
// ./src/pages/index.tsx (excerpt)
export default async function HomePage() {
const data = await getData();
return (
{data.title}
{data.headline}
{data.body}
About page
);
}
```
The content comes from a `getData` function defined below the component. Change its `body` text and save. The browser updates instantly without a full reload.
## Commands
- `npm run dev` starts the development server at [http://localhost:3000](http://localhost:3000)
- `npm run build` creates a production build in `dist`
- `npm run start` serves the production build at [http://localhost:8080](http://localhost:8080)
## Next Step
Read [What is Waku?](/guides/introduction) for a tour of what the framework offers.
---
### Guides/Getting Started/Use Cases
---
slug: use-cases
title: Use Cases
description: Where Waku fits well, and where another framework may serve you better.
category: Getting Started
order: 50
---
## What fit means here
Whether Waku fits is a question about the architecture you want (how much of the application the framework should own), not about the size of your company or your traffic. Waku's rendering granularity is per page, per layout, and per slice, so the shapes below are about how much of your UI is stable versus request-dependent, and who you want managing the difference.
## Strong fits
- **Content, marketing, documentation, and editorial sites** with some dynamic or personalized surfaces. The stable majority is prerendered at build time; the dynamic parts execute per request.
- **Headless commerce.** Catalog and editorial pages are static; inventory, pricing, cart, account, and recommendation surfaces are dynamic and always fresh, with no cache invalidation choreography between your commerce backend and your frontend framework.
- **Full-stack React products** that want React Server Components and server actions without adopting a framework-owned data lifecycle. You choose your database client, auth approach, and (if ever needed) caching.
- **Teams that value deployment portability.** Agencies shipping to whatever host each client uses, and products that don't want architecture coupled to a platform.
- **Framework and platform authors** who need a thin React Server Components substrate to build on, rather than a competing opinion stack.
## Weaker fits
Waku is intentionally not everything. Consider a more batteries-included framework if:
- You want the framework to own **integrated solutions** for authentication, image processing, internationalization, analytics, and similar concerns, rather than assembling ecosystem libraries.
- Your application's core requirement is a **framework-managed distributed cache and revalidation platform**. Waku treats caching as an explicit, application-owned optimization, not a built-in subsystem.
- You want a **single vendor's platform** to manage the entire application lifecycle from framework through hosting.
None of these are failures of scale; Waku serves small sites and large ones. They are differences in how much framework you want.
## Next Step
Ready to build? The Learn series begins with [The Mental Model](/guides/concept).
---
### Guides/Integrations/Explicit Caching
---
slug: explicit-caching
title: Explicit Caching
description: Why caching is outside Waku's core, and waku-cache as one solution.
category: Integrations
order: 10
---
## Rendering in Waku
Waku's rendering model has two modes and no hidden cache:
| Rendering | When server code runs | Freshness | Invalidation |
| --------- | --------------------- | -------------------------- | --------------------------------------- |
| Static | At build time | Fixed until the next build | None; a rebuild produces a new artifact |
| Dynamic | On every request | Fresh by default | None needed; nothing is cached |
Static pages and layouts are prerendered into build artifacts that a deploy replaces wholesale. Dynamic ones execute on every request, and what you fetch is what you render. Waku does not place a cache in front of either, so there is no framework revalidation model to learn: when your data changes, the next request sees it. The [Learn series](/guides/static-and-dynamic-rendering) covers this model hands-on.
## Caching is outside the core
That is a deliberate boundary, not a missing feature. A built-in cache would give the framework its own data lifecycle, with keys, lifetimes, and invalidation rules that every app inherits whether it needs them or not. Waku keeps the core minimal and its execution semantics visible (see [Philosophy](/guides/philosophy)), and leaves caching to ordinary libraries, the same way `waku/router` itself is a library on top of the Waku core.
Practically, this means:
1. Correctness never depends on a cache. Start without one.
2. Measure before caching. Look for repeated, expensive work: hot database queries, slow upstream APIs, costly renders.
3. Choose the tool that fits: HTTP and CDN caching for whole responses, memoization you write yourself, or a caching library. Whatever you pick, you own its policy, and removing it changes performance, never correctness.
## waku-cache, one solution
[`waku-cache`](https://github.com/wakujs/waku-cache) is one library that fills this space: a separate, still-evolving package that caches exactly what you wrap and nothing else. It offers two primitives, caching a function and caching an RSC subtree, over a small swappable storage interface.
Create one cache instance for your app:
```ts
// ./src/lib/cache.ts
import { createCache } from 'waku-cache';
import { memoryStore } from 'waku-cache/stores/memory';
export const cache = createCache({
store: memoryStore(),
defaults: { ttl: 60_000 },
});
```
The memory store is process-local, so a multi-instance deployment needs a shared store instead.
### Cache a function
Wrap an async function; the call signature is unchanged, so it is a drop-in:
```ts
import { cache } from './lib/cache.js';
const getProduct = cache.fn(
async (id: string) => db.products.findUnique({ where: { id } }),
{
key: (id) => ['product', id],
ttl: 5 * 60_000,
},
);
const product = await getProduct('abc'); // identical to the unwrapped call
```
The key is a function of the arguments, and concurrent misses for the same key are single-flighted within a process.
### Cache an RSC subtree
Wrap a server component; its rendered subtree is serialized once and replayed on later requests:
```tsx
import { cache } from '../lib/cache.js';
const ProductCard = async ({ id }: { id: string }) => {
const product = await getProduct(id);
return {product.name};
};
const { Component, getEtag } = cache.rsc(ProductCard, {
key: ({ id }) => ['product', id, 'card'],
ttl: 60_000,
});
export default Component;
```
The returned `getEtag` is optional; the section below explains what it is for.
### Keys and invalidation
Keys are arrays of parts, and invalidation matches by prefix, so one call can drop a whole group of entries:
```ts
await cache.invalidate({ key: ['product', 'abc'] }); // one product and its card
await cache.invalidate({ key: ['product'] }); // every product entry
```
A common place to call `invalidate` is wherever you handle a mutation, such as a server action or an API route. This is the only invalidation in a Waku app, and it exists only because you added the cache.
Include every value that can affect the output in the cache key: the user, tenant, locale, permissions, and anything else read from request context rather than from arguments or props. The cache returns entries purely by key, so a missing dimension replays one request's output to another, and a process-local store does not make this safe. Do not cache request-specific or sensitive subtrees unless the key safely scopes them. The same applies to `cache.fn` when the wrapped function depends on context that is not part of its arguments.
## Etags: skipping unchanged payloads
The client keeps a small cache of etags for the elements it currently holds. Each slot in an RSC response (a page, a layout, a slice) can carry an etag; the client remembers them and sends them back with the next navigation fetch in the `X-Waku-Etags` header. The server compares them and omits any slot whose etag still matches, so the response carries only what changed, and the client keeps the elements it already has for the omitted slots.
Static slots are marked immutable, which is how the router knows it can always reuse them. Dynamic slots have no etag by default, so they are always re-sent: without one, the server cannot know whether the content changed.
A dynamic route can opt in through `getConfig`'s `unstable_getEtag`, and this is where waku-cache fits: `cache.rsc` returns a `getEtag` that hashes the cached subtree's serialized bytes. The tag stays stable while the cached bytes are unchanged; after invalidation or TTL expiry the entry is regenerated, and the tag changes only when the serialized content changes:
```tsx
export const getConfig = () => {
return {
render: 'dynamic',
unstable_getEtag: getEtag, // delete this line and the render cache still works
} as const;
};
```
The two savings compose: `cache.rsc` saves the server from re-rendering the subtree, and the etag saves the network from re-sending it. See the [waku-cache README](https://github.com/wakujs/waku-cache#readme) for the details.
## What stays uncached
During client-side navigation, the router reuses prefetched RSC payloads so that moving between pages is fast. This is a browser-side navigation optimization: it does not create a server data cache. Static payloads can be reused freely because they change only with a deploy. An explicitly prefetched dynamic response may also be reused until its prefetch TTL expires (60 seconds by default), so a navigation shortly after a prefetch may not issue a new request; whenever a request does reach the server, it renders fresh. See [Navigation and Prefetching](/guides/navigation-prefetching) for the details and the experimental tuning APIs.
Beyond the application, other caches exist with their own controls: the browser cache and CDNs (driven by HTTP headers), static build output (typically served with long-lived caching by hosts), and database drivers or API SDKs that cache internally. Waku does not control these caches; they can affect whether a request reaches your server at all, and what data your dependencies return when it does.
## Common misconceptions
- **"Dynamic means uncached."** Dynamic means uncached _by Waku_. You can still cache expensive work explicitly, and infrastructure layers can cache responses if you tell them to.
- **"Static output is a cache."** Static output is a build artifact. It has no keys, no TTL, and no invalidation API; it is replaced wholesale by the next build.
- **"Client navigation reuse makes server data stale."** It only affects what the browser shows during navigation; the server's answer to any request it receives is computed fresh.
- **"Server components run once."** Static server components execute at build time; dynamic server components execute on every request. Neither runs in the browser.
- **"Every framework needs a revalidation model."** An invalidation responsibility appears when a cache is added. Waku without an explicit cache has nothing to invalidate.
---
### Guides/Integrations/State Across Client Server
---
slug: state-across-client-server
title: State Across Client and Server
description: Share selected client-module values with server code using unstable_allowServer.
category: Integrations
order: 30
tags: [Experimental]
---
## When to Use `unstable_allowServer`
Sometimes a value must be declared in a client module, but server code still needs to import that one value. `unstable_allowServer` marks a specific export from a `'use client'` module as safe for Waku's server build.
Use it for shared definitions, not live state:
- Jotai atom definitions that must be colocated with a client component
- small constants used by both server and client code
- pure factory results that do not touch browser APIs
Do not use it for:
- React components
- hooks
- functions that read `window`, `document`, `localStorage`, or browser-only globals
- mutable singletons that should be request-scoped
- secrets, database clients, or server-only resources
- a shortcut to import an entire client module from server code
The API is highly experimental and may change.
## Colocated Jotai Atom
This example follows the pattern used by the Waku Jotai examples. The atom is declared in the same client module as the component, and only the atom export is marked as server-safe.
```tsx
// src/components/counter.tsx
'use client';
import { useTransition } from 'react';
import { atom, useAtom } from 'jotai';
import { unstable_allowServer as allowServer } from 'waku/client';
export const countAtom = allowServer(atom(1));
export const Counter = () => {
const [count, setCount] = useAtom(countAtom);
const [isPending, startTransition] = useTransition();
const increment = () => {
startTransition(() => {
setCount((count) => count + 1);
});
};
return (
);
};
```
Because `countAtom` is wrapped with `allowServer(...)`, server code can import that one export from the client module and use it with the Jotai store:
```tsx
// src/pages/index.tsx
import { getStore } from 'waku-jotai/router';
import { Counter, countAtom } from '../components/counter';
export default async function Page() {
const store = await getStore();
return (
<>
Initial count: {store.get(countAtom)}
>
);
}
export const getConfig = async () => {
return {
render: 'dynamic',
} as const;
};
```
For file-system-router apps, wrap the route tree with `RouterProvider`:
```tsx
// src/pages/_layout.tsx
import type { ReactNode } from 'react';
import { RouterProvider } from 'waku-jotai/router';
export default function RootLayout({ children }: { children: ReactNode }) {
return {children};
}
export const getConfig = async () => {
return {
render: 'dynamic',
} as const;
};
```
Every page or layout that reads `getStore()` or mounts `RouterProvider` must render dynamically: the store is request-scoped and the atom values arrive with each RSC request, so a static render would bake in build-time values.
If you use the Minimal API, use `Provider` and `getStore` from `waku-jotai/minimal`. Unlike `RouterProvider`, this `Provider` needs the RSC request identity as props:
```tsx
{children}
```
## Prefer a Neutral Module When Possible
If the shared definition does not need to live in a client module, put it in a module without a `'use client'` directive. React Server Components can import ordinary server-safe modules without `unstable_allowServer`.
For example, a Jotai atom definition can often live in a shared module:
```ts
// src/state/count.ts
import { atom } from 'jotai/vanilla';
export const countAtom = atom(1);
```
Then client components can import that shared value from a client module:
```tsx
// src/components/counter.tsx
'use client';
import { useAtom } from 'jotai';
import { countAtom } from '../state/count';
export const Counter = () => {
const [count, setCount] = useAtom(countAtom);
return (
);
};
```
Server code can also import `countAtom` from `src/state/count.ts` if it has a real server-side use for the atom definition. Use this pattern when you can because it keeps the client/server boundary obvious.
## What Waku Transforms
`unstable_allowServer` is an identity function at runtime. Its main purpose is to tell Waku's Vite plugin which expression from a client module should remain importable in the RSC build.
In the RSC environment, Waku transforms a `'use client'` module so that:
- the wrapped `allowServer(...)` export is preserved
- dependencies used by that wrapped expression are preserved
- other exports are not made callable server functions
- the wrapper call itself is removed from the emitted server-side expression
This means the server can import `countAtom`, but it does not make every export in `counter.tsx` server-safe.
`unstable_allowServer` must receive exactly one argument:
```tsx
export const countAtom = allowServer(atom(1));
```
## What It Does Not Do
`unstable_allowServer` itself only gives the atom a shared, server-safe identity. When the waku-jotai provider is mounted, wrapped atom values are synchronized from the client to the server during RSC refetches: the client subscribes to the allowed atoms and refetches the route with their current values, which the server reads through the request-scoped store. The synchronization is one way; the server does not independently push atom updates back to the browser.
It also does not make browser-only code safe on the server. This is unsafe:
```tsx
'use client';
import { unstable_allowServer as allowServer } from 'waku/client';
export const theme = allowServer(localStorage.getItem('theme'));
```
The server has no `localStorage`, so this module cannot be evaluated safely in the RSC environment.
## Safer Alternatives
Before using `unstable_allowServer`, consider these alternatives:
- Move shared definitions to a neutral module without `'use client'`.
- Pass serializable values from server components to client components as props.
- Put request-specific data in request context or provider state.
- Keep browser-only behavior inside client components and hooks.
- Use provider boundaries to initialize client state from server-rendered values.
Use `unstable_allowServer` only for the narrow case where a client module must expose one server-safe definition and moving that definition to a neutral module is worse for the integration.
---
### Guides/Learn/Building For Production
---
slug: building-for-production
title: Building for Production
description: Build the app, inspect what got prerendered, and watch the rendering model hold.
category: Learn
order: 50
---
## Build
Stop the dev server and run:
```sh
npm run build
```
The build bundles the client and server code, then executes every static page and layout to prerender them. The result lands in `dist`:
```text
dist/
βββ public/ # served as static files
β βββ index.html
β βββ about/index.html
β βββ blog/index.html
β βββ blog/hello-waku/index.html
β βββ blog/server-components/index.html
β βββ built/index.html
β βββ contact/index.html
β βββ RSC/ # prerendered payloads for client-side navigation
β βββ assets/ # JS and CSS bundles
β βββ ... # plus everything copied from public/
βββ server/ # the server that renders dynamic routes
```
You can read the rendering model straight out of this listing:
- Every static page is now an HTML file, including one page per `staticPaths` entry of the blog post route.
- `/now` is nowhere in `dist/public`. A dynamic page has no build-time output; it lives in `dist/server` and executes when requested.
- The `RSC` directory holds the prerendered payloads that make client-side navigation between static pages fast.
## Run it
```sh
npm run start
```
Open [http://localhost:8080](http://localhost:8080) and check the two pages from the [previous chapter](/guides/static-and-dynamic-rendering):
- `/built` shows the moment the build ran, and reloading never changes it. The page executed once, during `npm run build`; you are now being served its artifact.
- `/now` shows a new timestamp on every reload. The page executes on each request, and Waku does not implicitly cache the result.
This is the whole freshness model, observed: **static at build time, fresh at request time**. To update prerendered content, such as a blog post in `src/lib/posts.ts`, run `npm run build` again: a rebuild produces new artifacts.
## Deploy
The starter uses Waku's default adapter, which targets Node.js and automatically switches when the build runs on Vercel, Netlify, or Cloudflare. Other adapters cover AWS Lambda, Deno, and Bun. Fully static sites can skip the server entirely and deploy `dist/public` to any static host.
See the deployment guides for specifics:
- [Static Deployments](/guides/static-deployments)
- [Cloudflare](/guides/cloudflare)
- [AWS Lambda](/guides/aws-lambda)
- [Docker](/guides/docker)
## Where to go from here
You have now seen every idea from [The Mental Model](/guides/concept) working: file-based routing, server and client components, declared rendering modes, and a production build that honors them.
- The [guides](/guides) cover specific capabilities: styling, metadata, request context, middleware, and more.
- The [waku-examples](https://github.com/wakujs/waku-examples) repository has focused, runnable examples for common patterns.
- The [API reference](https://waku.gg/#routing) documents the full routing convention set, server actions, and API routes.
---
### Guides/Learn/Concept
---
slug: concept
title: The Mental Model
description: The five ideas that explain every Waku app.
category: Learn
order: 10
---
## Five ideas
Everything you will build in this series follows from five ideas. Hold onto these and the rest of Waku is detail.
### 1. Server components first
Components are server components by default. They execute on the server, can be `async`, and can fetch data, read files, or query databases directly. Their output, not their code, is sent to the browser. When you need interactivity, you add `'use client'` to a file; that module and everything it imports, directly or transitively, become client code running in the browser.
### 2. Static first
Every page and layout is prerendered at build time by default. If a page should instead reflect the current request (current data, cookies, headers), you declare it dynamic with `getConfig`. Static output is a build artifact: it stays exactly the same until the next build replaces it.
### 3. Fresh at request time
A dynamic page executes on every request, and Waku does not implicitly cache the result or the data fetched inside it. When your data changes, the next request sees it. There is no revalidation API to call because there is no hidden cache to invalidate.
### 4. All React, small surface
Waku adds conventions, not a parallel programming model. Routing is files in `src/pages`, navigation is the `` component, rendering mode is a `getConfig` export, and mutations are React server actions. Data fetching is `await` inside a server component; there is no framework data layer between you and your sources.
### 5. Portable across runtimes
The same application deploys through adapters to Node.js, Vercel, Netlify, Cloudflare, AWS Lambda, Deno, and Bun. The React programming model does not change per host; only runtime-specific capabilities (like filesystem access) vary.
## Where code runs
| Code | Where it runs | When it runs |
| ------------------------- | ---------------------- | ------------------------- |
| Static page or layout | Server (build machine) | Once, at build time |
| Dynamic page or layout | Server | On every request |
| Server action / API route | Server | When invoked |
| Client component | Browser | On render and interaction |
One nuance: client components also execute once on the server per page render to produce the initial HTML (server-side rendering), then hydrate in the browser.
## What this series proves
Each chapter demonstrates one of these ideas hands-on, continuing the project from the [Quick Start](/guides/quick-start):
1. [Pages and Layouts](/guides/pages-and-layouts): routing with files
2. [Server and Client Components](/guides/server-and-client-components): the component model and the `'use client'` boundary
3. [Static and Dynamic Rendering](/guides/static-and-dynamic-rendering): declaring rendering modes and observing freshness
4. [Building for Production](/guides/building-for-production): seeing the model hold in a real build
---
### Guides/Learn/Pages And Layouts
---
slug: pages-and-layouts
title: Pages and Layouts
description: Add routes, nest layouts, and link between pages with the file-based router.
category: Learn
order: 20
---
## Files become routes
This chapter continues the project from the [Quick Start](/guides/quick-start). Everything inside `src/pages` follows the router's conventions. The starter already contains:
- `src/pages/index.tsx` β `/`
- `src/pages/about.tsx` β `/about`
- `src/pages/_layout.tsx` β the root layout wrapping every page
There are two equivalent ways to define a page: a named file (`contact.tsx`) or an index file in a directory (`contact/index.tsx`). Both render at `/contact`.
## Add a page
Create `src/pages/contact.tsx`:
```tsx
// ./src/pages/contact.tsx
import { Link } from 'waku';
export default async function ContactPage() {
return (