# Fumadocs (Framework Mode): Comparisons
URL: /docs/comparisons
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/comparisons.mdx
How is Fumadocs different from other existing frameworks?
## Nextra [#nextra]
Fumadocs is highly inspired by Nextra. For example, the Routing Conventions. That is why
`meta.json` also exists in Fumadocs.
Nextra is more opinionated than Fumadocs, as a side effect, you have to
configure things manually compared to simply editing a configuration file.
Fumadocs works great if you want more control over everything, such as
adding it to an existing codebase or implementing advanced routing.
### Feature Table [#feature-table]
| Feature | Fumadocs | Nextra |
| ------------------- | ------------ | ------------------------- |
| Static Generation | Yes | Yes |
| Cached | Yes | Yes |
| Light/Dark Mode | Yes | Yes |
| Syntax Highlighting | Yes | Yes |
| Table of Contents | Yes | Yes |
| Full-text Search | Yes | Yes |
| i18n | Yes | Yes |
| Last Git Edit Time | Yes | Yes |
| Page Icons | Yes | Yes, via `_meta.js` files |
| RSC | Yes | Yes |
| Remote Source | Yes | Yes |
| SEO | Via Metadata | Yes |
| Built-in Components | Yes | Yes |
| RTL Layout | Yes | Yes |
### Additional Features [#additional-features]
Features supported via 3rd party libraries like [TypeDoc](https://typedoc.org) will not be listed here.
| Feature | Fumadocs | Nextra |
| -------------------------- | -------- | ------ |
| OpenAPI Integration | Yes | No |
| TypeScript Docs Generation | Yes | No |
| TypeScript Twoslash | Yes | Yes |
## Mintlify [#mintlify]
Mintlify is a documentation service, as compared to Fumadocs, it offers a free tier but isn't completely free and open source.
Fumadocs is not as powerful as Mintlify, for example, the OpenAPI integration of Mintlify.
As the creator of Fumadocs, I wouldn't recommend switching to Fumadocs from Mintlify if you're satisfied with the current way you build docs.
However, I believe Fumadocs is a suitable tool for all React.js developers who want to have elegant docs.
## Docusaurus [#docusaurus]
Docusaurus is a powerful framework based on React.js. It offers many cool
features with plugins and custom themes.
### Lower Complexity [#lower-complexity]
As Fumadocs is designed to integrate with React frameworks, you may need more knowledge of React.js to get started.
In return, Fumadocs have better customizability.
For a simple docs, Docusaurus might be a better choice if you don't need any framework-specific functionality.
### Plugins [#plugins]
You can easily achieve many things with plugins, their ecosystem is indeed larger and maintained by many contributors.
In comparison, the flexibility of Fumadocs allows you to implement them on your own, it may take longer to tune it to your satisfaction.
# Fumadocs (Framework Mode): Deploying
URL: /docs/deploying
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/deploying/index.mdx
Deploy your Fumadocs app
## Overview [#overview]
Your Fumadocs app is powered by the underlying React framework (e.g. Next.js), you will need to check the relevant docs of your React framework for deployment guides.
- [Next.js](https://nextjs.org/docs/app/getting-started/deploying)
- [React Router](https://reactrouter.com/start/framework/deploying)
- [Tanstack Start](https://tanstack.com/start/latest/docs/framework/react/guide/hosting)
- [Waku](https://waku.gg/#deployment)
If you are looking to host the app as a SPA app (fully static, hostable on CDN), see [Static Build](/docs/deploying/static).
There's some extra notes on specific combinations:
### Next.js + Cloudflare [#nextjs--cloudflare]
Use https://opennext.js.org/cloudflare, Fumadocs doesn't work on Edge runtime.
### Next.js + Docker [#nextjs--docker]
If you want to deploy your Fumadocs app using Docker with **Fumadocs MDX configured**, make sure to add the `source.config.ts` file and your `next.config.js` file to the `WORKDIR` in the Dockerfile.
The following snippet is taken from the official [Next.js Dockerfile Example](https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile):
```zsh title="Dockerfile"
# syntax=docker.io/docker/dockerfile:1
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager [!code highlight]
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* source.config.ts next.config.* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
```
This ensures Fumadocs MDX can access your configuration file during builds.
# Fumadocs (Framework Mode): Static Build
URL: /docs/deploying/static
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/deploying/static.mdx
Output static website with Fumadocs.
## Setup [#setup]
By default, Fumadocs use a server-first approach which always requires a running server to serve.
You can output a static build by configuring your React framework.
### Search [step] [#search]
#### Built-in Search [#built-in-search]
1. [Configure Search Server](/docs/headless/search/orama#static-export).
2. [Configure Search UI/Client](/docs/search/orama#static).
After the configurations, your app will statically store the search indexes, and search will be computed on browser instead.
#### Cloud Solutions [#cloud-solutions]
Since the search functionality is powered by remote servers, static export works without configuration.
### Deployment [step] [#deployment]
#### Next.js [#nextjs]
You can enable Next.js static export, it allows you to export the app as a static HTML site without a Node.js server.
```js title="next.config.mjs"
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
output: 'export',
// Optional: Change links `/me` -> `/me/` and emit `/me.html` -> `/me/index.html`
// trailingSlash: true,
// Optional: Prevent automatic `/me` -> `/me/`, instead preserve `href`
// skipTrailingSlashRedirect: true,
};
```
See [Next.js docs](https://nextjs.org/docs/app/guides/static-exports) for limitations and details.
#### React Router [#react-router]
By configuring SPA mode, you can serve the `build/client` directory as a static website.
On SPA mode, all your server-side loaders must be pre-rendered.
See [React Router Docs](https://reactrouter.com/how-to/spa) for details.
```ts title="react-router.config.ts"
import type { Config } from '@react-router/dev/config';
import { glob } from 'node:fs/promises';
import { createGetUrl, getSlugs } from 'fumadocs-core/source';
const getUrl = createGetUrl('/docs');
export default {
// disable SSR [!code highlight]
ssr: false,
// [!code ++:9]
async prerender({ getStaticPaths }) {
const paths: string[] = [...getStaticPaths()];
for await (const entry of glob('**/*.mdx', { cwd: 'content/docs' })) {
paths.push(getUrl(getSlugs(entry)));
}
return paths;
},
} satisfies Config;
```
#### Tanstack Start [#tanstack-start]
Configure SPA mode and pre-rendering, refer to [SPA Mode](https://tanstack.com/start/latest/docs/framework/react/guide/spa-mode) for deploying SPA apps with Tanstack Start.
```ts title="vite.config.ts"
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
// ...other plugins
tanstackStart({
spa: {
enabled: true,
// Tanstack Router will automatically crawl your pages [!code ++:4]
prerender: {
enabled: true,
},
// if you have any hidden paths that's not visible on UI, you can add them explicitly. [!code highlight:5]
pages: [
{
path: '/docs/test',
},
],
},
}),
],
});
```
#### Waku [#waku]
Waku can serve your site statically when all pages are configured with `static` render mode.
See [Deployment](https://waku.gg/#deployment) for details.
# Fumadocs (Framework Mode): Visual Editor
URL: /docs/editor
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/editor.mdx
Write Markdown and MDX visually, with your files as the source of truth.
## Introduction [#introduction]
[Fumadocs Editor](https://editor.fumadocs.dev) is a visual editor for Fumadocs MDX.
### Start with Studio [#start-with-studio]
Studio opens your local docs in a browser without adding an editor to your app.
From your Fumadocs project, run:
```bash
npx @fumadocs-editor/studio
```
Studio requires **Node.js 24 or later**. It looks for `content/docs`, or the parent directories when missing.
To select a different content folder:
```bash
npx @fumadocs-editor/studio --root ./docs
```
Edits autosave to disk, and changes from your code editor merge into the open document.
The file list follows your `meta.json` ordering.
Review the changes in Git and publish them through your site's usual deployment workflow.
> Studio edits existing files. Create, rename, and delete files in your code editor or file manager instead.
### Use as a Library [#use-as-a-library]
Use `@fumadocs-editor/ui` to build an editing interface inside your own app. See the docs for details:
Install the editor, import its styles, and render your first editable document.
Configure the content folder, custom components, and styles.
Connect an embedded editor to files through your development server.
### Write with Components [#write-with-components]
Built-in editing support includes Fumadocs components such as Callout, Cards, Steps, Accordions, and Files.
Custom components can define their own editable regions with [component specs](https://editor.fumadocs.dev/components).
The editor works on your Markdown and MDX files. Continue using [Fumadocs MDX](/docs/mdx) to load
them and your React framework to render the published site.
# Fumadocs (Framework Mode): Access Control
URL: /docs/guides/access-control
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/guides/access-control.mdx
Limit the access of content.
## Overview [#overview]
Thanks to the flexible design, it is simple to implement access control with Fumadocs.
## Loader API [#loader-api]
If you are using [`loader()`](/docs/headless/source-api) API, you can create the loaders on-demand, and filter files from content source to restrict accessible content.
For example, this setup with Fumadocs MDX allows only MDX files with frontmatter `permission: public` to be shown.
```ts title="lib/source.ts"
import { docs } from 'collections/server';
import { loader, update } from 'fumadocs-core/source';
const filteredSource = update(docs.toFumadocsSource())
.files((files) =>
files.filter((file) => {
// keep meta files (e.g. `meta.json`)
if (file.type === 'meta') return true;
// access file data (type-safe)
return file.data.permission === 'public';
}),
)
.build();
export const source = loader(filteredSource, {
baseUrl: '/docs',
});
```
Or with more complicated setup, you may turn `source` into a factory function.
```ts tab="lib/source.ts"
import { docs } from 'collections/server';
import { loader, update } from 'fumadocs-core/source';
// uncached, it's better to cache it in-memory for real use case.
export function createSource(permission: 'public' | 'admin') {
const filteredSource = update(docs.toFumadocsSource())
.files((files) =>
files.filter((file) => {
if (file.type === 'meta') return true;
return file.data.permission === permission;
}),
)
.build();
return loader(filteredSource, {
baseUrl: '/docs',
});
}
```
```ts tab="References"
// [!code --]
import { source } from '@/lib/source';
// [!code ++:4]
import { createSource } from '@/lib/source';
const user = await getUser();
const source = createSource(user.permission);
const page = source.getPage(params.slugs);
```
### Advantages [#advantages]
What is special with this approach is that **the access is limited at input level**, rather than at rendering phase or build-time.
This guarantees the protected content is filtered entirely, and without compromise on flexibility.
## Custom Implementation [#custom-implementation]
Without using Loader API, you can also implement your access control at framework-level (e.g. via Next.js routing & proxy).
For example, to limit access to certain pages:
```tsx title="page.tsx"
import { notFound } from 'next/navigation';
import { source } from '@/lib/source';
export default async function Page({ params }) {
const { slugs } = await params;
const user = await getUser();
const page = source.getPage(slugs);
if (!page) notFound();
if (page.data.permission !== user.permission) notFound();
// render page...
}
```
Note that it is up to you to manage the page tree (sidebar items), search and other details.
This approach offers the best flexibility, but it doesn't rely on Fumadocs itself for access control, the setup may take longer and be more complicated.
It is better to consult the docs of your React.js framework when implementing.
# Fumadocs (Framework Mode): Customize UI
URL: /docs/guides/customize-ui
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/guides/customize-ui.mdx
A complete guide on how to customize Fumadocs UI.
## Introduction [#introduction]
Fumadocs UI provides pre-built components for simplicity, serving as a middleground between "installing all UI code to your codebase" and an opinionated package.
While Fumadocs UI itself has many designs around flexibility & backward compatibility, if used incorrectly, the measures may take no effect.
> It is also recommended to feed this guide to your AI agents, if they may edit your UI.
### Pick the Right Component [step] [#pick-the-right-component]
For layout UIs, there's different built-in layout components you can try:
The layout of documentation
A compact version of Docs Layout
Shared layout for other pages
An aggressively minimal layout for docs
A docs layout with a floating, translucent sidebar and header
A less compact version of Docs Layout
### Props [step] [#props]
You can first check if the options of components can suffice your needs, for example:
```tsx
{/* content */}
```
Or styling the container via props:
```tsx
{/* content */}
```
Detailed documentation for each component is in the [Fumadocs UI](/docs/ui) section, you can take it as a reference.
Most options support passing JSX elements to allow your own components.
This approach should suffice most use cases, such as adding sections below table of contents or in sidebar.
### CSS [step] [#css]
When the styling is more complicated, some components of Fumadocs UI attaches its own `id` & `data-` attributes for selecting.
You can target them in your CSS selectors:
```css
#nd-docs-layout #nd-subnav {
background-color: purple;
}
[data-toc-popover] {
background-color: purple;
}
```
**Avoid invasive styles, or anything that presume a very specific DOM structure**, as it is difficult to guarantee backward compatibility, especially with newer UI features.
```css title="Bad Example"
/* avoid targeting elements like this! */
[data-toc-popover] > div {
background-color: purple;
}
```
CSS approach can be risky as only the `id` & `data-` attributes can be versioned under its own life cycles, prefer other approaches if possible.
### Install via CLI [step] [#install-via-cli]
For advanced needs beyond the exposed options, or if your custom CSS styles became overly invasive, Fumadocs CLI is a powerful tool.
You can find similar prompts under the relevant component docs, indicating the component is installable, such as:
For **layout components** (which often composes multiple components), Fumadocs CLI has a special `customize` command with granular choices.
```npm
npx @fumadocs/cli@latest customize
```
Despite installing the entire layout component, you can choose to install a **slot** (a replaceable part of the layout component).
Run the command above and you can view the full options.
When installed, make sure to use the installed types over the types exported from Fumadocs UI. For example, if you have installed `layout/docs`, update your imports:
```tsx
// [!code --]
import type { DocsLayoutProps } from 'fumadocs-ui/layouts/docs';
// [!code ++]
import type { DocsLayoutProps } from '@/components/layout/docs';
```
This ensure your local components will be decoupled from Fumadocs UI.
While Fumadocs CLI offers full control for the UI code, it is worth mentioning that:
- You won't be able to receive UI updates for the installed the components.
- You can re-install the component with Fumadocs CLI, but prior customisations will be erased. (You can choose which file to overwrite)
- Fumadocs CLI will assume the latest Fumadocs Core + UI to be used.
## Other Questions [#other-questions]
Feel free to reach out in [GitHub Discussions](https://github.com/fuma-nama/fumadocs/discussions) for questions, or if you have encountered unexpected breakages.
# Fumadocs (Framework Mode): Export EPUB
URL: /docs/guides/export-epub
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/guides/export-epub.mdx
Export your documentation to EPUB format for e-readers
## Introduction [#introduction]
The EPUB integration lets you export your Fumadocs documentation as an EPUB file, making it easy for readers to download and read your docs on e-readers, tablets, or any EPUB-compatible app.
It converts your MDX content to HTML, resolves images, and produces a standards-compliant EPUB with a table of contents.
## Setup [#setup]
Fumadocs CLI can set it up for you, it creates the export route protected by an `EXPORT_SECRET` and enables `includeProcessedMarkdown`:
```package-install
npx @fumadocs/cli feature epub
```
Or manually:
### Installation [#installation]
```npm
npm install fumadocs-epub
```
### Enable Processed Markdown [#enable-processed-markdown]
EPUB export requires `includeProcessedMarkdown` in your docs collection config:
```ts title="source.config.ts"
import { defineDocs } from 'fumadocs-mdx/config';
export const docs = defineDocs({
docs: {
postprocess: {
// [!code ++]
includeProcessedMarkdown: true,
},
},
});
```
### Create Export Route [#create-export-route]
`createEpubExportAPI` builds the route handler. It is guarded by `secret`: without a matching `Authorization: Bearer ` header the request is rejected, and the route answers `503` when the secret itself is unset.
```ts tab="Next.js" title="app/export/epub/route.ts"
import { source } from '@/lib/source';
import { createEpubExportAPI } from 'fumadocs-epub';
export const revalidate = false;
export const { GET } = createEpubExportAPI({
source,
title: 'My Documentation',
author: 'My Team',
description: 'Documentation for my project',
cover: '/cover.png',
secret: process.env.EXPORT_SECRET,
});
```
```ts tab="React Router" title="app/routes/export.epub.ts"
import type { Route } from './+types/export.epub';
import { source } from '@/lib/source';
import { createEpubExportAPI } from 'fumadocs-epub';
const api = createEpubExportAPI({
source,
title: 'My Documentation',
secret: process.env.EXPORT_SECRET,
});
export function loader({ request }: Route.LoaderArgs) {
return api.GET(request);
}
```
```ts tab="React Router" title="app/routes.ts"
import { route, type RouteConfig } from '@react-router/dev/routes';
export default [
// [!code ++]
route('export/epub', 'routes/export.epub.ts'),
] satisfies RouteConfig;
```
```ts tab="Tanstack Start" title="src/routes/export/epub.ts"
import { createFileRoute } from '@tanstack/react-router';
import { source } from '@/lib/source';
import { createEpubExportAPI } from 'fumadocs-epub';
const api = createEpubExportAPI({
source,
title: 'My Documentation',
secret: process.env.EXPORT_SECRET,
});
export const Route = createFileRoute('/export/epub')({
server: {
handlers: {
GET: ({ request }) => api.GET(request),
},
},
});
```
```ts tab="Waku" title="pages/_api/export/epub.ts"
import { source } from '@/lib/source';
import { createEpubExportAPI } from 'fumadocs-epub';
export const { GET } = createEpubExportAPI({
source,
title: 'My Documentation',
secret: process.env.EXPORT_SECRET,
});
```
For a public endpoint, or to build the file yourself (e.g. in a script), call `exportEpub()` directly and return the buffer.
### Using the CLI [#using-the-cli]
You can also export EPUB via the Fumadocs CLI:
```bash
fumadocs export epub --framework next
# or: astro | tanstack-start | react-router | waku
```
This fetches from your running server (Next.js) or copies from the build output (other frameworks). Run a production build first for non-Next.js frameworks.
## Options [#options]
Both `exportEpub` and `createEpubExportAPI` accept these options:
| Option | Type | Description |
| -------------- | -------------------- | ------------------------------------------------------------------------------- |
| `title` | `string` | **Required.** Book title |
| `author` | `string \| string[]` | Author name(s). Default: `'anonymous'` |
| `description` | `string` | Book description |
| `language` | `string` | Language code (e.g. `'en'`). Default: `'en'` |
| `publisher` | `string` | Publisher name. Default: `'anonymous'` |
| `isbn` | `string` | ISBN |
| `cover` | `string` | Cover image. Supports `file://`, `http(s)://`, `/public/...`, or relative paths |
| `outputPath` | `string` | If set, writes the EPUB to file in addition to returning the buffer |
| `includePages` | `(page) => boolean` | Filter: include only pages where this returns `true` |
| `excludePages` | `(page) => boolean` | Filter: exclude pages where this returns `true` |
| `css` | `string` | Custom CSS for the EPUB. Uses `defaultEpubStyles` if omitted |
| `publicDir` | `string` | Public directory for resolving `/public/...` image paths. Default: `./public` |
`createEpubExportAPI` adds two more: `secret` (required, guards the route) and `filename` (the downloaded file name, default `docs.epub`).
### Custom CSS [#custom-css]
You can override the default EPUB styles:
```ts
import { exportEpub, defaultEpubStyles } from 'fumadocs-epub';
const buffer = await exportEpub({
source,
title: 'My Docs',
author: 'Me',
css: `${defaultEpubStyles}
/* Custom overrides */
body { font-size: 1.1em; }
`,
});
```
### Filtering Pages [#filtering-pages]
Use `includePages` and `excludePages` to control which docs are exported:
```ts
const buffer = await exportEpub({
source,
title: 'My Docs',
author: 'Me',
// Only include docs under /docs/getting-started
includePages: (page) => page.path.startsWith('getting-started'),
// Exclude specific paths
excludePages: (page) => page.path === 'changelog',
});
```
### Image Resolution [#image-resolution]
Images in your MDX are resolved automatically:
- **Relative paths** (e.g. `./image.png`) — resolved relative to the page file
- **Public paths** (e.g. `/og.png`) — resolved from the `public` directory
- **Remote URLs** (e.g. `https://...`) — embedded as-is
# Fumadocs (Framework Mode): Export PDF
URL: /docs/guides/export-pdf
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/guides/export-pdf.mdx
Export docs pages as PDF documents
## Overview [#overview]
In general, we strongly recommend you to download the entire website (HTML files, etc) for offline browsing.
In case if you want to export the page as a PDF, you can follow this guide.
## Setup [#setup]
Use Puppeteer to export PDF files.
```ts title="scripts/export-pdf.ts"
import puppeteer from 'puppeteer';
import fs from 'node:fs/promises';
import path from 'node:path';
const browser = await puppeteer.launch();
const outDir = 'pdfs';
// update this [!code highlight]
const urls = ['/docs/ui', '/docs/ui/customizations'];
async function exportPdf(pathname: string) {
const page = await browser.newPage();
await page.goto('http://localhost:3000' + pathname, {
waitUntil: 'networkidle2',
});
await page.pdf({
path: path.join(outDir, pathname.slice(1).replaceAll('/', '-') + '.pdf'),
width: 950,
printBackground: true,
});
console.log(`PDF generated successfully for ${pathname}`);
await page.close();
}
await fs.mkdir(outDir, { recursive: true });
await Promise.all(urls.map(exportPdf));
await browser.close();
```
Add the following to your Tailwind CSS file to disable navigation elements when printing:
```css
@media print {
#nd-docs-layout {
--fd-sidebar-width: 0px !important;
}
#nd-sidebar {
display: none;
}
}
```
You can now run the script:
```bash
bun ./scripts/export-pdf.ts
```
### Invisible Contents [#invisible-contents]
For invisible contents in accordions/tabs, you may need to temporarily override the MDX components. For example:
```tsx title="components/mdx.tsx"
import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
// you may use environment variable here [!code highlight]
const isPrinting = true;
function PrintingAccordion(props: React.ComponentProps) {
return (
{props.title}
{props.children}
);
}
function PrintingAccordions(props: React.ComponentProps) {
return
{props.children}
;
}
export function getMDXComponents(components?: MDXComponents) {
return {
...defaultMdxComponents,
// updated accordions:
Accordion: isPrinting ? PrintingAccordion : Accordion,
Accordions: isPrinting ? PrintingAccordions : Accordions,
...components,
} satisfies MDXComponents;
}
```
# Fumadocs (Framework Mode): Guides
URL: /docs/guides
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/guides/index.mdx
Useful guides for extending Fumadocs.
# Fumadocs (Framework Mode): RSS Feed
URL: /docs/guides/rss
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/guides/rss.mdx
Generate a RSS feed for your docs/blog.
## Setup [#setup]
You can create the feed object:
```ts title="lib/rss.ts"
import { Feed } from 'feed';
import { source } from '@/lib/source';
const baseUrl = 'https://fumadocs.dev';
export function getRSS() {
const feed = new Feed({
title: 'Fumadocs Blog',
id: `${baseUrl}/blog`,
link: `${baseUrl}/blog`,
language: 'en',
image: `${baseUrl}/banner.png`,
favicon: `${baseUrl}/icon.png`,
copyright: 'All rights reserved 2025, Fuma Nama',
});
for (const page of source.getPages()) {
feed.addItem({
id: page.url,
title: page.data.title,
description: page.data.description,
link: `${baseUrl}${page.url}`,
date: new Date(page.data.lastModified),
author: [
{
name: 'Fuma',
},
],
});
}
return feed.rss2();
}
```
And expose it using a server loader/route handler like:
```ts tab="Next.js" title="app/rss.xml/route.ts"
import { getRSS } from '@/lib/rss';
export const revalidate = false;
export function GET() {
return new Response(getRSS());
}
```
```ts tab="React Router" title="app/routes/rss.ts"
import { getRSS } from '@/lib/rss';
export async function loader() {
return new Response(getRSS());
}
```
```ts tab="React Router" title="app/routes.ts"
import { route, type RouteConfig } from '@react-router/dev/routes';
export default [
// [!code ++]
route('rss.xml', 'routes/rss.ts'),
] satisfies RouteConfig;
```
```ts tab="Tanstack Start" title="src/routes/rss[.]xml.ts"
import { createFileRoute } from '@tanstack/react-router';
import { getRSS } from '@/lib/rss';
export const Route = createFileRoute('/rss.xml')({
server: {
handlers: {
GET: async () => new Response(getRSS()),
},
},
});
```
```ts tab="Waku" title="pages/_api/rss.xml.ts"
import { getRSS } from '@/lib/rss';
export function GET() {
return new Response(getRSS());
}
export async function getConfig() {
return {
render: 'static' as const,
} as const;
}
```
### Next.js Metadata [#nextjs-metadata]
You can add an alternates object to the metadata object with your feed’s title and URL.
```ts title="app/layout.tsx"
import type { Metadata } from 'next';
export const metadata: Metadata = {
alternates: {
types: {
'application/rss+xml': [
{
title: 'Fumadocs Blog',
url: 'https://fumadocs.dev/blog/index.xml',
},
],
},
},
};
```
# Fumadocs (Framework Mode): Quick Start
URL: /docs
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/index.mdx
Getting Started with Fumadocs
## Introduction [#introduction]
Fumadocs (Foo-ma docs) is a **documentation framework**, designed to be fast, flexible,
and composes seamlessly into your React framework. It consists of multiple layers:
} title='Fumadocs Core' href='/docs/headless'>
Handles most of the logic, including document search, content source adapters, and Markdown extensions.
} title='Fumadocs UI' href='/docs/ui'>
The default theme of Fumadocs offers a beautiful look for documentation sites and interactive components.
} title='Content Source'>
The source of your content, can be a CMS or local data layers like [Fumadocs MDX](/docs/mdx) (the official content source).
} title='Fumadocs CLI' href='/docs/cli'>
A command line tool to install UI components and automate things, useful for customizing layouts.
Read our in-depth [What is Fumadocs](/docs/what-is-fumadocs) introduction.
### Terminology [#terminology]
**Markdown/MDX:** Markdown is a markup language for creating formatted text. Fumadocs natively supports Markdown and MDX (superset of Markdown).
**[Bun](https://bun.sh):** A JavaScript runtime, it will be used in our guides for running scripts. If you prefer Node.js, try [unrun](https://gugustinette.github.io/unrun/guide/getting-started.html#cli) instead.
Some basic knowledge of React.js would be useful for further customizations.
[Fumapress](https://press.fumadocs.dev) is a site generator that manages more features on top of Fumadocs.
- Automatic routing, `llms.txt`, MCP and more basic features.
- Based on Vite ([Waku](https://waku.gg)).
It enforces a React framework of its own, and offers a better DX in return.
## Automatic Installation [#automatic-installation]
A minimum version of Node.js 22 required.
```npm
npm create fumadocs-app
```
It will ask you the built-in template to use:
- **Framework**: Next.js, Astro (with React), Waku, React Router, Tanstack Start.
- **Content source**: Fumadocs MDX.
A new fumadocs app should be initialized, with following features configured by default:
- [LLM integration](/docs/integrations/llms).
- [Dynamic metadata image](/docs/integrations/og).
You can follow the [Manual Installation](/docs/manual-installation) guide to get started.
### Enjoy! [#enjoy]
Create your first MDX file in the docs folder.
```mdx title="content/docs/index.mdx"
---
title: Hello World
---
## Yo what's up
```
Run the app in development mode and see http://localhost:3000/docs.
```npm
npm run dev
```
## FAQ [#faq]
Some common questions you may encounter.
Make sure to upgrade Fumadocs when you've encountered any problems or trying out new features:
```bash title="pnpm"
pnpm update -i -r --latest
```
Routing is handled by your React framework, you need to change the routing structure first.
For example, in Next.js, rename the route (`/docs/*` -> `/info/*`):
Or rename from `/docs/*` to `/*` using a route group:
Finally, update the base URL of pages in `source.ts`:
```ts title="lib/source.ts"
import { loader } from 'fumadocs-core/source';
export const source = loader({
baseUrl: '/info', // to the new value [!code highlight]
});
```
We recommend using [Layout Tabs](/docs/ui/layouts/docs#layout-tabs).
### For Vite [#for-vite]
There's some weird pre-bundling problems with Vite: [#3910](https://github.com/vitejs/vite/issues/3910).
Make sure to exclude Fumadocs from pre-bundling and add it to `noExternal`:
```ts title="vite.config.ts"
import { defineConfig } from 'vite';
export default defineConfig({
resolve: {
// add other Fumadocs deps as needed
noExternal: ['fumadocs-core', 'fumadocs-ui', 'fumadocs-openapi', '@fumadocs/base-ui'],
},
});
```
### For Next.js [#for-nextjs]
If you have similar Turbopack errors like:
```
JSON file not found: FileSystemPath { ... path: "lib/locales/sources/file.json.json" }
```
It is usually because `require(esm)` is not supported ([#3312](https://github.com/fuma-nama/fumadocs/issues/3312)), please upgrade to Node.js 22+.
On Node.js 23.1, if you encountered this error in production build:
```
Could not find a production build in the '.next' directory.
```
It is a known problem, see [#1021](https://github.com/fuma-nama/fumadocs/issues/1021), you can upgrade your Node.js version to 23.6.0+.
Next.js turns dynamic route into static routes when `generateStaticParams` is configured.
Hence, it is as fast as static pages.
You can [enable Static Exports](/docs/deploying/static) on Next.js to get a static build output.
Same as managing layouts in Next.js App Router, remove the original MDX file from content directory (`/content/docs`).
This ensures duplicated pages will not cause errors.
Now, You can add the page to another route group, which isn't a descendant of docs layout.
For example, to replace `/docs/test`:
For `/docs`, you need to change the catch-all route to be non-optional:
## Learn More [#learn-more]
New to here? Don't worry, we are welcome for your questions.
If you find anything confusing, please give your feedback on [Github Discussion](https://github.com/fuma-nama/fumadocs/discussions)!
### Writing Content [#writing-content]
For authoring docs, make sure to read:
Fumadocs has some additional features for authoring content.
Learn how to customize navigation structure.
Learn how to organise content.
### Special Needs [#special-needs]
# Fumadocs (Framework Mode): createAsyncAPIPage()
URL: /docs/integrations/asyncapi/api-page
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/asyncapi/api-page.mdx
The component for rendering AsyncAPI docs content
## Overview [#overview]
Fumadocs AsyncAPI uses a `` component to render page contents, it should be a client component.
```tsx title="components/api-page.tsx"
'use client';
import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui';
export const AsyncAPIPage = createAsyncAPIPage({
// config
});
```
### Custom Layout [#custom-layout]
You can customize how operations and message examples are laid out.
```tsx title="components/api-page.tsx"
'use client';
import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui';
export const AsyncAPIPage = createAsyncAPIPage({
content: {
renderOperationLayout(slots) {
return (
);
},
},
});
```
### Schema UI [#schema-ui]
Customize how JSON schemas are rendered, or disable schema examples.
```tsx title="components/api-page.tsx"
'use client';
import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui';
export const AsyncAPIPage = createAsyncAPIPage({
schemaUI: {
showExample: true,
render(options, ctx) {
// fully custom schema renderer
return ctx.SchemaUI(options);
},
},
});
```
### Internationalization [#internationalization]
Assuming you have configured [Internationalization](/docs/internationalization) at UI level:
```tsx title="layout.shared.tsx"
import { defineI18n } from 'fumadocs-core/i18n';
import { uiTranslations } from 'fumadocs-ui/i18n';
import { asyncapiTranslations } from '@fumadocs/asyncapi/i18n';
const i18n = defineI18n({
languages: ['en', 'cn'],
defaultLanguage: 'en',
});
export const translations = i18n
.translations()
.extend(uiTranslations())
// [!code ++]
.extend(asyncapiTranslations())
.add({
cn: {
displayName: 'Chinese'
// [!code ++]
'Messages(operation page)': '消息',
},
})
```
See [Translations](/docs/ui/translations) for more details.
### Syntax Highlighting [#syntax-highlighting]
AsyncAPI pages use Shiki for code blocks. You can customize the highlighter and themes.
```tsx title="components/api-page.tsx"
'use client';
import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui';
import { defaultShikiFactory } from 'fumadocs-core/highlight/shiki/full';
export const AsyncAPIPage = createAsyncAPIPage({
shiki: defaultShikiFactory,
shikiOptions: {
themes: {
light: 'github-light',
dark: 'github-dark',
},
},
});
```
### Custom Components [#custom-components]
Override the default heading and code block components.
```tsx title="components/api-page.tsx"
'use client';
import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui';
export const AsyncAPIPage = createAsyncAPIPage({
components: {
Heading({ id, depth, ...props }) {
const Tag = `h${depth}` as const;
return ;
},
CodeBlock({ lang, code }) {
return
{code}
;
},
},
});
```
## Customise UI [#customise-ui]
For customisations beyond the available options, you can install the UI into your codebase with [Fumadocs CLI](/docs/cli).
### Full UI [#full-ui]
The entire UI of API pages, built on [the headless layer](/docs/integrations/asyncapi/headless).
```npm
npx @fumadocs/cli add asyncapi/page
```
It installs `` itself, your `components/api-page.tsx` is no longer needed.
```tsx title="mdx-components.tsx"
import { AsyncAPIPage } from '@/components/api-page'; // [!code --]
import { AsyncAPIPage } from '@/components/asyncapi/page'; // [!code ++]
```
### Operation [#operation]
The UI of a single operation, including its channel, messages and bindings.
```npm
npx @fumadocs/cli add asyncapi/operation
```
```tsx title="components/api-page.tsx"
'use client';
import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui';
import { Operation } from '@/components/asyncapi/operation';
export const AsyncAPIPage = createAsyncAPIPage({
components: {
Operation,
},
});
```
### Schema UI [#schema-ui-1]
The UI of JSON schemas, shared with the other API integrations. Its state comes from [``](/docs/integrations/openapi/headless#schema-ui).
```npm
npx @fumadocs/cli add api-docs/schema
```
```tsx title="components/api-page.tsx"
'use client';
import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui';
import { Schema } from '@/components/api/schema';
export const AsyncAPIPage = createAsyncAPIPage({
components: {
SchemaUI: Schema,
},
});
```
# Fumadocs (Framework Mode): generateFiles()
URL: /docs/integrations/asyncapi/generate-files
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/asyncapi/generate-files.mdx
Generate pages from AsyncAPI schema.
The `generateFiles()` function generates MDX files for API documentation.
Note that the page content is still rendered by the `` component, the docs generator only arranges the operations to render for each page.
## Usage [#usage]
It takes an AsyncAPI instance.
```ts
import { generateFiles } from '@fumadocs/asyncapi';
import { asyncapi } from '@/lib/asyncapi';
void generateFiles({
input: asyncapi,
// The output directory.
output: '/content/docs',
});
```
### `per` [#per]
Customize the content of pages, default to `operation`.
Operation refers to an AsyncAPI operation with a specific action like
`receiveLightMeasurement:receive`.
| mode | content | file path |
| --------- | ------------------------------ | ------------------- |
| tag | operations with same tag | `{tag_name}.mdx` |
| file | operations in same schema file | `{file_name}.mdx` |
| operation | each operation | `{operationId}.mdx` |
| custom | see below | N/A |
```ts
import { generateFiles } from '@fumadocs/asyncapi';
void generateFiles({
per: 'tag',
});
```
When set to `custom`, you can pass a function to fully customize the generation process:
```ts title="Example"
import { generateFiles, OperationOutput } from '@fumadocs/asyncapi';
void generateFiles({
per: 'custom',
toPages(builder) {
const items = builder.extract();
for (const op of items.operations) {
const { operation, channel, displayName } = builder.fromExtractedOperation(op)!;
const entry: OperationOutput = {
type: 'operation',
schemaId: builder.id,
item: op,
path: '...',
info: {
title: displayName,
description: operation.description ?? channel.description,
},
};
builder.create(entry);
}
},
});
```
When set to `tag`, adding `x-displayName` to the tag definition can control the title of each page.
```yaml title="asyncapi.yaml"
tags:
- name: lighting
description: Operations related to streetlights.
x-displayName: Lighting
```
### `groupBy` [#groupby]
In `operation` mode, you can group output files with folders.
| value | output |
| -------------- | ------------------------------------- |
| tag | `{tag}/{operationId}.mdx` |
| channel | `{channel_address}/{operationId}.mdx` |
| none (default) | `{operationId}.mdx` |
```ts
import { generateFiles } from '@fumadocs/asyncapi';
void generateFiles({
per: 'operation',
groupBy: 'tag',
});
```
### `index` [#index]
Generate index files with cards linking to generated pages.
```ts
import { generateFiles } from '@fumadocs/asyncapi';
void generateFiles({
index: {
// for generating `href`
url: {
baseUrl: '/docs',
contentDir: './content/docs',
},
items: [
{
// output path
path: 'index.mdx',
// optional: restrict the input files (identical to the `input` field in server)
only: ['streetlights.yaml'],
// optional: set frontmatter
description: 'All available pages',
},
],
},
});
```
### `meta` [#meta]
Generate [`meta.json`](/docs/page-conventions#meta) files automatically.
You can always define the `meta.json` files manually for maximal flexibility.
```ts
import { generateFiles } from '@fumadocs/asyncapi';
void generateFiles({
meta: true,
// or customize how folders are represented:
meta: {
folderStyle: 'separator',
},
});
```
### `imports` [#imports]
Add custom imports to generated MDX files. This is useful for including components, utilities, or other dependencies in your generated documentation.
```ts
import { generateFiles } from '@fumadocs/asyncapi';
void generateFiles({
imports: [
{
names: ['API_BASE_URL'],
from: '@/constants',
},
],
});
```
This will add the following imports to all generated MDX files:
```ts
import { API_BASE_URL } from '@/constants';
```
### `name` [#name]
A function that controls the output file name of MDX pages.
```ts
import { generateFiles } from '@fumadocs/asyncapi';
void generateFiles({
name(output) {
if (output.type === 'operation') {
const { operation } = this.fromExtractedOperation(output.item)!;
console.log(operation);
return `my-dir/${output.item.id}`;
}
return 'my-dir/filename';
},
});
```
You can also use the `algorithm` option:
```ts
import { generateFiles } from '@fumadocs/asyncapi';
void generateFiles({
// v2 (default): use operation id
// v1: use `{operationId}/{action}`
name: { algorithm: 'v1' },
});
```
### `frontmatter` [#frontmatter]
Customize the frontmatter of MDX files.
By default, it includes:
| property | description |
| ------------- | ---------------------------------- |
| `title` | Page title |
| `description` | Page description |
| `full` | Always true, added for Fumadocs UI |
| `_asyncapi` | Internal Properties |
```ts
import { generateFiles } from '@fumadocs/asyncapi';
void generateFiles({
frontmatter: (title, description) => ({
myProperty: 'hello',
}),
});
```
### `addGeneratedComment` [#addgeneratedcomment]
Add a comment to the top of generated files indicating they are auto-generated.
```ts
import { generateFiles } from '@fumadocs/asyncapi';
void generateFiles({
// Add default comment
addGeneratedComment: true,
// Or provide a custom comment
addGeneratedComment: 'Custom auto-generated comment',
// Or disable comments
addGeneratedComment: false,
});
```
# Fumadocs (Framework Mode): Headless
URL: /docs/integrations/asyncapi/headless
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/asyncapi/headless.mdx
The state and logic of AsyncAPI pages, without UI.
## Overview [#overview]
Everything outside `@fumadocs/asyncapi/ui` is headless.
| Path | Module |
| ------------------------------ | --------------------------------------------------------- |
| `@fumadocs/asyncapi` | the page: the document, its servers and your components |
| `@fumadocs/asyncapi/operation` | an operation: its channel, parameters, messages and reply |
## Page [#page]
`createAsyncAPIRenderer()` takes your components and returns ``, which accepts the props of generated pages.
```tsx title="components/api-page.tsx"
'use client';
import { type CodeBlockProps, createAsyncAPIRenderer } from '@fumadocs/asyncapi';
import { Operation } from '@/components/my-operation';
import { SchemaUI } from '@/components/my-schema';
export const AsyncAPIPage = createAsyncAPIRenderer({
components: {
Operation,
SchemaUI,
CodeBlock({ lang, code }: CodeBlockProps) {
return (
{code}
);
},
},
});
```
| Component | Renders |
| ----------- | ------------------------------------------------------------ |
| `Operation` | each operation of the page, see [Operation](#operation) |
| `SchemaUI` | the JSON schemas of parameters, headers and message payloads |
| `Layout` | optional, wraps the rendered operations |
### Hooks [#hooks]
Components under the page read its state.
| Hook | Returns |
| -------------------- | -------------------------------------------------- |
| `useAsyncAPI()` | the dereferenced document (`doc`) and page options |
| `useComponents()` | the components passed to the page |
| `useServer()` | the servers, the selected one and its variables |
| `useRenderContext()` | the render options of the page, like `content` |
To render operations yourself, mount `` in place of `createAsyncAPIRenderer()`.
## Operation [#operation]
`` derives the details of an operation once: traits are applied, and its channel, parameters, messages and reply are resolved.
```tsx title="components/my-operation.tsx"
'use client';
import {
OperationProvider,
type PageOperationProps,
useOperation,
useOperationSecurity,
} from '@fumadocs/asyncapi/operation';
export function Operation(props: PageOperationProps) {
return (
);
}
function Content() {
const { title, channel, parameters, messages } = useOperation();
const schemes = useOperationSecurity();
return (
<>
{title}
{channel.address}
{parameters.map((param) => (
{param.name}
))}
{messages.map((message) => (
{message.name}
{message.examples.map((example) => (
{JSON.stringify(example.payload, null, 2)}
))}
))}
{schemes.map((scheme) => scheme.type).join(', ')}
>
);
}
```
| Hook | Returns |
| ------------------------ | ------------------------------------------------------------------------ |
| `useOperation()` | the operation with its details resolved |
| `useOperationSecurity()` | security schemes of the operation, falling back to the selected server's |
### OperationInfo
| Prop | Type | Description |
| -------------- | -------- | ----------------------------- |
| `id` | `string` | |
| `action` | `union` | |
| `operation` | `object` | resolved, with traits applied |
| `channel` | `object` | |
| `title` | `string` | |
| `description?` | `string` | |
| `parameters` | `array` | |
| `messages` | `array` | |
| `reply?` | `object` | |
Examples of a message come from the document, or are generated from its schemas.
### OperationMessage
| Prop | Type | Description |
| ---------- | -------- | ----------------------------------- |
| `id` | `string` | id for anchors and accordion values |
| `name` | `string` | display name |
| `message` | `object` | resolved, with traits applied |
| `headers?` | `union` | |
| `payload?` | `union` | |
| `examples` | `array` | example messages |
# Fumadocs (Framework Mode): AsyncAPI
URL: /docs/integrations/asyncapi
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/asyncapi/index.mdx
Generating docs for AsyncAPI schema.
## Setup [#setup]
Install the required packages.
```npm
npm i @fumadocs/asyncapi shiki
```
### Generate Styles [#generate-styles]
Add the following line:
```css title="Tailwind CSS"
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
/* [!code ++] */
@import '@fumadocs/asyncapi/css/preset.css';
```
### Configure Plugin [#configure-plugin]
Create the AsyncAPI server instance & `` component.
```ts tab="lib/asyncapi.ts"
import { createAsyncAPI } from '@fumadocs/asyncapi/server';
// note: this is a server-side API
export const asyncapi = createAsyncAPI({
// the AsyncAPI schema, you can also give it an external URL.
input: ['./asyncapi.yaml'],
});
```
```tsx tab="components/api-page.tsx"
'use client';
import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui';
export const AsyncAPIPage = createAsyncAPIPage();
```
```ts tab="lib/source.ts"
import { asyncapiPlugin } from '@fumadocs/asyncapi/server';
import { loader } from 'fumadocs-core/source';
export const source = loader({
// [!code ++] optional: adds a send/receive badge to each page item in page tree
plugins: [asyncapiPlugin()],
});
```
See [`createAsyncAPI()`](/docs/integrations/asyncapi/server) & [`createAsyncAPIPage()`](/docs/integrations/asyncapi/api-page) for available options.
### Generate Pages [#generate-pages]
You can generate MDX files directly from your AsyncAPI schema.
Create a script:
```js title="scripts/generate-docs.ts"
import { generateFiles } from '@fumadocs/asyncapi';
import { asyncapi } from '@/lib/asyncapi';
void generateFiles({
input: asyncapi,
output: './content/docs',
// we recommend to enable it
// make sure your operation description doesn't break MDX syntax.
includeDescription: true,
});
```
Generate docs with the script:
```bash
bun ./scripts/generate-docs.ts
```
Add the `AsyncAPIPage` component to your MDX components. Generated files use the `` tag.
```tsx tab="Server Components" title="app/docs/[[...slug]]/page.tsx"
import { source } from '@/lib/source';
import { asyncapi } from '@/lib/asyncapi';
import { AsyncAPIPage } from '@/components/api-page';
import { getMDXComponents } from '@/components/mdx';
// e.g. in your page renderer
export default function Page({ slug }) {
const page = source.getPage(slug);
const MdxContent = page.data.body;
return (
(
),
})}
/>
);
}
```
```tsx tab="Without Server Components" title="docs/[...slug]"
import { source } from '@/lib/source';
import { asyncapi } from '@/lib/asyncapi';
import { AsyncAPIPage } from '@/components/api-page';
import { getMDXComponents } from '@/components/mdx';
// e.g. where you return server payload to client
export function loader({ slug }) {
const page = source.getPage(slug);
return {
// [!code ++]
asyncapiData: await asyncapi.preloadAsyncAPIPage(page),
// ...
};
}
// e.g. in your page renderer
export default function Page() {
// consume loader data
const { asyncapiData } = useLoaderData();
return (
,
})}
/>
);
}
```
You can also use it without generating real files by integrating into [Loader API](/docs/headless/source-api/source).
```ts title="lib/source.ts"
import { loader } from 'fumadocs-core/source';
import { docs } from 'collections/server';
import { asyncapi } from '@/lib/asyncapi';
export const source = loader(
// [!code ++:6]
{
docs: docs.toFumadocsSource(),
asyncapi: await asyncapi.staticSource({
baseDir: 'asyncapi',
}),
},
{
baseUrl: '/docs',
plugins: [asyncapi.loaderPlugin()],
// ...
},
);
```
`staticSource()` is a server-side API that generates pages directly to your `loader()`, hence it allows dynamic content generation, such as re-generating page tree as schema changes.
**It will change the type of your pages**, make sure to update all references to your `source`.
For example, where you return text for LLM:
```ts
import { llms } from 'fumadocs-core/source';
import { source } from '@/lib/source';
export const docsLlms = llms(source, {
renderPage: (page) => {
// [!code ++:4]
if (page.type === 'asyncapi') {
// e.g. return the stringified AsyncAPI schema
return JSON.stringify(page.data.getSchema().bundled, null, 2);
}
// your original flow below...
},
});
```
And update your page renderer:
```tsx title="docs/[[...slug]]/page.tsx"
import { AsyncAPIPage } from '@/components/api-page';
export default function Page({ slug }) {
const page = source.getPage(slug);
// for AsyncAPI pages
if (page.type === 'asyncapi') {
return (
{page.data.title}
);
}
// your original flow below...
}
```
Pass a client payload from server, then render the page using the `` component you created above.
For example, in Tanstack Start:
```tsx title="routes/docs/$.tsx"
import { createFileRoute, notFound } from '@tanstack/react-router';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { createServerFn } from '@tanstack/react-start';
import { docs, source } from '@/lib/source';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { useFumadocsLoader } from 'fumadocs-core/source/client';
import { type ReactNode, Suspense } from 'react';
import { AsyncAPIPage } from '@/components/api-page';
export const Route = createFileRoute('/docs/$')({
component: Page,
loader: async ({ params }) => {
const slugs = params._splat?.split('/') ?? [];
const data = await serverLoader({ data: slugs });
// Fumadocs MDX: only preload content for normal pages [!code highlight:3]
if (data.type === 'docs') {
await docs.getPage(data.path)?.preload();
}
return data;
},
});
const serverLoader = createServerFn({
method: 'GET',
})
.validator((slugs: string[]) => slugs)
.handler(async ({ data: slugs }) => {
const page = source.getPage(slugs);
if (!page) throw notFound();
const pageTree = await source.serializePageTree(source.getPageTree());
// different result for AsyncAPI pages [!code ++:9]
if (page.type === 'asyncapi') {
return {
type: 'asyncapi',
title: page.data.title,
description: page.data.description,
pageTree,
props: page.data.getAsyncAPIPageProps(),
};
}
return {
type: 'docs',
path: page.path,
pageTree,
// ...
};
});
// renders a page of your `docs` collection
function Content({ path }: { path: string }) {
// ...
}
function Page() {
const page = useFumadocsLoader(Route.useLoaderData());
let content: ReactNode;
// render AsyncAPI page content [!code ++:11]
if (page.type === 'asyncapi') {
content = (
{page.title}{page.description}
{/* pass the payload data */}
);
} else {
content = ;
}
return (
{content}
);
}
```
You can see the full [AsyncAPI example](https://github.com/fuma-nama/fumadocs/blob/dev/examples/asyncapi).
Run a type check to verify before continuing, e.g.
```npm
npm run types:check
```
## Features [#features]
The official AsyncAPI integration supports:
- Basic operation information (send/receive)
- Channel, message, and reply documentation
- Message payload & header schemas
- Security schemes
- Message examples (from schema examples or generated samples)
- Bindings and traits
### Demo [#demo]
See the [AsyncAPI example](/docs/asyncapi).
# Fumadocs (Framework Mode): createAsyncAPI()
URL: /docs/integrations/asyncapi/server
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/asyncapi/server.mdx
The AsyncAPI server instance.
## AsyncAPI Server [#asyncapi-server]
The main config for Fumadocs AsyncAPI.
> It should not be referenced in browser environments.
### `input` [#input]
The AsyncAPI schemas to read from.
- File Paths
- External URLs
- Functions (see below)
```ts tab="Basic"
import { createAsyncAPI } from '@fumadocs/asyncapi/server';
export const asyncapi = createAsyncAPI({
input: ['./streetlights.yaml'],
});
```
```ts tab="Functions"
import { createAsyncAPI } from '@fumadocs/asyncapi/server';
export const asyncapi = createAsyncAPI({
input: {
// [id]: downloaded AsyncAPI Schema
galaxy: async () => {
const res = await fetch('https://example.com/asyncapi.yaml');
return res.text();
},
},
});
```
### `disableCache` [#disablecache]
Disable caching of loaded schemas. Useful during development when schemas change frequently.
```ts
import { createAsyncAPI } from '@fumadocs/asyncapi/server';
export const asyncapi = createAsyncAPI({
input: ['./asyncapi.yaml'],
disableCache: true,
});
```
# Fumadocs (Framework Mode): Build Your Own
URL: /docs/integrations/content/custom
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/custom.mdx
Build your own content source for Fumadocs.
## Introduction [#introduction]
For a custom content source implementation, there's two ways:
### Using Loader API [#using-loader-api]
Loader API has a file-system-like interface for integrating different content sources.
See [Loader API `source`](/docs/headless/source-api/source) for details.
### Using Low-Level API [#using-low-level-api]
For more robust control, you can use lower level APIs directly.
#### Page Tree [step] [#page-tree]
You can either hardcode the page tree, or write some code to generate one.
See [Definitions of Page Tree](/docs/headless/page-tree).
Pass your page tree to `DocsLayout` (usually in a `layout.tsx`):
```tsx title="layout.tsx"
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import type { ReactNode } from 'react';
export default function Layout({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
The page tree is like a smarter "sidebar items", they will be referenced everywhere in the UI for navigation elements, such as the page footer.
#### Page Content [step] [#page-content]
For docs page, it's the same logic:
- Define path params (`slugs`).
- Fetch page content from path params.
- Render the content (inside the `DocsPage` component).
You may need table of contents, which can be generated on your own, or using the [`getTableOfContents`](/docs/headless/utils/get-toc) utility (Markdown/MDX only).
```tsx
import { DocsPage, DocsBody } from 'fumadocs-ui/layouts/docs/page';
import { getPage } from './my-content-source';
import { notFound } from 'next/navigation';
export default function Page({ params }: { params: { slug?: string[] } }) {
const page = getPage(params.slug);
if (!page) notFound();
return (
{page.render()}
);
}
```
#### Pre-rendering [step] [#pre-rendering]
The mechanism for pre-rendering differs depending on your React.js framework.
It's important to pre-render pages (especially the frequently visited ones) to improve initial load time.
Define the [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) function to populate dynamic & catch-all routes.
When pre-rendering pages from a remote source, make sure to optimize repeated reads during the build phase.
For example, if you're fetching content via GitHub API, it is better to clone the repository content locally for pre-rendering.
```text tab="Before"
/docs/* -> GET github.com
/docs/introduction -> GET github.com
/docs/my-page -> GET github.com
Error: API Ratelimit
```
```text tab="After"
start -> git clone
/docs/* -> ls
/docs/introduction -> read file
/docs/my-page -> read file
```
#### Document Search [step] [#document-search]
This can be difficult considering your content may not be necessarily Markdown/MDX.
For Markdown and MDX, the built-in [Search API](/docs/headless/search/orama) + search index usage is adequate for most use cases.
Otherwise, you will have to bring your own implementation.
We recommend 3rd party solutions like Orama or Algolia Search. They are more flexible than the built-in Search API, and is easier to integrate with remote sources.
Fumadocs offers a variant of [Search Integrations](/docs/headless/search).
# Fumadocs (Framework Mode): Content Source
URL: /docs/integrations/content
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/index.mdx
Integrate different content sources with Fumadocs.
## Introduction [#introduction]
**Fumadocs is very flexible.** You can integrate with any content source, even without an official adapter.
### Examples [#examples]
You can see examples to use Fumadocs with a CMS, which allows a nice experience on publishing content, and real-time update without re-building the app.
- [BaseHub](https://github.com/fuma-nama/fumadocs-basehub)
- [Sanity](https://github.com/fuma-nama/fumadocs-sanity)
- [Community: Payload CMS by `MFarabi619`](https://github.com/MFarabi619/fumadocs-payloadcms)
- [Community: Payload CMS by `bapspatil`](https://github.com/bapspatil/fumadocs-payload-template)
Not enough? [Build your own content source](/docs/integrations/content/custom).
# Fumadocs (Framework Mode): Local HTML
URL: /docs/integrations/content/local-html
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/local-html.mdx
Content source for local HTML files.
## Introduction [#introduction]
`@fumadocs/local-html` is a content source for local HTML files, it is bundleless (works fully at runtime) by design.
It is made for pages you receive as HTML instead of author yourself, like exported decks, reports, and the output of other tools. Each file is adapted to your docs theme rather than rendered as it came.
As compared to [Local Markdown](/docs/integrations/content/local-md), HTML is treated as data to adapt, not as a document format to write in.
### Limitations [#limitations]
- Scripts are stripped, the interactive behaviour of the original page is not preserved.
- No MDX components in content, but you can map HTML tags to components at render phase.
- Processing is a transform, not sanitization. Nothing that executes survives it, but the remaining markup is rendered as-is, and the resources it references (images above all) are fetched by everyone who opens the page. Keep the content directory to files you trust, like your Markdown content.
## Setup [#setup]
Install the package:
```package-install
@fumadocs/local-html
```
Create a `localHtml` instance and connect it to Fumadocs:
```ts title="lib/source.ts"
import { dynamicLoader } from 'fumadocs-core/source';
import { localHtml } from '@fumadocs/local-html';
const pages = localHtml({
dir: 'content/pages',
// options
});
const pagesLoader = dynamicLoader(pages.dynamicSource(), {
baseUrl: '/docs',
});
export async function getSource() {
return pagesLoader.get();
}
```
The returned source from `pagesLoader.get()` is a normal [content loader](/docs/headless/source-api) instance.
## Usage [#usage]
`page.data.load()` returns a renderer for the processed content:
```tsx title="app/docs/[[...slug]]/page.tsx"
import { getSource } from '@/lib/source';
import { DocsPage, DocsBody, DocsTitle, DocsDescription } from 'fumadocs-ui/page';
import { notFound } from 'next/navigation';
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
const params = await props.params;
const source = await getSource();
const page = source.getPage(params.slug);
if (!page) notFound();
const renderer = await page.data.load();
const { body, toc } = await renderer.render();
return (
{page.data.title}{page.data.description}{body}
);
}
```
Pass components to `render()` to map HTML tags onto your own, like headings with anchors:
```tsx
import { getMDXComponents } from '@/mdx-components';
const { body, toc } = await renderer.render(getMDXComponents());
```
### Page Metadata [#page-metadata]
The title and description of each page are read from its ``:
- `` becomes the page title.
- `` becomes the description.
You can override them (and set an icon) without touching the visible content, using `fumadocs:` metas:
```html
```
Every ``/`content` pair of the document is also available on `page.data.metadata`.
Like other local sources, `meta.json` files customize the page tree, validated with `metaSchema`.
### Content Options [#content-options]
A page keeps only its content element: the only ``, then the only ``, falling back to `` with page chrome like `` and `