Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/docs/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import LazyServerComponents from "./pages/learn/LazyServerComponents.mdx";
import OptimizingPayloads from "./pages/learn/OptimizingPayloads.mdx";
import RSCConcept from "./pages/learn/RSC.mdx";
import DeferAndActivity from "./pages/learn/DeferAndActivity.mdx";
import FileSystemRouting from "./pages/learn/FileSystemRouting.mdx";
import MultipleEntrypoints from "./pages/advanced/MultipleEntrypoints.mdx";
import SSR from "./pages/advanced/SSR.mdx";
import EntryDefinitionApi from "./pages/api/EntryDefinition.mdx";
Expand Down Expand Up @@ -108,6 +109,14 @@ export const routes: RouteDefinition[] = [
</Layout>
),
}),
route({
path: "/learn/file-system-routing",
component: (
<Layout>
{defer(<FileSystemRouting />, { name: "FileSystemRouting" })}
</Layout>
),
}),
route({
path: "/advanced/multiple-entrypoints",
component: (
Expand Down
4 changes: 4 additions & 0 deletions packages/docs/src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export const navigation: NavSection[] = [
label: "Prefetching with Activity",
href: "/learn/defer-and-activity",
},
{
label: "File-System Routing",
href: "/learn/file-system-routing",
},
],
},
{
Expand Down
95 changes: 95 additions & 0 deletions packages/docs/src/pages/learn/FileSystemRouting.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# 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](https://github.com/uhyo/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.

```tsx
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](/api/entry-definition) from the route list:

```tsx
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`](https://github.com/uhyo/funstack-static/tree/master/packages/example-fs-routing) package in the FUNSTACK Static repository.

## See Also

- [Multiple Entrypoints](/advanced/multiple-entrypoints) - Generating multiple HTML pages from a single project
- [EntryDefinition](/api/entry-definition) - API reference for entry definitions
- [How It Works](/learn/how-it-works) - Overall FUNSTACK Static architecture