From 39454143675c1cdd02c19027a87dcd5c8b0dcc93 Mon Sep 17 00:00:00 2001 From: uhyo Date: Sun, 22 Mar 2026 11:19:03 +0900 Subject: [PATCH] docs: add Learn page on file-system routing Adds a new Learn page explaining how to implement file-system routing using import.meta.glob and @funstack/router, with a pointer to the example-fs-routing package for a complete working example. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/docs/src/App.tsx | 9 ++ .../docs/src/components/Sidebar/Sidebar.tsx | 4 + .../src/pages/learn/FileSystemRouting.mdx | 95 +++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 packages/docs/src/pages/learn/FileSystemRouting.mdx diff --git a/packages/docs/src/App.tsx b/packages/docs/src/App.tsx index 627cf6f..d2fe691 100644 --- a/packages/docs/src/App.tsx +++ b/packages/docs/src/App.tsx @@ -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"; @@ -108,6 +109,14 @@ export const routes: RouteDefinition[] = [ ), }), + route({ + path: "/learn/file-system-routing", + component: ( + + {defer(, { name: "FileSystemRouting" })} + + ), + }), route({ path: "/advanced/multiple-entrypoints", component: ( diff --git a/packages/docs/src/components/Sidebar/Sidebar.tsx b/packages/docs/src/components/Sidebar/Sidebar.tsx index 0eaf5fa..9d1a09d 100644 --- a/packages/docs/src/components/Sidebar/Sidebar.tsx +++ b/packages/docs/src/components/Sidebar/Sidebar.tsx @@ -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", + }, ], }, { diff --git a/packages/docs/src/pages/learn/FileSystemRouting.mdx b/packages/docs/src/pages/learn/FileSystemRouting.mdx new file mode 100644 index 0000000..fad9e33 --- /dev/null +++ b/packages/docs/src/pages/learn/FileSystemRouting.mdx @@ -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: , + }); + }, +); +``` + +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: , + })); +} +``` + +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