Routing
Different ways to create a new page.
Overview
Fumapress supports a special routing model on top of the file-based routing model from Waku.
Content files are usually .md or .mdx files, they are useful for:
- Documentation pages.
- Blog pages.
File-based routes are .tsx files with a renderer & route config (see Page), they are useful for:
- Landing page.
- Other custom pages with its own renderer & metadata.
Plugins can also generate routes, which is useful for advanced use cases like:
- Generating blog routes from content sources.
- Making custom pages to be reusable (as a plugin).
With Content Files
Content pages are generated from the content option configured in press.config.tsx.
For the Fumadocs MDX adapter, each Markdown/MDX file under the collection directory becomes a page:
| File | URL |
|---|---|
content/docs/index.mdx | /docs |
content/docs/routing.mdx | /docs/routing |
content/blog/introducing.mdx | /blog/introducing |
The file path will decide its actual pathname & appearance on sidebar, see Page Tree & Page Slugs for available syntax.
The pathname generation can be customized from source configuration. For example, baseDir adds a directory name in generated URLs:
import { defineConfig } from "fumapress";
import { defineDocs } from "fumadocs-mdx/macro";
const docs = defineDocs({
dir: "content/docs",
docs: { async: true },
});
export default defineConfig({
content: docs.toFumadocsSource({
// all pages will locate under `docs/`
baseDir: "docs",
}),
});Fumapress renders these pages with the configured renderPage layout. You can customize it in your press config, or let plugins override how certain pages are rendered.
If you use other content sources (e.g. a CMS), consult the adapters documentation instead.
File-based Routing
Fumapress also reads route files from src/pages. This is a good fit for landing pages, dashboards, or other custom screens that do not belong to a content collection.
| File | Route |
|---|---|
src/pages/index.tsx | / |
src/pages/about.tsx | /about |
src/pages/blog/index.tsx | /blog |
src/pages/_layout.tsx | Shared layout |
For detailed Waku route conventions, see Waku File-based Routing.
To create a page:
export default function Page() {
return (
<main>
<h1>About</h1>
<p>This page is rendered from a route file.</p>
</main>
);
}You can export getConfig() to configure the route:
import type { RouteConfig } from "fumapress";
export async function getConfig() {
return {
render: "dynamic",
} satisfies RouteConfig;
}
export default function Page() {
return <main>Dashboard</main>;
}When i18n is enabled, file-based pages are automatically placed under the language segment (e.g. /my-page -> /en/my-page). Disable that behavior with autoI18n: false:
import type { RouteConfig } from "fumapress";
export async function getConfig() {
return {
autoI18n: false,
render: "static",
} satisfies RouteConfig;
}
export default function Page() {
return null;
}For file-based API routes, place files under src/pages/_api. Export HTTP method handlers:
export async function GET() {
return Response.json({
version: "1.0.0",
});
}Fumapress still creates the default catch-all route for content pages, so avoid defining a file-based route and a content page with the same URL.
With Plugins
Plugins can create pages, layouts, API routes, and slices with createPages(). This is useful when a feature needs routes that are derived from config or content instead of individual files.
import { defineConfig, type AppShape, type PressPlugin } from "fumapress";
function changelogPlugin<C extends AppShape>(): PressPlugin<C> {
return {
name: "changelog",
async createPages({ createPage }) {
createPage({
path: "/changelog",
render: "static",
component() {
return <main>Changelog</main>;
},
});
},
};
}
export default defineConfig({
// ...
}).plugins(changelogPlugin());Inside plugin hooks, this is the Fumapress app context. You can access the content loader, resolved config, i18n config, and other runtime data:
import { defineConfig, type AppShape, type PressPlugin } from "fumapress";
function allDocsPlugin<C extends AppShape>(): PressPlugin<C> {
return {
name: "all-docs",
async createPages({ createPage }) {
const source = await this.getLoader();
const pages = source.getPages();
createPage({
path: "/all-docs",
render: "static",
component() {
return (
<ul>
{pages.map((page) => (
<li key={page.url}>
<a href={page.url}>{page.data.title}</a>
</li>
))}
</ul>
);
},
});
},
};
}
export default defineConfig({
// ...
}).plugins(allDocsPlugin());Client
Import navigation helpers from fumapress/client.
<Link>
Use <Link> for in-app links instead of <a>. It enables client-side navigation and works with link validation during build.
import { Link } from "fumapress/client";
<Link href="/about">About</Link>;
// /* skip scrolling to the top */
return (
<Link href="/docs" scroll={false}>
Docs
</Link>
);Use a normal <a> for external URLs.
useRouter()
useRouter() reads the current location and navigates programmatically:
"use client";
import { useRouter } from "fumapress/client";
function Nav() {
const router = useRouter();
return (
<div>
<p>
{router.path}
{router.query ? `?${router.query}` : ""}
</p>
<button type="button" onClick={() => router.push("/about")}>
About
</button>
</div>
);
}push and replace accept the same scroll option as <Link>:
router.push("/docs", { scroll: false });
router.replace("/docs", { scroll: true });Server Redirects
On the server, use redirect to redirect to another route:
import { redirect } from "fumapress/router";
export default function Page() {
redirect("/docs");
}Or notFound to show a not found screen.
import { notFound } from "fumapress/router";
export default function Page() {
if (!hasAccess()) notFound();
return <main>Secret</main>;
}Last updated on
