FFUNSTACK Static
DocsAPILearn

Getting Started

IntroductionMigrating from Vite SPA

Learn

React Server ComponentsHow It WorksOptimizing RSC PayloadsUsing lazy() in ServerPrefetching with ActivityFile-System Routing

Advanced

Multiple Entrypoints (SSG)Server-Side Rendering

API Reference

funstackStatic()defer()EntryDefinition

Help

FAQ

File-System Routing

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.

How It Works

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 |

Why import.meta.glob?

Using import.meta.glob has two key advantages:

  • Automatic discovery — you don't need to manually register each page. Just add a new .tsx file 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.

Static Generation

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.

Full Example

For a complete working example, see the example-fs-routing package in the FUNSTACK Static repository.

See Also

  • Multiple Entrypoints - Generating multiple HTML pages from a single project
  • EntryDefinition - API reference for entry definitions
  • How It Works - Overall FUNSTACK Static architecture