The funstackStatic() function is the main Vite plugin that enables React Server Components for building SPAs without a runtime server.
import funstackStatic from "@funstack/static";
There are two configuration modes: single-entry (one HTML page) and multiple entries (multiple HTML pages).
Use root and app to produce a single index.html:
// vite.config.ts
import funstackStatic from "@funstack/static";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
funstackStatic({
root: "./src/root.tsx",
app: "./src/App.tsx",
}),
],
});
Use entries to produce multiple HTML pages from a single project:
// vite.config.ts
import funstackStatic from "@funstack/static";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
funstackStatic({
entries: "./src/entries.tsx",
}),
react(),
],
});
See Multiple Entrypoints for a full guide.
The plugin accepts either root + app (single-entry) or entries (multiple entries). These two modes are mutually exclusive.
Type: string
Required in: single-entry mode
Path to the root component file. This component wraps your entire application and defines the HTML document structure (<html>, <head>, <body>).
Cannot be used together with entries.
funstackStatic({
root: "./src/root.tsx",
// ...
});
The root component receives children as a prop:
// src/root.tsx
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="UTF-8" />
<title>My Site</title>
</head>
<body>{children}</body>
</html>
);
}
Type: string
Required in: single-entry mode
Path to the app component file. This component defines your application's content.
Cannot be used together with entries.
funstackStatic({
root: "./src/root.tsx",
app: "./src/App.tsx",
});
// src/App.tsx
export default function App() {
return (
<div>
<h1>Welcome to My Site</h1>
{/* Your fantastic application goes here */}
</div>
);
}
Note: if your app has multiple pages, you can use a routing library here just like in a traditional SPA.
Type: string
Required in: multiple entries mode
Path to an entries module that exports a function returning entry definitions. Each entry produces its own HTML file.
Cannot be used together with root or app.
funstackStatic({
entries: "./src/entries.tsx",
});
The entries module must default-export a function that returns entry definitions:
// src/entries.tsx
import type { EntryDefinition } from "@funstack/static/entries";
export default function getEntries(): EntryDefinition[] {
return [
{
path: "index.html",
root: () => import("./root"),
app: () => import("./pages/Home"),
},
{
path: "about.html",
root: () => import("./root"),
app: () => import("./pages/About"),
},
];
}
See Multiple Entrypoints for details on the EntryDefinition type and advanced usage patterns like async generators.
Type: string
Default: "dist/public"
Output directory for the generated static files.
funstackStatic({
root: "./src/root.tsx",
app: "./src/App.tsx",
publicOutDir: "build/static",
});
Type: boolean
Default: false
Enable server-side rendering of the App component.
When false (default), only the Root shell is rendered to HTML at build time. The App component's RSC payload is fetched separately and rendered client-side using createRoot. This results in faster initial HTML delivery but requires JavaScript to display the App content.
When true, both the Root and App components are fully rendered to HTML. The client hydrates the existing HTML using hydrateRoot, which can improve perceived performance and SEO since the full content is visible before JavaScript loads.
funstackStatic({
root: "./src/root.tsx",
app: "./src/App.tsx",
ssr: true, // Enable full SSR
});
Note: In both modes, React Server Components are used - the ssr option only controls whether the App's HTML is pre-rendered or rendered client-side.
Type: string
Path to a module that runs on the client side before React hydration. Use this for client-side instrumentation like Sentry, analytics, or feature flags.
The module is imported for its side effects only - no exports are needed.
funstackStatic({
root: "./src/root.tsx",
app: "./src/App.tsx",
clientInit: "./src/client-init.ts",
});
Example client init file:
// src/client-init.ts
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "https://your-sentry-dsn",
environment: import.meta.env.MODE,
});
Note: Errors in the client init module will propagate normally and prevent the app from rendering.
Type: string
Default: "fun__rsc-payload"
Directory name used for RSC payload files in the build output. The final file paths follow the pattern /funstack__/{rscPayloadDir}/{hash}.txt.
Change this if your hosting platform has issues with the default directory name.
Important: The value is used as a marker for string replacement during the build process. Choose a value that is unique enough that it does not appear in your application's source code. The default value "fun__rsc-payload" is designed to be unlikely to collide with user code.
funstackStatic({
root: "./src/root.tsx",
app: "./src/App.tsx",
rscPayloadDir: "my-custom-rsc-payload",
});
// vite.config.ts
import funstackStatic from "@funstack/static";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
funstackStatic({
root: "./src/root.tsx",
app: "./src/App.tsx",
publicOutDir: "dist/public",
}),
],
});
// vite.config.ts
import funstackStatic from "@funstack/static";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
funstackStatic({
entries: "./src/entries.tsx",
}),
react(),
],
});
You can use the same Vite commands you would use in a normal Vite project:
vite dev - Starts the development servervite build - Builds the static filesvite preview - Previews the built static files locally