By default, FUNSTACK Static produces a single index.html from one root + app pair. The multiple entries feature lets you produce multiple HTML pages from a single project, targeting SSG (Static Site Generation) use cases where a site has distinct pages like index.html, about.html, and blog/post-1.html.
Use the entries option when you want to build a multi-page static site where each page is more like a self-contained HTML document.
root + app): One HTML file, client-side routing between pages. Best for app-like experiences where dynamic data loading and client-side interactivity are heavily used, and SEO is less of a concern (e.g., dashboards, web apps).entries): Multiple HTML files, each independently pre-rendered. Best for content sites (blogs, docs, marketing pages) where SEO and fast initial load are priorities. Client-side routing is still possible by using a router library with SSR support.Instead of root and app, pass an entries path:
// 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(),
],
});
The entries module is run in the server environment at build time. It must default-export a function that returns an array of entry definitions (async functions are also supported):
// 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"),
},
];
}
The root and page components work exactly the same as in single-entry mode:
// 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>
);
}
// src/pages/Home.tsx
export default function Home() {
return (
<main>
<h1>Home Page</h1>
<a href="/about">About</a>
</main>
);
}
// src/pages/About.tsx
export default function About() {
return (
<main>
<h1>About Page</h1>
<a href="/">Home</a>
</main>
);
}
Each entry in the array is an EntryDefinition object imported from @funstack/static/entries:
import type { EntryDefinition } from "@funstack/static/entries";
Type: string
The output file path relative to the build output directory. Must include file extensions and must not start with /.
{
path: "index.html", // -> /index.html
path: "about.html", // -> /about.html
path: "blog/post-1.html", // -> /blog/post-1.html
}
The path specifies the exact output file name. The dev and preview servers handle mapping URL paths to these file names automatically (e.g., a request to /about finds about.html).
Type: MaybePromise<RootModule> | (() => MaybePromise<RootModule>)
The root component module. Accepts either a lazy import or a synchronous module object:
// Lazy import (recommended for memory efficiency)
root: () => import("./root"),
// Synchronous module object
import * as Root from "./root";
root: Root,
The module must have a default export of a component that accepts children.
Type: ReactNode | MaybePromise<AppModule> | (() => MaybePromise<AppModule>)
The app content for this entry. Accepts a module (sync or lazy), or a React node for direct rendering:
// Lazy import
app: () => import("./pages/Home"),
// Synchronous module object
import * as Home from "./pages/Home";
app: Home,
// React node (server component JSX)
app: <BlogPost slug="hello-world" />,
The React node form is especially useful for parameterized SSG, where each entry renders the same component with different data.
For sites with many pages generated from external data, use an async generator to stream entries without building the full array in memory:
// src/entries.tsx
import type { EntryDefinition } from "@funstack/static/entries";
import * as Root from "./root";
import { readdir } from "node:fs/promises";
export default async function* getEntries(): AsyncGenerator<EntryDefinition> {
// Static pages
yield {
path: "index.html",
root: Root,
app: () => import("./pages/Home"),
};
// Dynamic pages generated from the filesystem
for (const slug of await readdir("./content/blog")) {
const content = await loadMarkdown(`./content/blog/${slug}`);
yield {
path: `blog/${slug.replace(/\.md$/, ".html")}`,
root: Root,
app: <BlogPost content={content} />,
};
}
}
The entries function runs in the RSC environment at build time, so it has access to Node.js APIs like fs, making it straightforward to generate pages from files, a CMS, or a database.
Given entries with paths index.html, about.html, and blog/post-1.html, the build produces:
dist/public/
├── index.html
├── about.html
├── blog/
│ └── post-1.html
├── funstack__/
│ └── fun__rsc-payload/
│ ├── a1b2c3d4.txt # RSC payload for index.html
│ ├── e5f6g7h8.txt # RSC payload for about.html
│ ├── i9j0k1l2.txt # RSC payload for blog/post-1.html
│ └── ... # deferred component payloads
└── assets/
└── client.js # Client bundle (shared)
All pages share the same client JavaScript bundle. Only the HTML and RSC payloads differ per entry.