# Adapters (/docs/adapters)
## Overview [#overview]
Adapters translate your content source into page data that Fumapress can render. They handle tasks such as rendering MDX bodies, extracting table of contents, and exposing plain text for search or LLM exports.
If you use a custom CMS or another content format, you can implement your own adapter to integrate it with Fumapress.
## Usage [#usage]
Register adapters on your Fumapress config with `.adapters()`:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { fumadocsMdx } from "fumapress/adapters/mdx";
import { defineDocs } from "fumadocs-mdx/macro";
const docs = defineDocs({
dir: "content/docs",
docs: { async: true },
});
export default defineConfig({
content: docs.toFumadocsSource(),
})
// [!code ++]
.adapters(fumadocsMdx());
```
You can register multiple adapters for different content sources.
# Fumadocs MDX (/docs/adapters/mdx)
## Installation [#installation]
The adapter is included in `fumapress`, install [Fumadocs MDX](https://fumadocs.dev/docs/mdx) as a dependency:
npm
pnpm
yarn
bun
```bash
npm i fumadocs-mdx
```
```bash
pnpm add fumadocs-mdx
```
```bash
yarn add fumadocs-mdx
```
```bash
bun add fumadocs-mdx
```
## Setup [#setup]
Fumadocs MDX needs two pieces of configuration:
* The Vite plugin.
* Collections defined with `fumadocs-mdx/macro` in `press.config.tsx`, plus the Fumapress adapter.
vite.config.ts
press.config.tsx
```ts
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import press from "fumapress/vite";
import { fumadocsMdx } from "fumadocs-mdx/vite";
export default defineConfig({
plugins: [
press(),
// [!code ++]
fumadocsMdx(),
tailwindcss(),
],
});
```
```tsx
import { defineConfig } from "fumapress";
import { fumadocsMdx } from "fumapress/adapters/mdx";
// necessary so that page frontmatter matches what Fumapress expects:
import { metaSchema, pageSchema } from "fumapress/adapters/mdx/schema";
import { defineDocs } from "fumadocs-mdx/macro";
// [!code ++:16]
const docs = defineDocs({
dir: "content/docs",
docs: {
async: true,
schema: pageSchema,
lastModified: true,
postprocess: {
// Plugins such as `llms.txt` require this
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,
},
});
export default defineConfig({
// [!code ++]
content: docs.toFumadocsSource(),
})
// [!code ++] register adapter
.adapters(fumadocsMdx());
```
Start the dev server and write content under your collection directory.
## Frontmatter [#frontmatter]
With `pageSchema`, pages support the following frontmatter fields:
| Field | Description |
| ------------- | ---------------------------------------------------------------------------------------------------------------- |
| `title` | The page title (required) |
| `description` | Shown below the title and in page metadata |
| `icon` | A [Lucide](https://lucide.dev) icon name shown in the sidebar, see [Loader Options](/docs/config#loader-options) |
The adapter also reads fields your schema adds:
* `date`: the creation date, used by [RSS](/docs/plugins/rss) and [changelog entries](/docs/plugins/tegami).
* `tags`: post tags, included in `blogPageSchema`, see [Blog](/docs/plugins/blog).
For example, to support `date`:
```tsx title="press.config.tsx"
import { pageSchema } from "fumapress/adapters/mdx/schema";
import { defineDocs } from "fumadocs-mdx/macro";
import { z } from "zod";
const docs = defineDocs({
dir: "content",
docs: {
schema: pageSchema.extend({
date: z.coerce.date().optional(),
}),
},
});
```
The last modified date comes from git history when the collection sets `lastModified: true`.
## Options [#options]
The adapter only handle body rendering, see the [Fumadocs MDX documentation](https://fumadocs.dev/docs/mdx) for other available configurations.
### MDX Components [#mdx-components]
By default, `fumadocsMdx()` renders pages with Fumadocs UI's built-in MDX components: the Markdown elements, plus `Card`, `Cards`, and `Callout`. Other components such as `Tabs`, `Steps`, and `Files` are not registered, import them inside the MDX file, or register them globally.
Override `getMdxComponents` to add custom components:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { fumadocsMdx } from "fumapress/adapters/mdx";
import { defineDocs } from "fumadocs-mdx/macro";
import defaultMdxComponents, { createRelativeLink } from "fumadocs-ui/mdx";
import { TypeTable } from "fumadocs-ui/components/type-table";
const docs = defineDocs({
dir: "content/docs",
docs: { async: true },
});
export default defineConfig({
content: docs.toFumadocsSource(),
}).adapters(
fumadocsMdx({
async getMdxComponents(page) {
const source = await this.getLoader();
return {
...defaultMdxComponents,
TypeTable,
a: createRelativeLink(source, page),
};
},
}),
);
```
# Notion (/docs/adapters/notion)
The `@fumapress/notion` package provides both a dynamic content source and the adapter plugin needed to render Notion blocks. It uses the official Notion API and does not ship a client-side renderer.
## Installation [#installation]
Install the integration and the official Notion client:
npm
pnpm
yarn
bun
```bash
npm i @fumapress/notion @notionhq/client
```
```bash
pnpm add @fumapress/notion @notionhq/client
```
```bash
yarn add @fumapress/notion @notionhq/client
```
```bash
bun add @fumapress/notion @notionhq/client
```
Import the renderer's Tailwind source in your global CSS after the Fumadocs UI styles:
```css title="src/app.css"
@import "@fumapress/notion/css/preset.css";
```
## Notion Setup [#notion-setup]
1. [Create a Notion integration](https://developers.notion.com/docs/getting-started) with permission to read content.
2. Connect the database containing your pages to the integration.
3. Store the integration token and the database's **data source ID** in server-only environment variables.
```ini title=".env.local"
NOTION_TOKEN=ntn_...
NOTION_DATA_SOURCE_ID=...
```
Use a data source ID, not the database ID shown in the database URL. In the current Notion API, a
database is a container and each query targets one of its data sources. See Notion's [data source
upgrade guide](https://developers.notion.com/guides/get-started/upgrade-guide-2025-09-03).
## Configuration [#configuration]
For a runnable project, see [`examples/notion`](https://github.com/fuma-nama/fumapress/tree/main/examples/notion).
Create the shared integration once, use it as the Fumapress content source, and pass the same object to `notionPlugin()`:
```tsx title="press.config.tsx"
import { Client } from "@notionhq/client";
import { fumapressNotion, notionPlugin } from "@fumapress/notion";
import { defineConfig } from "fumapress";
const notion = fumapressNotion({
client: new Client({ auth: process.env.NOTION_TOKEN }),
dataSourceId: process.env.NOTION_DATA_SOURCE_ID!,
});
export default defineConfig({
content: notion.dynamicSource({
properties: {
title: "Name",
slug: "Slug",
description: "Description",
},
query: {
filter: {
property: "Published",
checkbox: { equals: true },
},
},
}),
loaderOptions: {
alwaysRevalidate: true,
},
mode: "dynamic",
}).plugins(notionPlugin(notion));
```
The source paginates through every matching data-source row and recursively pulls nested page blocks. Calls to load the same page body are deduplicated until the content loader is invalidated.
`alwaysRevalidate` is the simplest way to reflect Notion edits immediately. For a higher-traffic site, leave it disabled and call `revalidateLoader()` from a trusted webhook or revalidation route instead.
### Page Properties [#page-properties]
When `properties` is omitted, the source:
* Uses the first Notion `title` property as the page title.
* Uses a property named `Slug` for the URL, falling back to a slug generated from the title.
* Uses a property named `Description` when present.
A slug can contain `/` to create nested routes. Duplicate or unsafe virtual paths fail with a descriptive error instead of silently replacing a page.
### Query and Paths [#query-and-paths]
Pass Notion filters and sorts through `query`. Use `baseDir` to place the virtual pages under a directory, or `generatePath` for complete control over the virtual file path.
```tsx
notion.dynamicSource({
baseDir: "docs",
query: {
sorts: [{ timestamp: "last_edited_time", direction: "descending" }],
},
generatePath(page, info) {
return info.slugs.length > 0 ? `${info.slugs.join("/")}.mdx` : `${page.id}.mdx`;
},
});
```
## Rendering [#rendering]
`notionPlugin()` contributes renderers for common block types, you can add or override the default renderers.
```tsx title="press.config.tsx"
notionPlugin(notion, {
components: {
callout({ block, children, renderRichText }) {
return (
);
},
},
});
```
### Notion Files [#notion-files]
Notion-hosted file URLs expire after one hour. By default, `notionPlugin()` renders internal assets through `/api/notion/file`, which retrieves a fresh signed URL and verifies that the requested block belongs to a page in the configured data source.
The proxy needs a dynamic deployment. For a static export, disable it only when your content uses durable external URLs or when a custom renderer copies assets to durable storage:
```tsx
notionPlugin(notion, {
fileProxy: false,
});
```
## Source-only Import [#source-only-import]
Code that only needs to pull Notion pages can avoid the React renderer entry:
```ts
import { fumapressNotion } from "@fumapress/notion/source";
```
This entry keeps the Notion SDK external and contains no React imports.
# Obsidian (/docs/adapters/obsidian)
The `@fumapress/obsidian` package connects the runtime source from `fumadocs-obsidian` to Fumapress. Vault notes are compiled in memory without generating intermediate MDX files, and the renderer stays on the server.
Supported Obsidian syntax includes wikilinks and embeds, callouts, block IDs, and comments.
## Installation [#installation]
Install the integration:
npm
pnpm
yarn
bun
```bash
npm i @fumapress/obsidian
```
```bash
pnpm add @fumapress/obsidian
```
```bash
yarn add @fumapress/obsidian
```
```bash
bun add @fumapress/obsidian
```
Import its Tailwind source after the Fumadocs UI styles:
```css title="src/app.css"
@import "@fumapress/obsidian/css/preset.css";
```
## Configuration [#configuration]
For a runnable project, see [`examples/obsidian`](https://github.com/fuma-nama/fumapress/tree/main/examples/obsidian).
Create the vault source once, pass its dynamic source to Fumapress, and register `obsidianPlugin()` with the same source:
```tsx title="press.config.tsx"
import { obsidian, obsidianPlugin } from "@fumapress/obsidian";
import { defineConfig } from "fumapress";
const vault = obsidian({
dir: "public/vault",
// Map attachments to URLs served from the public directory.
url: (file) => `/vault/${file}`,
});
export default defineConfig({
content: vault.dynamicSource(),
}).plugins(obsidianPlugin(vault));
```
`page.data.load()` compiles a note at most once per vault snapshot. Media files are mapped to URLs and are not read into memory. Ambiguous wikilinks resolve like Obsidian: notes take precedence over attachments, then the shortest path wins.
### Development Watching [#development-watching]
The Fumapress plugin registers the vault with Vite in development. Add its Vite companion to receive file changes and reload the page:
```ts title="vite.config.ts"
import { defineConfig } from "vite";
import { obsidianVitePlugin } from "@fumapress/obsidian/vite";
import tailwindcss from "@tailwindcss/vite";
import press from "fumapress/vite";
export default defineConfig({
plugins: [press(), obsidianVitePlugin(), tailwindcss()],
});
```
Because wikilinks and aliases can create cross-file dependencies, a vault change invalidates the source snapshot. Only changed files are read from disk again.
Set `watch: false` when another process owns invalidation:
```tsx
obsidianPlugin(vault, { watch: false });
```
## Rendering [#rendering]
The plugin supplies Fumadocs MDX components, Obsidian callout components, and relative-link resolution. Add or replace components through `components`:
```tsx title="press.config.tsx"
obsidianPlugin(vault, {
components: {
img: (props) => ,
},
});
```
The same compiled representation supplies the page body, table of contents, structured search data, and plain text for LLM exports.
## Vault Schema [#vault-schema]
The source accepts Standard Schema validators for frontmatter and `meta.json` files. It also supports additional remark and rehype plugins for syntax such as Mermaid and math. See the [Fumadocs Obsidian integration guide](https://www.fumadocs.dev/docs/integrations/obsidian) for the complete source configuration.
# Sanity (/docs/adapters/sanity)
The `@fumapress/sanity` package provides a dynamic content source backed by a Sanity dataset, and the adapter plugin needed to render Portable Text content.
## Installation [#installation]
Install the integration and the Sanity client:
npm
pnpm
yarn
bun
```bash
npm i @fumapress/sanity @sanity/client
```
```bash
pnpm add @fumapress/sanity @sanity/client
```
```bash
yarn add @fumapress/sanity @sanity/client
```
```bash
bun add @fumapress/sanity @sanity/client
```
## Document Schema [#document-schema]
Point the integration at a document type in your Sanity Studio, with the fields:
| Field | Type |
| ------------- | ----------------------------- |
| `title` | string (required) |
| `slug` | slug (required) |
| `description` | string |
| `body` | block content (Portable Text) |
For a complete Studio, see [`examples/example-sanity-studio`](https://github.com/fuma-nama/fumapress/tree/main/examples/example-sanity-studio), which also defines custom block types like callouts, cards, tabs, and steps.
## Configuration [#configuration]
For a runnable project, see [`examples/example-sanity`](https://github.com/fuma-nama/fumapress/tree/main/examples/example-sanity).
Create the shared integration once, use it as the Fumapress content source, and pass the same object to `sanityPlugin()`:
```tsx title="press.config.tsx"
import { fumapressSanity, sanityPlugin } from "@fumapress/sanity";
import { createClient } from "@sanity/client";
import { defineConfig } from "fumapress";
const sanity = fumapressSanity({
client: createClient({
projectId: process.env.SANITY_STUDIO_PROJECT_ID,
dataset: process.env.SANITY_STUDIO_DATASET,
apiVersion: "2024-12-04",
}),
docType: "docs",
});
export default defineConfig({
content: sanity.dynamicSource(),
loaderOptions: {
alwaysRevalidate: true,
},
mode: "dynamic",
}).plugins(sanityPlugin(sanity));
```
`alwaysRevalidate` is the simplest way to reflect Sanity edits immediately. For a higher-traffic site, leave it disabled and call `revalidateLoader()` from a trusted webhook or revalidation route instead.
### Paths [#paths]
Page URLs are generated from the `slug` field, which may contain `/` for nested routes. Use `baseDir` to place the pages under a directory, or `generatePath` for complete control over the virtual file path:
```tsx
sanity.dynamicSource({
baseDir: "docs",
generatePath(doc) {
return `${doc.slug?.current ?? doc._id}.mdx`;
},
});
```
## Rendering [#rendering]
`sanityPlugin()` implements the content adapter:
* Renders the Portable Text body and its table of contents.
* Extracts plain text for search and LLM exports.
* Resolves creation & modified dates from the document's `_createdAt` and `_updatedAt`.
The default renderer covers standard blocks only. To support your own block types, pass a `PortableText` renderer with the component mapping:
```tsx title="press.config.tsx"
import { fumapressSanity } from "@fumapress/sanity";
import { PortableText } from "@portabletext/react";
const sanity = fumapressSanity({
// ...
PortableText({ value }) {
return (
{/* ... */},
},
}}
/>
);
},
});
```
The example project includes ready-made mappings for callouts, cards, files, tabs, steps, and accordions, matching the block types of the example Studio.
# Static Assets (/docs/assets)
## Public Directory [#public-directory]
Files in the `public` directory are served from the root URL path: `public/screenshot.png` is available at `/screenshot.png`. They are copied to the build output as-is.
## Images [#images]
Reference them from Markdown with an absolute path:
```mdx

```
Markdown images are rendered through the `Image` component, so they benefit from [image optimization](/docs/plugins/image) when a provider is enabled.
In your own components, use the component directly and give it intrinsic dimensions to avoid layout shift:
```tsx
import { Image } from "fumapress/image";
;
```
### Navbar Logo [#navbar-logo]
A common use is the site logo, passed as part of the navbar title:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { Image } from "fumapress/image";
export default defineConfig({
// ...
defaultLayoutProps: {
nav: {
title: (
<>
My Docs
>
),
},
},
});
```
## Favicon [#favicon]
Place the icon in `public` and link it from `meta.root`:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
export default defineConfig({
// ...
meta: {
root() {
return (
<>
{/* [!code ++] */}
>
);
},
},
});
```
The same place works for any other head tag, such as web fonts or verification tags, see [Meta](/docs/config#meta).
## Open Graph Images [#open-graph-images]
Social preview images are generated per page by the [Takumi plugin](/docs/plugins/takumi), enabled by default. See its `generate` option to customize the design.
# Basics (/docs/basics)
## Project Layout [#project-layout]
A new app looks like this:
You mostly edit `content/` and `press.config.tsx`. Styles live in `src/app.css`.
## Write Pages [#write-pages]
Each Markdown or MDX file under `content/` becomes a page. The starter file is:
```mdx title="content/index.mdx"
---
title: My Page
description: Hello World
---
## Overview
This is my first document.
```
The block at the top is frontmatter. `title` and `description` show in the page layout. Use `##` in the body: Fumapress already renders `title` as the main heading.
Frontmatter also accepts an `icon`, a [Lucide](https://lucide.dev) icon name shown in the sidebar. Icon names are resolved by the `lucideIconsPlugin` loader plugin, see [Loader Options](/docs/config#loader-options).
Add another file:
```mdx title="content/getting-started.mdx"
---
title: Getting Started
description: Install and run the app.
---
## Install
Create a new app, then start the dev server.
```
That page is available at `/getting-started`. Folders work the same way:
| File | URL |
| ----------------------------- | ------------------ |
| `content/index.mdx` | `/` |
| `content/getting-started.mdx` | `/getting-started` |
| `content/guide/install.mdx` | `/guide/install` |
To control sidebar order and labels, add a `meta.json` next to the pages:
```json title="content/meta.json"
{
"pages": ["index", "getting-started", "guide"]
}
```
Fumapress uses the same Markdown features and page tree rules as Fumadocs. When you need more (callouts, code tabs, folder options), see:
MDX syntax, frontmatter, callouts, code blocks, tabs, and built-in components.
How file paths, folders, and meta.json shape URLs and the sidebar.
## Configure the Site [#configure-the-site]
Open `press.config.tsx`. This is where you name the site and connect a git repo:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
export default defineConfig({
// ...
site: {
name: "My Docs",
baseUrl: "https://docs.example.com",
git: {
user: "acme",
repo: "docs",
branch: "main",
},
},
});
```
`site.name` appears in the UI and metadata. `site.git` adds the repository link to navbar and an edit button on pages.
Add additional links to the navbar with `defaultLayoutProps`:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
export default defineConfig({
// ...
defaultLayoutProps: {
nav: {
title: "My Docs",
},
links: [
{
text: "GitHub",
url: "https://github.com/acme/docs",
},
],
},
});
```
## Change Colors and Fonts [#change-colors-and-fonts]
### Theme [#theme]
Edit `src/app.css`. Swap the theme import to change colors:
```css title="src/app.css"
@import "tailwindcss";
/* [!code --] */
@import "fumadocs-ui/css/neutral.css";
/* [!code ++] */
@import "fumadocs-ui/css/black.css";
@import "fumadocs-ui/css/preset.css";
@import "fumapress/css/preset.css";
```
More presets: [Fumadocs UI Themes](https://fumadocs.dev/docs/ui/theme).
### Fonts [#fonts]
The starter already loads Geist from Google Fonts. To use another font, pick one on [Google Fonts](https://fonts.google.com), copy the `` tags, and put them in `meta.root` inside `press.config.tsx`:
```tsx title="press.config.tsx"
export default defineConfig({
meta: {
root() {
return (
<>
>
);
},
},
});
```
Then set the CSS variable in `src/app.css`:
```css title="src/app.css"
@theme {
--default-font-family: "Inter", sans-serif;
}
```
## Add a Custom Page [#add-a-custom-page]
Markdown covers documentation. For a landing page or other React UI, create a file under `src/pages`:
```tsx title="src/pages/about.tsx"
export default function Page() {
return (
About
Built with Fumapress.
);
}
```
The above page will be available at `/about`, see [Routing](/docs/routing) for more details.
### Rendering [#rendering]
Fumapress use **React Server Components** (RSC), it requires basic knowledge of React to create UIs.
If you are familiar with React but not RSC, make a look at reads like [Making Sense of React Server Components](https://www.joshwcomeau.com/react/server-components).
## Read Content from Code [#read-content-from-code]
Sometimes a custom page needs data from your Markdown files, such as a list of all pages. Export `getPressContext` from the config:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
const config = defineConfig({
// ...
});
export const { getPressContext } = config.utils();
export default config;
```
Use it in a page:
```tsx title="src/pages/all-pages.tsx"
import { getPressContext } from "../../press.config";
export default async function Page() {
const source = await getPressContext().getLoader();
const pages = source.getPages();
return (
);
}
```
`getLoader()` gives you helpers like `getPage`, `getPages`, and `getPageTree`. See the [Loader API](https://fumadocs.dev/docs/headless/source-api) for the full list.
You can skip this until you build something that needs it.
### Coming from Fumadocs? [#coming-from-fumadocs]
In Fumadocs you create a loader in `lib/source.ts` and import it in your routes.
In Fumapress, pass sources through `content` in `press.config.tsx`. You do not maintain `lib/source.ts` or a catch-all docs route yourself.
## Next Steps [#next-steps]
Content routes, file-based pages, and plugins.
Docs, notebook, glass, and home layouts.
Search, sitemap, RSS, OG images, and more.
All options in press.config.tsx.
Build the app and deploy it to your hosting platform.
# Content Collections (/docs/collections)
## Overview [#overview]
`content` accepts a single source, or a record of named sources to host several collections on one site:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { defineDocs } from "fumadocs-mdx/macro";
import {
blogMetaSchema,
blogPageSchema,
metaSchema,
pageSchema,
} from "fumapress/adapters/mdx/schema";
const docs = defineDocs({
dir: "content/docs",
docs: { async: true, schema: pageSchema },
meta: { schema: metaSchema },
});
const blog = defineDocs({
dir: "content/blog",
docs: { async: true, schema: blogPageSchema },
meta: { schema: blogMetaSchema },
});
export default defineConfig({
content: {
docs: docs.toFumadocsSource({ baseDir: "docs" }),
blog: blog.toFumadocsSource({ baseDir: "blog" }),
},
});
```
`baseDir` places each collection under its own URL prefix:
| File | URL |
| ------------------------ | ------------- |
| `content/docs/index.mdx` | `/docs` |
| `content/blog/hello.mdx` | `/blog/hello` |
The record keys become the `type` of each page, so code can tell which collection a page belongs to. Plugins use it too: the [blog plugin](/docs/plugins/blog) treats pages with `type === "blog"` as blog posts by default.
## Layout per Collection [#layout-per-collection]
To render each collection with a different layout, switch on `page.type` in `renderPage`. For example, with a `docs` and a `guides` collection:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { createDocsLayoutPage } from "fumapress/layouts/docs";
import { createHomeLayoutPage } from "fumapress/layouts/home";
const DocsPage = createDocsLayoutPage();
const GuidePage = createHomeLayoutPage();
const config = defineConfig({
// ...
renderPage: (props) => {
if (props.page.type === "guides") return ;
return ;
},
});
export default config;
```
Collections consumed by a plugin, such as the [blog plugin](/docs/plugins/blog) or [Tegami](/docs/plugins/tegami), are rendered by the plugin itself, you only configure the layout of the remaining collections.
## Sidebar [#sidebar]
All sources merge into a single page tree, with each `baseDir` as a top-level folder. Mark a collection's folder as a root folder so the sidebar only shows its own pages while browsing it:
```json title="content/docs/meta.json"
{
"title": "Documentation",
"root": true,
"pages": ["..."]
}
```
See [Page Tree](https://fumadocs.dev/docs/page-conventions) for root folders and other conventions.
To present one collection as the entire sidebar instead of a folder, replace the tree in your layout's `render` option:
```tsx title="press.config.tsx"
const DocsPage = createDocsLayoutPage({
async render({ locale }) {
let pageTree = (await this.getLoader()).getPageTree(locale);
for (const child of pageTree.children) {
if (child.type === "folder" && child.$id === "docs") {
pageTree = { ...pageTree, children: child.children };
}
}
return {
layoutProps: { tree: pageTree },
};
},
});
```
# Configurations (/docs/config)
## Main Config [#main-config]
Primary options for your app.
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
export default defineConfig({
// ...
});
```
### Site [#site]
The site information like name and linked git repository:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
export default defineConfig({
site: {
name: "Fumapress",
baseUrl: import.meta.env.DEV ? "http://localhost:3000" : "https://press.fumadocs.dev",
git: {
user: "fuma-nama",
branch: "dev",
repo: "fumapress",
},
},
});
```
The repository is used for the icon link in navbar, and the link to source file of pages.
Source file links are resolved against the detected git root, set `git.rootDir` to override it, such as when building in an environment without the `.git` directory.
#### Git Providers [#git-providers]
`git.provider` supports `github` (default), `gitlab`, and `bitbucket`, the navbar icon link and source file URLs follow the given provider. For self-hosted instances, pass the instance URL with `git.url`:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
export default defineConfig({
site: {
git: {
provider: "gitlab",
url: "https://gitlab.example.com",
user: "fuma-nama",
branch: "dev",
repo: "fumapress",
},
},
});
```
### Layouts [#layouts]
Specify or customize page UI with `renderPage`, `renderRoot`, and `renderNotFound`.
```tsx title="press.config.tsx"
import { createDocsLayoutPage } from "fumapress/layouts/docs";
import { defineConfig } from "fumapress";
const DocsLayout = createDocsLayoutPage();
const config = defineConfig({
// [!code ++]
renderPage: (props) => ,
});
export default config;
```
`renderNotFound` customizes the not found screen, shown for unknown URLs and `notFound()` calls:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { Link } from "fumapress/client";
export default defineConfig({
// [!code ++:6]
renderNotFound: () => (
Page not found
Back to home
),
});
```
See [Layouts](/docs/layouts) for details.
### Render Mode [#render-mode]
`mode` controls how routes are rendered:
* `default`: when your content source will not change after production build. It still emits API routes, such as the search API endpoint, which require a running server to host.
* `static`: same as `default`, but will not emit API routes. Production builds emit only static files, which can be served via CDNs.
* `dynamic`: when your content source will change after production build. Requires a running server to host.
```ts title="press.config.tsx"
import { defineConfig } from "fumapress";
export default defineConfig({
// ...
// [!code ++]
mode: "static",
});
```
See [Deployment](/docs/deployment) for hosting each mode.
### Preset [#preset]
By default, Fumapress adds recommended plugins automatically:
* [Sitemap](/docs/plugins/sitemap)
* [robots.txt](/docs/plugins/robots)
* [llms.txt](/docs/plugins/llms.txt)
* [RSS](/docs/plugins/rss)
* Search with [Flexsearch](/docs/plugins/flexsearch)
* OG images with [Takumi](/docs/plugins/takumi)
* [Image optimization](/docs/plugins/image) matching your deployment target
Plugins you add yourself take priority, e.g. adding `oramaSearchPlugin()` replaces the default search.
To start from an empty plugin list instead:
```ts title="press.config.tsx"
import { defineConfig } from "fumapress";
export default defineConfig({
// ...
// [!code ++]
preset: false,
});
```
### Loader Options [#loader-options]
`loaderOptions` configures the [Loader API](https://fumadocs.dev/docs/headless/source-api) that Fumapress creates from your content sources. A common use is loader plugins, such as resolving the `icon` frontmatter field to Lucide icons:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { lucideIconsPlugin } from "fumadocs-core/source/plugins/lucide-icons";
export default defineConfig({
// ...
// [!code ++:3]
loaderOptions: {
plugins: [lucideIconsPlugin()],
},
});
```
For dynamic content sources (e.g. a CMS), `alwaysRevalidate: true` refetches them on every request:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { lucideIconsPlugin } from "fumadocs-core/source/plugins/lucide-icons";
export default defineConfig({
// ...
// [!code ++:3]
loaderOptions: {
alwaysRevalidate: true,
},
});
```
Leave it disabled to control this yourself with `getPressContext().revalidateLoader()`.
### Meta [#meta]
To add meta tags, you can leverage the `meta` config.
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
export default defineConfig({
// [!code ++:19]
meta: {
root() {
return (
<>
{/* you can use it for link tags as well */}
>
);
},
page(page) {
return <>{/* page-level meta tags */}>;
},
},
});
```
## UI [#ui]
### Tailwind CSS [#tailwind-css]
Create a `src/app.css` file:
```css title="src/app.css"
@import "tailwindcss";
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";
@import "fumapress/css/preset.css";
```
It will be automatically loaded, restart the dev server if there's one running.
You can change the color theme or add custom styles, like:
```css title="src/app.css"
@import "tailwindcss";
/* [!code --] */
@import "fumadocs-ui/css/neutral.css";
/* [!code ++] */
@import "fumadocs-ui/css/black.css";
@import "fumadocs-ui/css/preset.css";
@import "fumapress/css/preset.css";
@theme {
/* [!code ++] */
--default-font-family: "Geist", sans-serif;
}
```
### Using Radix UI [#using-radix-ui]
Fumapress uses the [Base UI](https://base-ui.com) headless component library by default, same as Fumadocs.
If you are using [Radix UI](https://radix-ui.com), Fumapress also supports the [Radix UI build of Fumadocs UI](https://fumadocs.dev/docs/ui/component-library).
You can enable it by installing `fumadocs-ui` without the `@fumadocs/base-ui` alias:
npm
pnpm
yarn
bun
```bash
npm i fumadocs-ui
npm uninstall @base-ui/react
```
```bash
pnpm add fumadocs-ui
pnpm remove @base-ui/react
```
```bash
yarn add fumadocs-ui
yarn remove @base-ui/react
```
```bash
bun add fumadocs-ui
bun remove @base-ui/react
```
## Vite Plugin [#vite-plugin]
The `press()` plugin connects Fumapress to Vite:
```ts title="vite.config.ts"
import { defineConfig } from "vite";
import press from "fumapress/vite";
import { fumadocsMdx } from "fumadocs-mdx/vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [press(), fumadocsMdx(), tailwindcss()],
});
```
It takes the options of the app itself, like the directory layout and deployment target:
### Deployment Adapter [#deployment-adapter]
Vercel, Netlify, and Cloudflare are detected from the environment, other targets fall back to `waku/adapters/node`. Set `adapter` when the detected one isn't what you deploy to, see [Deployment](/docs/deployment):
```ts title="vite.config.ts"
export default defineConfig({
plugins: [
press({
// [!code ++]
adapter: "waku/adapters/vercel",
}),
],
});
```
## CLI [#cli]
The `fumapress` CLI runs Vite with the environments your app needs, use it instead of calling `vite` directly.
```json title="package.json"
{
"scripts": {
"dev": "fumapress dev",
"build": "fumapress build",
"start": "fumapress start"
}
}
```
`dev` and `start` accept `--host` (`-h`) and `--port` (`-p`), the port can also be set with the `PORT` environment variable. `dev` listens on port 3000, `start` picks a free port from 8080.
npm
pnpm
yarn
bun
```bash
npm run dev -- --port 4000
```
```bash
pnpm run dev --port 4000
```
```bash
yarn dev --port 4000
```
```bash
bun run dev --port 4000
```
## Server Entry [#server-entry]
The server entry file is optional, you can create one if needed. Useful for implementing custom router.
Custom server entries use [Waku.js](https://waku.gg) APIs directly. Since Fumapress manages Waku as its own dependency, install it explicitly for direct API access, matching the version pinned by Fumapress:
npm
pnpm
yarn
bun
```bash
npm i waku@1.0.0-rc.0
```
```bash
pnpm add waku@1.0.0-rc.0
```
```bash
yarn add waku@1.0.0-rc.0
```
```bash
bun add waku@1.0.0-rc.0
```
```tsx title="src/waku.server.tsx"
import _adapter from "waku/adapters/default";
// your main config file
import pressConfig from "../press.config";
import { createRouter } from "fumapress/router";
import { fsRouterFn } from "fumapress/router/fs";
const router = await createRouter(pressConfig);
const modules = import.meta.glob("./pages/**/*.{ts,tsx,js,jsx}", {
base: "/src",
});
// file-system router
const pages = router.createPages(fsRouterFn(modules));
const middlewareFns = router.createMiddlewares();
const adapter = router.patchAdapter(_adapter);
export default adapter(pages, { middlewareFns });
```
You can also use [Waku Config-based Routing](https://waku.gg/#routing) instead of the file-system router, see the linked docs for usage.
```tsx title="src/waku.server.tsx"
import _adapter from "waku/adapters/default";
// your main config file
import pressConfig from "../press.config";
import { createRouter } from "fumapress/router";
const router = await createRouter(pressConfig);
const pages = router.createPages(async ({ createPage }) => {
// note: root element is already created by Fumapress
createPage({
render: "static",
path: "/hello-world",
component() {
// ...
},
});
});
const middlewareFns = router.createMiddlewares();
const adapter = router.patchAdapter(_adapter);
export default adapter(pages, { middlewareFns });
```
# Deployment (/docs/deployment)
## Overview [#overview]
`fumapress build` writes the app to `dist/`:
* `dist/public`: static assets and prerendered pages.
* `dist/server`: the server bundle.
Two settings control the result: the **render mode** decides how much of the app is prerendered, and the **deployment adapter** packages the output for your hosting platform.
## Render Mode [#render-mode]
Set `mode` in your press config:
```ts title="press.config.tsx"
import { defineConfig } from "fumapress";
export default defineConfig({
// ...
// [!code ++]
mode: "static",
});
```
* `default`: pages are prerendered, API routes such as the search endpoint still run on a server.
* `static`: only static files are emitted, deploy `dist/public` to any static host or CDN.
* `dynamic`: pages render on request, for content sources that change after the build.
See [Render Mode](/docs/config#render-mode) for choosing one. A route's own `render` config takes priority over the global mode, see [Routing](/docs/routing).
### Static Mode [#static-mode]
With `mode: "static"`, every route is prerendered into `dist/public`:
* An HTML file per page, and a `404.html`.
* The output of enabled plugins, like `sitemap.xml` and Open Graph images.
* The search index at `/api/search`, downloaded by the search dialog to run queries in the browser.
Plugins that require a server fail the build with an error:
* [OpenAPI](/docs/plugins/openapi) proxy
* [AI](/docs/plugins/ai) and [MCP](/docs/plugins/mcp)
* [Feedback](/docs/plugins/feedback) integrations
* [Notion](/docs/adapters/notion) file proxy
Self-hosted image optimization is skipped automatically.
## Platforms [#platforms]
Each platform is served by a deployment adapter, detected from environment variables at build time: Vercel, Netlify, and Cloudflare set them on their build machines, other environments fall back to `waku/adapters/node`. To build for a platform from elsewhere (e.g. your own CI), set the `adapter` option:
```ts title="vite.config.ts"
import { defineConfig } from "vite";
import press from "fumapress/vite";
export default defineConfig({
plugins: [
press({
// [!code ++]
adapter: "waku/adapters/cloudflare",
}),
],
});
```
The adapter also selects the [image optimization](/docs/plugins/image) provider on Vercel, Cloudflare, and Node.js.
### Node.js [#nodejs]
The default target. Build, then start the server:
npm
pnpm
yarn
bun
```bash
npm run build
npm run start
```
```bash
pnpm run build
pnpm run start
```
```bash
yarn build
yarn run start
```
```bash
bun run build
bun run start
```
`fumapress start` runs the server entry emitted by the Node.js adapter, which serves both `dist/public` and dynamic routes. It reads the `PORT` and `HOST` environment variables, or pass `--port`/`--host`. You can also run the entry directly without the CLI:
```bash
PORT=8080 node dist/serve-node.js
```
For image optimization on Node.js, install `sharp` as a dependency, the recommended preset picks it up automatically.
### Vercel [#vercel]
No configuration needed: import the repository, and the build produces a [Build Output API](https://vercel.com/docs/build-output-api) structure with static files and a serverless function for dynamic routes. Image optimization uses [Vercel Image Optimization](https://vercel.com/docs/image-optimization).
### Netlify [#netlify]
The build emits a `netlify.toml` when your project has none, with `dist/public` as the publish directory and a Netlify Function for dynamic routes. Commit the generated file, or configure those values yourself.
### Cloudflare [#cloudflare]
The app deploys as a Cloudflare Worker with static assets. [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/) is detected automatically; to build elsewhere, set the adapter as shown above.
The build generates a Wrangler config unless your project already has one, in which case its `name` and compatibility settings are reused. It also points Wrangler at the built worker, so you can deploy right after building:
```bash
npx wrangler deploy
```
Image optimization uses [Cloudflare Image Transformations](https://developers.cloudflare.com/images/transform-images/), enable it on your zone.
### Static Hosting [#static-hosting]
For GitHub Pages, CDNs, or any other static file host: set `mode: "static"`, build, and upload `dist/public`. The emitted `404.html` is picked up by hosts that support it.
When the site is served from a sub-path (e.g. `https://user.github.io/repo/`), set `basePath`:
```ts title="vite.config.ts"
import { defineConfig } from "vite";
import press from "fumapress/vite";
export default defineConfig({
plugins: [
press({
// [!code ++]
basePath: "/repo/",
}),
],
});
```
## Environment Variables [#environment-variables]
The CLI loads `.env` and `.env.local` (plus mode-specific variants like `.env.production`) before running, variables already set in the environment take priority. Read them with `process.env` in `press.config.tsx` and server code, no prefix required:
```tsx title="press.config.tsx"
const client = createClient({
token: process.env.CMS_TOKEN,
});
```
Client-side code follows the usual Vite convention: only variables prefixed with `VITE_` are exposed through `import.meta.env`.
Files that must stay readable only on the server, such as key files, belong in the `private` directory (configurable with the `privateDir` plugin option).
Also set `site.baseUrl` to your production URL, metadata, feeds, and sitemap entries resolve against it.
# Introduction (/docs)
## What is Fumapress? [#what-is-fumapress]
Fumapress is a **[React](https://react.dev) site generator** powered by [Fumadocs](https://fumadocs.dev), as compared to Fumadocs, it manages more features such as routing, while being more opinionated.
It is perfect if you want a beautiful, flexible docs, without revealing too much complexity.
Under the hood, the app is still a [Waku](https://waku.gg) app, you can easily transition to a full Fumadocs app without heavy migration.
## Getting Started [#getting-started]
### Automatic Installation [#automatic-installation]
Create a new Fumapress app.
npm
pnpm
yarn
bun
```bash
npm create fumapress
```
```bash
pnpm create fumapress
```
```bash
yarn create fumapress
```
```bash
bunx create-fumapress
```
### Manual Installation [#manual-installation]
Install the dependencies:
npm
pnpm
yarn
bun
```bash
npm i fumapress fumadocs-ui@npm:@fumadocs/base-ui fumadocs-core fumadocs-mdx @base-ui/react react react-dom
npm i vite tailwindcss @tailwindcss/vite @types/react @types/react-dom typescript -D
```
```bash
pnpm add fumapress fumadocs-ui@npm:@fumadocs/base-ui fumadocs-core fumadocs-mdx @base-ui/react react react-dom
pnpm add vite tailwindcss @tailwindcss/vite @types/react @types/react-dom typescript -D
```
```bash
yarn add fumapress fumadocs-ui@npm:@fumadocs/base-ui fumadocs-core fumadocs-mdx @base-ui/react react react-dom
yarn add vite tailwindcss @tailwindcss/vite @types/react @types/react-dom typescript --dev
```
```bash
bun add fumapress fumadocs-ui@npm:@fumadocs/base-ui fumadocs-core fumadocs-mdx @base-ui/react react react-dom
bun add vite tailwindcss @tailwindcss/vite @types/react @types/react-dom typescript --dev
```
Create the config files:
vite.config.ts
press.config.tsx
tsconfig.json
```ts
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import press from "fumapress/vite";
import { fumadocsMdx } from "fumadocs-mdx/vite";
// the config file for Vite
export default defineConfig({
plugins: [press(), fumadocsMdx(), tailwindcss()],
});
```
```tsx
import { defineConfig } from "fumapress";
import { fumadocsMdx } from "fumapress/adapters/mdx";
import { metaSchema, pageSchema } from "fumapress/adapters/mdx/schema";
import { defineDocs } from "fumadocs-mdx/macro";
// see https://fumadocs.dev/docs/mdx
const docs = defineDocs({
dir: "content",
docs: {
async: true,
schema: pageSchema,
lastModified: true,
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,
},
});
export default defineConfig({
content: docs.toFumadocsSource(),
site: {
name: "Fumapress",
},
})
// use different content sources
.adapters(fumadocsMdx());
```
```json
{
"compilerOptions": {
"target": "ES2023",
"lib": ["dom", "dom.iterable", "ES2023"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"noEmit": true
},
"exclude": ["node_modules", "dist"]
}
```
Set the package type and scripts, the app must be an ES module:
```json title="package.json"
{
// [!code ++]
"type": "module",
"scripts": {
// [!code ++:4]
"dev": "fumapress dev",
"build": "fumapress build",
"start": "fumapress start",
"types:check": "tsc --noEmit"
}
}
```
Start the dev server:
npm
pnpm
yarn
bun
```bash
npm run dev
```
```bash
pnpm run dev
```
```bash
yarn dev
```
```bash
bun run dev
```
You can now start writing Markdown files under the `content` directory.
```mdx title="content/index.mdx"
---
title: My Page
description: Hello World
---
# Overview
This is my first document.
```
## Learn More [#learn-more]
Continue with [Basics](/docs/basics) for writing content, customization, routing, and more.
# Internationalization (/docs/internationalization)
## Setup [#setup]
To configure internationalization, define an i18n config:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { defineDocs } from "fumadocs-mdx/macro";
import { defineI18n } from "fumadocs-core/i18n";
import { uiTranslations } from "fumadocs-ui/i18n";
import { fumapressTranslations } from "fumapress/i18n";
const docs = defineDocs({
dir: "content/docs",
docs: { async: true },
});
// [!code ++:4]
const i18n = defineI18n({
languages: ["cn", "en"],
defaultLanguage: "en",
});
// [!code ++]
const translations = i18n
// [!code ++:3]
.translations()
.extend(uiTranslations())
.extend(fumapressTranslations());
export default defineConfig({
content: docs.toFumadocsSource(),
// [!code ++]
translations,
});
```
You can add translations to UI like:
```tsx
const translations = i18n
.translations()
.extend(uiTranslations())
.extend(fumapressTranslations())
// [!code ++:10]
.add({
en: { displayName: "English" },
cn: {
displayName: "Chinese",
"Search(search dialog)": "搜尋文檔",
"Blog(blog)": "博客",
"All Tags(blog tags page)": "全部标签",
},
});
```
If you have other integrations and want to add translations for them, include them via `extend()` like:
```tsx
import { openapiTranslations } from "fumadocs-openapi/i18n";
import { aiTranslations } from "@fumapress/ai/i18n";
import { feedbackTranslations } from "@fumapress/feedback/i18n";
const translations = i18n
.translations()
.extend(uiTranslations())
.extend(fumapressTranslations())
// [!code ++:3]
.extend(openapiTranslations())
.extend(feedbackTranslations())
.extend(aiTranslations());
```
### Using Language Packs [#using-language-packs]
The official language pack extends `@fumadocs/language` and adds translations for Fumapress.
npm
pnpm
yarn
bun
```bash
npm i @fumapress/language
```
```bash
pnpm add @fumapress/language
```
```bash
yarn add @fumapress/language
```
```bash
bun add @fumapress/language
```
```tsx title="press.config.tsx"
import { zhCN } from "@fumapress/language/zh-cn";
const translations = i18n
.translations()
// add Simplified Chinese translations to `cn` locale [!code ++]
.preset("cn", zhCN());
```
Available presets:
* `zhCN`
* `zhTW`
When using a language pack, you do not need to include integrations with `extend()` unless you want to override the default translations.
See [Fumadocs Translations API](https://fumadocs.dev/docs/ui/translations) for adding
translations.
### Writing Content [#writing-content]
Add Markdown/JSON files for different languages by appending `.{locale}` to your file name, like:
# Docs Layout (/docs/layouts/docs)
## Overview [#overview]
Use the docs layout for documentation pages with sidebar navigation, page title, description, table of contents, view options, and last updated time.
Fumapress uses this layout by default, so you only need to configure it when you want to customize the renderer.
## Config [#config]
Define the layout and pass it through `renderPage`.
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { createDocsLayoutPage } from "fumapress/layouts/docs";
const DocsLayout = createDocsLayoutPage();
const config = defineConfig({
// [!code ++]
renderPage: (props) => ,
});
export default config;
```
Customize what the layout renders with the `render` option:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { createDocsLayoutPage } from "fumapress/layouts/docs";
const DocsLayout = createDocsLayoutPage({
async render(page) {
return {
pageProps: {
tableOfContent: {
style: "clerk",
},
},
};
},
});
const config = defineConfig({
// ...
renderPage: (props) => ,
});
export default config;
```
The `render` function can return page props, layout props, markdown URL, last modified date, or a custom body.
### Interceptors [#interceptors]
`render` returns props. For full control over the rendered components, intercept them instead:
* `renderLayout`: wraps ``.
* `renderPage`: wraps ``.
* `renderBody`: wraps the body element.
Each receives the resolved props and a `next` function rendering the original component:
```tsx title="press.config.tsx"
const DocsLayout = createDocsLayoutPage({
renderPage({ props, next }) {
return next({
...props,
children: (
<>
{props.children}
>
),
});
},
});
```
By default, layouts merge `defaultLayoutProps` into their props, set `inherit: { layoutProps: false }` to opt out.
# Glass Layout (/docs/layouts/glass)
## Overview [#overview]
Use the glass layout for docs pages that should keep docs features, but use the glass appearance from Fumadocs UI: the sidebar and header sit on top of the content as floating, translucent panels.
It requires Fumadocs UI v16.12.0 or above.
## Setup [#setup]
Unlike other layouts, glass layout styles are not bundled into the Fumadocs UI CSS preset. Import them in `src/app.css`:
```css title="src/app.css"
@import "tailwindcss";
/* [!code ++] */
@import "fumadocs-ui/css/generated/glass.css";
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";
@import "fumapress/css/preset.css";
```
## Config [#config]
Define the layout and pass it through `renderPage`.
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { createGlassLayoutPage } from "fumapress/layouts/glass";
const GlassLayout = createGlassLayoutPage();
const config = defineConfig({
// ...
renderPage: (props) => ,
});
export default config;
```
The glass layout accepts the same kind of `render` option as the docs layout:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { createGlassLayoutPage } from "fumapress/layouts/glass";
const GlassLayout = createGlassLayoutPage({
async render(page) {
let pageTree = (await this.getLoader()).getPageTree(page.locale);
return {
layoutProps: {
tree: pageTree,
},
pageProps: {
full: true,
},
};
},
});
const config = defineConfig({
// ...
renderPage: (props) => ,
});
export default config;
```
The `render` function can return page props, layout props, markdown URL, last modified date, or a custom body.
The glass layout is a client component, you cannot pass unserializable values (e.g. functions) in `layoutProps` unless they are supported slots.
## AI Chat [#ai-chat]
The [AI plugin](/docs/plugins/ai) integrates with the glass layout natively: the Ask AI trigger is shown in the layout header & sidebar, using its `aiChat` option.
# Home Layout (/docs/layouts/home)
## Overview [#overview]
Use the home layout for standalone content pages that only need shared navbar.
It is a good fit for marketing pages, landing pages, or custom content pages that should live in your content source.
## Config [#config]
Define the layout and pass it through `renderPage`.
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { createHomeLayoutPage } from "fumapress/layouts/home";
const HomePage = createHomeLayoutPage();
const config = defineConfig({
// ...
renderPage: (props) => ,
});
export default config;
```
Customize the rendered body or layout props with the `render` option:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { createHomeLayoutPage } from "fumapress/layouts/home";
const HomePage = createHomeLayoutPage({
render(page) {
return {
layoutProps: {
nav: {
title: page.data.title,
},
},
};
},
});
const config = defineConfig({
// ...
renderPage: (props) => ,
});
export default config;
```
## File-based Layout [#file-based-layout]
For custom route files, use `createHomeLayout()` directly as a wrapper:
```tsx title="src/pages/_layout.tsx"
import type { ReactNode } from "react";
import { createHomeLayout } from "fumapress/layouts/home";
import type PressConfig from "../../press.config";
const HomeLayout = createHomeLayout();
export default function Layout({ children }: { children: ReactNode }) {
return {children};
}
```
The `` is a normal component that you can use anywhere.
## Layout Props [#layout-props]
`createHomeLayout` accepts `layoutProps` to configure the navbar, deep-merged with `defaultLayoutProps`:
```tsx title="press.config.tsx"
import { createHomeLayout } from "fumapress/layouts/home";
import { BookIcon } from "lucide-react";
export const HomeLayout = createHomeLayout({
layoutProps: {
links: [
{
url: "/docs",
text: "Documentation",
icon: ,
active: "nested-url",
},
{
url: "https://github.com/acme/docs",
text: "GitHub",
external: true,
},
],
},
});
```
Set `inherit: { layoutProps: false }` to opt out of `defaultLayoutProps`.
A layout defined this way can be shared across your app, as a file-based layout and with plugins that render their own pages:
src/pages/_layout.tsx
press.config.tsx
```tsx
export { HomeLayout as default } from "../../press.config";
```
```tsx
config.plugins(
blogPlugin({
layouts: { layout: HomeLayout },
}),
);
```
# Layouts (/docs/layouts)
## Overview [#overview]
Layouts are page renderers. They decide how a content page is wrapped, how the page body is rendered, and which UI pieces like sidebar, table of contents, navbar, and footer are shown.
Fumapress uses the docs layout by default. Pick another layout when a page needs a different reading experience, or switch between layouts per page with your own renderer.
For documentation pages with sidebar navigation, table of contents, page title, and page
metadata.
For reference-style pages that keep docs features with the notebook appearance from Fumadocs UI.
For docs pages with the glass appearance from Fumadocs UI, where sidebar and header are shown as
floating translucent panels.
For standalone pages that need the shared navbar and layout props without docs page chrome.
## Shared Props [#shared-props]
Define `defaultLayoutProps` to share props across Fumadocs layouts. These props are merged into layouts that inherit default props, such as docs, notebook, and home layouts:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
export default defineConfig({
// ...
defaultLayoutProps: {
nav: {
title: "My Docs",
},
links: [
{
text: "GitHub",
url: "https://github.com/acme/docs",
},
],
},
});
```
## Switching Layouts [#switching-layouts]
`renderPage` is just a function that returns JSX node. To switch between layouts for every page, create typed layout components and forward props to the one you want:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { createDocsLayoutPage } from "fumapress/layouts/docs";
import { createNotebookLayoutPage } from "fumapress/layouts/notebook";
const DocsPage = createDocsLayoutPage();
const NotebookPage = createNotebookLayoutPage();
const config = defineConfig({
// ...
renderPage: (props) => {
if (props.page.path.startsWith("reference/")) {
return ;
}
return ;
},
});
export default config;
```
With multiple content sources, switch on `page.type` instead, see [Content Collections](/docs/collections).
## File-based Layouts [#file-based-layouts]
File-based routes in `src/pages` use Waku's file-based routing model. Use `_layout.tsx` files to apply layouts on children routes.
Define & export layouts:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { createHomeLayout } from "fumapress/layouts/home";
const config = defineConfig({
// ...
});
export const HomeLayout = createHomeLayout();
export default config;
```
To apply `HomeLayout` on all file-based routes under `/`:
```tsx title="src/pages/_layout.tsx"
export { HomeLayout as default } from "../../press.config";
```
# Notebook Layout (/docs/layouts/notebook)
## Overview [#overview]
Use the notebook layout for reference pages that should keep docs features, but use the notebook appearance from Fumadocs UI.
It is a good fit for API references, generated content, or dense technical pages where the notebook style is easier to scan than the default docs page.
## Config [#config]
Define the layout and pass it through `renderPage`.
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { createNotebookLayoutPage } from "fumapress/layouts/notebook";
const NotebookLayout = createNotebookLayoutPage();
const config = defineConfig({
// ...
renderPage: (props) => ,
});
export default config;
```
The notebook layout accepts the same kind of `render` option as the docs layout:
```tsx title="press.config.tsx"
import { defineConfig } from "fumapress";
import { createNotebookLayoutPage } from "fumapress/layouts/notebook";
const NotebookLayout = createNotebookLayoutPage({
async render(page) {
let pageTree = (await this.getLoader()).getPageTree(page.locale);
return {
layoutProps: {
tree: pageTree,
},
pageProps: {
full: true,
},
};
},
});
const config = defineConfig({
// ...
renderPage: (props) => ,
});
export default config;
```
The `render` function can return page props, layout props, markdown URL, last modified date, or a custom body.
# Markdown Output (/docs/markdown)
## Overview [#overview]
Fumapress serves pages as Markdown for LLMs and AI agents through the [llms.txt plugin](/docs/plugins/llms.txt). Content pages get their Markdown from the content source, while custom routes built with React can define their own Markdown form with `fumapress/markdown`.
## API [#api]
### `asMarkdown` [#asmarkdown]
Inside a server component, `asMarkdown()` returns whether the current render targets Markdown, and calling it opts the component in:
```tsx
import { asMarkdown, md } from "fumapress/markdown";
function Callout({ title, children }) {
if (asMarkdown()) return md.linePrefix("> ")`**${title}**\n${children}`;
return
...
;
}
```
Components that never call it are kept as JSX syntax in the output (`...`), and so are client components since they don't run during the server render. Wrap a client component in a server component to give it a Markdown form.
### `md` [#md]
A tagged template returning a `Promise`. Interpolated values can be strings, React nodes, or arrays and promises of them, rendered to Markdown in place:
```tsx
export default function Page() {
if (asMarkdown()) {
return md`
# My Site
Welcome! Read the [docs](/docs).
`;
}
return ...;
}
```
Indentation from the source code is stripped, so templates stay correct when your formatter indents them.
Use `md.linePrefix(prefix)` to prefix every line of a block, such as `"> "` for blockquotes, and `md.indent(size)` to indent it, such as content nested under a list item:
```tsx
md`
- **${item.title}**
${md.indent()`${item.body}`}
`;
```
### `renderToMarkdown` [#rendertomarkdown]
Renders a React tree to a Markdown string yourself:
```tsx
import { renderToMarkdown } from "fumapress/markdown";
const text = await renderToMarkdown();
```
Standard HTML elements are converted to their Markdown equivalents, non-content tags like `