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
4 changes: 2 additions & 2 deletions packages/docs/src/pages/learn/FileSystemRouting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ export default function BlogPost({ params }: { params: { slug: string } }) {

This generates `blog/hello.html` and `blog/world.html`. Each page component receives the resolved `params` as a prop.

A dynamic route without `generateStaticParams` is **not** pre-rendered (a warning is logged); it still resolves on the client via the SPA fallback.
A dynamic route **must** export `generateStaticParams`; the build fails otherwise. A static site can only serve pages that were enumerated at build time, so a dynamic route without it would produce no output.

> **Note:** Because static hosting serves one pre-rendered RSC payload per page, soft client-side navigation between different values of the _same_ dynamic route reflects the params of the initially-loaded page. Loading a dynamic URL directly (or via the SPA fallback) always renders the correct params. Static routes and layouts navigate fully on the client.
> **Note:** Because static hosting serves one pre-rendered RSC payload per page, soft client-side navigation between different values of the _same_ dynamic route reflects the params of the initially-loaded page. Loading a dynamic URL directly always renders the correct params. Static routes and layouts navigate fully on the client.

## Custom Conventions (Adapters)

Expand Down
2 changes: 1 addition & 1 deletion packages/static/src/fs-routes/runtime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function createFsRoutesEntries(
};
const files = modulesToRouteFiles(modules, warn);
const tree = adapter.buildRoutes(files);
const pages = await collectStaticPaths(tree, warn);
const pages = await collectStaticPaths(tree);
for (const { urlPath, params } of pages) {
yield {
path: urlPathToFilePath(urlPath),
Expand Down
10 changes: 4 additions & 6 deletions packages/static/src/fs-routes/tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,13 @@ describe("collectStaticPaths", () => {
]);
});

it("warns and skips a dynamic route without generateStaticParams", async () => {
it("throws for a dynamic route without generateStaticParams", async () => {
const tree: FsRouteTreeNode[] = [
{ path: "/blog/:slug", page: true, module: component },
];
const warn = vi.fn();
const pages = await collectStaticPaths(tree, warn);
expect(pages).toEqual([]);
expect(warn).toHaveBeenCalledTimes(1);
expect(warn.mock.calls[0]?.[0]).toContain("/blog/:slug");
await expect(collectStaticPaths(tree)).rejects.toThrow(
/"\/blog\/:slug".*generateStaticParams/,
);
});

it("throws when generateStaticParams is missing a param value", async () => {
Expand Down
22 changes: 10 additions & 12 deletions packages/static/src/fs-routes/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ async function addPagesForLeaf(
segments: string[],
module: FsRouteModule,
pages: StaticPage[],
onWarn?: (message: string) => void,
): Promise<void> {
const dynamicSegments = segments.filter(isDynamicSegment);

Expand All @@ -98,11 +97,11 @@ async function addPagesForLeaf(

const generate = module.generateStaticParams;
if (typeof generate !== "function") {
onWarn?.(
`Dynamic route "${segmentsToUrl(segments)}" has no generateStaticParams() export; skipping static generation. ` +
`It will still work on the client via the SPA fallback.`,
throw new Error(
`Dynamic route "${segmentsToUrl(segments)}" has no generateStaticParams() export. ` +
`Every page of a static site must be enumerated at build time; ` +
`export generateStaticParams() from the page module to list the params to pre-render.`,
);
return;
}

const paramSets = await generate();
Expand All @@ -126,17 +125,16 @@ async function walk(
nodes: FsRouteTreeNode[],
prefixSegments: string[],
pages: StaticPage[],
onWarn?: (message: string) => void,
): Promise<void> {
for (const node of nodes) {
const ownSegments =
node.path !== undefined ? splitRoutePath(node.path) : [];
const segments = [...prefixSegments, ...ownSegments];
if (node.page) {
await addPagesForLeaf(segments, node.module, pages, onWarn);
await addPagesForLeaf(segments, node.module, pages);
}
if (node.children) {
await walk(node.children, segments, pages, onWarn);
await walk(node.children, segments, pages);
}
}
}
Expand All @@ -146,15 +144,15 @@ async function walk(
*
* Static routes are emitted directly. Dynamic routes (with `:param` or
* catch-all segments) are expanded using each page module's
* `generateStaticParams()`; if absent, the route is skipped and `onWarn` is
* invoked.
* `generateStaticParams()`; a dynamic route without that export fails the
* build, since a static site cannot serve pages that were not enumerated at
* build time.
*/
export async function collectStaticPaths(
tree: FsRouteTreeNode[],
onWarn?: (message: string) => void,
): Promise<StaticPage[]> {
const pages: StaticPage[] = [];
await walk(tree, [], pages, onWarn);
await walk(tree, [], pages);
return pages;
}

Expand Down
7 changes: 3 additions & 4 deletions packages/static/src/fs-routes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@ export interface FsRouteModule {
/** The component for this page or layout. */
default?: ComponentType<{ params: Record<string, string> }> | ComponentType;
/**
* Optional function used to statically generate a dynamic route.
* Function used to statically generate a dynamic route. Required for pages
* whose route contains a dynamic segment; the build fails without it, since
* a static site cannot serve pages that were not enumerated at build time.
*
* Returns the list of concrete params to pre-render. Each entry maps every
* dynamic param name in the route's path to a concrete string value. For a
* catch-all segment, the value may contain slashes.
*
* Without this export, a dynamic route is not pre-rendered to HTML (it still
* works on the client via the SPA fallback).
*/
generateStaticParams?: () => MaybePromise<Array<Record<string, string>>>;
[key: string]: unknown;
Expand Down