Skip to Content

Custom Pages

Add landing pages, API routes and other pages outside your content.

For a landing page or other custom UI, add a file under src/pages. It uses Waku's routing, and takes priority over a plugin page at the same path.

Creating a Page

src/pages/about.tsx
export default function Page() {
  return (
    <main>
      <h1>About</h1>
      <p>Built with Fumapress.</p>
    </main>
  );
}

It is available at /about. Custom pages are React Server Components, and they are prerendered by default.

Don't give a custom page the same URL as a content page. To leave / free, put your content under a baseDir.

Page Data

data in getConfig() is the page data of the page. Fumapress renders its title and description in <head>, and plugins read the rest, like ogImage for Open Graph images:

src/pages/about.tsx
import type { WakuRouteConfig } from "fumapress";

export function getConfig() {
  return {
    data: { title: "About", description: "Who builds Acme." },
  } satisfies WakuRouteConfig;
}

On-Demand Rendering

Set render in getConfig():

src/pages/status.tsx
import type { WakuRouteConfig } from "fumapress";

export function getConfig() {
  return { render: "dynamic" } satisfies WakuRouteConfig;
}

Layouts

_layout.tsx wraps the pages in its folder. Show your site's navbar with the Home Layout:

src/pages/_layout.tsx
import type { ReactNode } from "react";
import { HomeLayout } from "fumadocs-ui/layouts/home";
import { getPressContext, type WakuRouteProps } from "fumapress";
import type { app } from "../../press.config";

export default async function Layout({
  lang,
  children,
}: WakuRouteProps<typeof app> & { children: ReactNode }) {
  const layoutProps = await getPressContext<typeof app>().getLayoutProps({ lang });
  // the Home Layout takes these props, the others are for docs layouts
  const { nav, links, githubUrl, i18n, themeSwitch, searchToggle } = layoutProps;
  const home = { nav, links, githubUrl, i18n, themeSwitch, searchToggle };

  return <HomeLayout {...home}>{children}</HomeLayout>;
}

links has the links of ui.layoutProps and plugins. To add links to this layout only, put them in front, like links={[...homeLinks, ...links]}. A links prop without them replaces them.

Dynamic Routes

src/pages/showcase/[slug].tsx
import type { WakuRouteConfig, WakuRouteProps } from "fumapress";
import type { app } from "../../../press.config";

const items: Record<string, { title: string; description: string }> = {
  acme: { title: "Acme", description: "Docs of Acme" },
};

export function getConfig() {
  return {
    render: "static",
    staticPaths: Object.keys(items),
    data: { title: "Showcase" },
  } satisfies WakuRouteConfig;
}

type Props = WakuRouteProps<typeof app, "/showcase/[slug]">;

export default function Showcase({ slug }: Props) {
  const item = items[slug];
  if (!item) return null;

  return (
    <main>
      <h1>{item.title}</h1>
      {item.description}
    </main>
  );
}

data is the same for every path of the route. To give each path its own title, return it from head, which receives the path.

404 Page

src/pages/404.tsx
export default function NotFound() {
  return <main>This page doesn't exist.</main>;
}

Without this file, Fumapress renders the not found page of Fumadocs UI.

With internationalization, the built-in page uses the language of the URL when a server renders it. In static builds, 404.html is in the default language.

Reading Content

getPressContext() gives you the loader and helpers of your site. To get its types, export app from your config:

export const app = createPress(content).plugins(/* ... */); 

export default app.config({/* ... */});

See the Loader API for its other methods. If you are coming from Fumadocs, getPressContext() replaces your lib/source.ts.

Import the client helpers from fumapress/client:

src/components/nav.tsx
"use client";
import { Link, useRouter } from "fumapress/client";

export function Nav() {
  const router = useRouter();

  return (
    <nav>
      <Link href="/about">About</Link>
      <p>{router.path}</p>
      <button type="button" onClick={() => router.push("/docs", { scroll: false })}>
        Docs
      </button>
    </nav>
  );
}

Use Link rather than <a>, so that link validation can check it.

The router also has replace(), reload(), back(), forward() and prefetch(), and the query and hash of the URL. Inside a Link, useNavigationStatus() returns pending while its navigation loads.

In a server component, redirect or show the not found page with fumapress/router:

src/pages/secret.tsx
import { notFound, redirect } from "fumapress/router";

export default function Page() {
  if (!isSignedIn()) redirect("/login");
  if (!hasAccess()) notFound();

  return <main>Secret</main>;
}

API Routes

src/pages/_api/version.ts
export async function GET() {
  return Response.json({ version: "1.0.0" });
}

The path under _api is the URL, so this one is /version. Name each handler after an HTTP method, or use a default export for all methods.

API routes render on demand by default, and handlers can read getPressContext(). To generate a file at build time, use a single GET handler:

src/pages/_api/version.json.ts
export function getConfig() {
  return { render: "static" };
}

export async function GET() {
  return Response.json({ version: "1.0.0" });
}

Feature Support

Some features of content pages work differently on custom pages:

Last updated on

On this page