This example demonstrates a multi-page static site built with Rsbuild and React Server Components (RSC). It showcases file-based page routing, static site generation (SSG), client-side hydration, and client-side navigation between pages — all powered by rsbuild-plugin-rsc.
# Start development server
pnpm dev
# Build for production (includes static HTML generation)
pnpm build
# Preview the production build
pnpm previewsrc/
├── pages/ # Page components (file-based routing)
│ ├── Index.tsx
│ └── Other.tsx
├── components/ # Shared components
│ ├── Counter.tsx # Client component
│ ├── Nav.tsx # Server component
│ └── style.css
└── framework/ # Framework layer
├── entry.rsc.tsx # RSC entrypoint (server)
├── entry.ssr.tsx # SSR entrypoint (server)
├── entry.client.tsx # Client entrypoint (browser)
├── request.tsx # RSC/SSR request routing
├── shared.tsx # Shared types
└── ssg.tsx # Page types
generate.mjs # Post-build static generation script
After pnpm build, the dist/ directory contains a fully static site:
dist/
├── index.html # Pre-rendered HTML for /index
├── other.html # Pre-rendered HTML for /other
├── index_.rsc # RSC payload for client-side navigation
├── other_.rsc # RSC payload for client-side navigation
├── static/
│ ├── js/ # Client JavaScript bundles
│ └── css/ # Client CSS bundles
└── server/
└── index.js # Server bundle (used during generation only)
Each file under src/pages/ represents a page. Pages are React Server Components that render the full <html> document tree using the "use server-entry" directive. The file name determines the route (e.g., Index.tsx → /index, Other.tsx → /other).
'use server-entry';
import { Counter } from '../components/Counter';
import { Nav } from '../components/Nav';
export default function Index({ pages, currentPage }: PageProps) {
return (
<html lang="en">
<head><title>Static RSC</title></head>
<body>
<h1>This is an RSC!</h1>
<Nav pages={pages} currentPage={currentPage} />
<Counter />
</body>
</html>
);
}Pages are automatically discovered at build time via import.meta.webpackContext in entry.rsc.tsx, so adding a new .tsx file to src/pages/ is all that's needed to create a new route.
Server components run only on the server. Nav renders a list of links for all pages, highlighting the current page via aria-current.
Client components use the "use client" directive and run in the browser. Counter demonstrates interactive state with useState.
The RSC entrypoint runs in the react-server-components layer. It:
- Discovers all pages under
src/pages/usingimport.meta.webpackContext - Exports
getStaticPaths()to list all routes for static generation - Exports
renderStaticPage(route)andrenderStaticRsc(route)for SSG - Provides a dev server handler for development mode
CSS and JS bootstrap files are automatically resolved via entryCssFiles and entryJsFiles injected by the RSC plugin on "use server-entry" components.
The SSR entrypoint consumes the RSC stream and renders it to an HTML stream using react-dom/server. It supports both SSG (via prerender()) and runtime SSR (via renderToReadableStream()). The RSC payload is injected into the HTML via rsc-html-stream for seamless client hydration.
The client entrypoint hydrates the server-rendered HTML using the embedded RSC payload. It also implements a simple client-side router that:
- Intercepts link clicks to perform client-side navigation
- Fetches pre-generated RSC payloads for new pages (via
_.rscURL convention) - Updates the page without a full browser reload
The rsbuild.config.ts configures two environments and a static generation plugin:
- server: Builds
entry.rsc.tsxwith the RSC layer, outputs todist/server/ - client: Builds
entry.client.tsxfor browser hydration and navigation - pluginStaticGenerate: Runs
generate.mjsafter build to produce static HTML and RSC payloads
import { Layers, pluginRSC } from 'rsbuild-plugin-rsc';
export default defineConfig({
plugins: [
pluginReact(),
pluginRSC({
layers: {
ssr: path.join(import.meta.dirname, './src/framework/entry.ssr.tsx'),
},
}),
pluginStaticGenerate(),
],
environments: {
server: {
source: {
entry: {
index: {
import: './src/framework/entry.rsc.tsx',
layer: Layers.rsc,
},
},
},
output: {
distPath: { root: 'dist/server' },
},
},
client: {
source: {
entry: {
index: './src/framework/entry.client.tsx',
},
},
},
},
});- Rsbuild builds both the server bundle and client assets
- The
pluginStaticGenerateplugin runsgenerate.mjsafter the build generate.mjsloads the server bundle and callsgetStaticPaths()to discover all routes- For each route, it generates:
- An HTML file with pre-rendered content, embedded RSC payload, and client bootstrap scripts/CSS
- An RSC payload file (
_.rsc) for client-side navigation
- The result is a fully static site that can be served by any static file server
- Browser requests a URL (e.g.,
/index.html) - The browser receives pre-rendered HTML with embedded RSC payload
- Client JavaScript hydrates the page using the embedded RSC payload
- User clicks a link (e.g., from
IndextoOther) - The client router intercepts the click and calls
history.pushState - The client fetches the pre-generated RSC payload for the new page (e.g.,
/other_.rsc) - React updates the page in-place without a full reload