FUNSTACK Static does not include a built-in file-system router, but you can implement one in userland using Vite's import.meta.glob and a router library like FUNSTACK Router.
The idea is to use import.meta.glob to discover page components from a pages/ directory at compile time, then convert the file paths into route definitions.
import { route, type RouteDefinition } from "@funstack/router/server";
const pageModules = import.meta.glob<{ default: React.ComponentType }>(
"./pages/**/*.tsx",
{ eager: true },
);
function filePathToUrlPath(filePath: string): string {
let urlPath = filePath.replace(/^\.\/pages/, "").replace(/\.tsx$/, "");
if (urlPath.endsWith("/index")) {
urlPath = urlPath.slice(0, -"/index".length);
}
return urlPath || "/";
}
export const routes: RouteDefinition[] = Object.entries(pageModules).map(
([filePath, module]) => {
const Page = module.default;
return route({
path: filePathToUrlPath(filePath),
component: <Page />,
});
},
);With this setup, files in the pages/ directory are automatically mapped to routes:
| File | Route |
|---|---|
pages/index.tsx |
/ |
pages/about.tsx |
/about |
pages/blog/index.tsx |
/blog |
Using import.meta.glob has two key advantages:
- Automatic discovery — you don't need to manually register each page. Just add a new
.tsxfile and it becomes a route. - Hot module replacement — Vite tracks the glob pattern, so adding or removing page files in development triggers an automatic update without a server restart.
To generate static HTML for each route, derive entry definitions from the route list:
import type { EntryDefinition } from "@funstack/static/entries";
import type { RouteDefinition } from "@funstack/router/server";
function collectPaths(routes: RouteDefinition[]): string[] {
const paths: string[] = [];
for (const route of routes) {
if (route.children) {
paths.push(...collectPaths(route.children));
} else if (route.path !== undefined && route.path !== "*") {
paths.push(route.path);
}
}
return paths;
}
function pathToEntryPath(path: string): string {
if (path === "/") return "index.html";
return `${path.slice(1)}.html`;
}
export default function getEntries(): EntryDefinition[] {
return collectPaths(routes).map((pathname) => ({
path: pathToEntryPath(pathname),
root: () => import("./root"),
app: <App ssrPath={pathname} />,
}));
}This produces one HTML file per route at build time.
For a complete working example, see the example-fs-routing package in the FUNSTACK Static repository.
- Multiple Entrypoints - Generating multiple HTML pages from a single project
- EntryDefinition - API reference for entry definitions
- How It Works - Overall FUNSTACK Static architecture