fumadocs Documentation

The beautiful & flexible React.js docs framework.

13,447 stars TypeScript TypeScript 1 file · ~60,284 tokens #app-router#content-collections#contentlayer#docs#fumadocs#mdx#next#nextjs
RAW Doc

Technical Documentation: fuma-nama/fumadocs

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (README.md)

The framework for building documentation websites in React.js frameworks and Astro.

Officially Supported:

  • Next.js
  • Astro (with React)
  • Vite: Tanstack Start, Waku, React Router

📘 Learn More: Documentation.

Compatibility

All packages are ESM only.

Sticker

Welcome to print it out :D

Contributions

Make sure to read the Contributing Guide before submitting a pull request.


Examples Astro Content Docs Index (`examples/astro/content/docs/index.mdx`)


title: Hello World

Welcome to the docs! You can start writing documents in /content/docs.

js
console.log('Hello World');

What's next?

The web framework for content-driven websites The docs framework by Fuma

Examples Asyncapi Content Docs Index (`examples/asyncapi/content/docs/index.mdx`)


title: Overview

Scalar Galaxy Events is an AsyncAPI 3.0 specification that documents real-time notifications and event-driven capabilities for the Scalar Galaxy API.

Event-Driven Features

  • Planet Updates: Get notified when planets are created, updated, or experience cosmic events
  • User Activity Streams: Track user signups, authentication events, and profile changes
  • Celestial Body Monitoring: Monitor changes to satellites, asteroids, and other celestial objects
  • System Notifications: Receive alerts about system maintenance, rate limits, and security events

Operations

AsyncAPI operations use send and receive actions instead of HTTP methods. Each operation references a channel and the messages it can process.

Browse the generated reference pages under References to explore channels, messages, bindings, and security schemes from the spec.


Examples Base Ui Content Docs Index (`examples/base-ui/content/docs/index.mdx`)


title: Hello World

Welcome to the docs! You can start writing documents in /content/docs.

What is Next?


Examples Content Collections Content Docs Index (`examples/content-collections/content/docs/index.mdx`)


title: Hello World

Hey there!

Hello World

Heading

Heading

Heading

name description
Hello World Goodbye

Examples Graphql Content Docs Index (`examples/graphql/content/docs/index.mdx`)


title: Overview

This example documents a GraphQL schema with Fumadocs GraphQL, generated from a single SDL file (schema.graphql) and example operations (examples/store.graphql).

API Reference

Browse the generated reference pages under API Reference:

  • Queries, Mutations, Subscriptions: a page per operation with its arguments, return type, code samples and examples.
  • Objects, Interfaces, Unions, Enums, Input Objects, Scalars: a page per named type, enabled with includeTypes: true in lib/source.ts.

Playground

Open Try in Playground on an operation page, then press Send to send the operation to the endpoint.

This example ships a mock endpoint at /api/graphql that responds with sample values, replace it with your own GraphQL server.


Examples I18n Content Docs Index (`examples/i18n/content/docs/index.mdx`)


title: Hello World

Hey there!

Heading

Heading

Heading


Examples Next Local Md Content Docs Index (`examples/next-local-md/content/docs/index.mdx`)


title: Hello World

Welcome to the docs! You can start writing documents in /content/docs.

What is Next?


Examples Next Min Content Docs Index (`examples/next-min/content/docs/index.mdx`)


title: Hello World
Your first document

ts
console.log("Hello World")

Hey there! fsd asfd sdfsfda
sfd

fdsa ff dsaf sdf sda fasd fsd

Heading

Hello World dsasfdafsd

Heading

Heading

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

./test.mdx


Examples Next Shadcn Content Docs Index (`examples/next-shadcn/content/docs/index.mdx`)


title: Hello World
Your first document

ts
console.log("Hello World")

Hey there! fsd asfd sdfsfda
sfd

fdsa ff dsaf sdf sda fasd fsd

Heading

Hello World dsasfdafsd

Heading

Heading

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples Next Static Content Docs Index (`examples/next-static/content/docs/index.mdx`)


title: Hello World

Welcome to the docs! You can start writing documents in /content/docs.

What is Next?


Examples Next Content Docs Index (`examples/next/content/docs/index.mdx`)


title: Hello World

Welcome to the docs! You can start writing documents in /content/docs.

What is Next?


Examples Openapi Content Docs Index (`examples/openapi/content/docs/index.mdx`)


title: Overview

The Unkey API uses HTTP RPC-style methods and generally follow the schema:

text
https://api.unkey.dev/{version}/{service}.{method}

For example GET https://api.unkey.dev/v1/apis.listKeys to list all keys for an API.

HTTP Methods

We strictly use only GET and POST methods. PUT and DELETE are not used.

`GET`

GET methods are used for reading data. Filtering, sorting, or pagination is done via query parameters.

http
curl "https://api.unkey.dev/v1/keys.getKey?keyId=key_123" \
  -H "Authorization: Bearer <ROOT_KEY>"

`POST`

POST methods are used for creating, updating, and deleting data. Data is passed as application/json in the request body.

http
curl -XPOST "https://api.unkey.dev/v1/keys.createKey" \
  -H "Authorization: Bearer <ROOT_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"apiId": "api_123", "name": "My Key"}'

Examples React Router I18n Content Docs Index (`examples/react-router-i18n/content/docs/index.mdx`)


title: Hello World Your first `document` You'll love it!

Hey there! Fumadocs is the docs framework that also works on React Router!

Heading

Hello World

ts
console.log('I love React!');

Heading

Heading

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples React Router Min Content Docs Index (`examples/react-router-min/content/docs/index.mdx`)


title: Hello World Your first `document` You'll love it!

Hey there! Fumadocs is the docs framework that also works on React Router!

Heading

Hello World

ts
console.log('I love React!');

Heading

Heading

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples React Router Rsc Content Docs Index (`examples/react-router-rsc/content/docs/index.mdx`)


title: Hello World Your first `document` You'll love it!

Hey there! Fumadocs is the docs framework that also works on React Router!

Heading

Hello World

ts
console.log('I love React!');

Heading

Heading

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples React Router Spa Content Docs Index (`examples/react-router-spa/content/docs/index.mdx`)


title: Hello World Your first `document` You'll love it!

Hey there! Fumadocs is the docs framework that also works on React Router!

Heading

Hello World

ts
console.log('I love React!');

Heading

Heading

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples React Router Content Docs Index (`examples/react-router/content/docs/index.mdx`)


title: Hello World Your first `document` You'll love it!

Hey there! Fumadocs is the docs framework that also works on React Router!

Heading

Hello World

ts
console.log('I love React!');

Heading

Heading

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples Stackblitz Content Docs Index (`examples/stackblitz/content/docs/index.mdx`)


title: Hello World icon: Rocket

Hey there! Fumadocs is a docs framework that works on Waku!

Heading

Hello World!

CodeBlock

ts
console.log('Hello World');

Table

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples Tanstack Start I18n Content Docs Index (`examples/tanstack-start-i18n/content/docs/index.mdx`)


title: Hello World icon: Rocket

Hey there! Fumadocs is a docs framework built for Next.js, but do you know it also works on Tanstack Start?

Heading

Hello World!

CodeBlock

ts
console.log('Hello World');

Table

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples Tanstack Start Local Md Content Docs Index (`examples/tanstack-start-local-md/content/docs/index.mdx`)


title: Hello World icon: Rocket

Hey there! Fumadocs is the docs framework that also works on Tanstack Start!

Heading

Hello World!

CodeBlock

ts
console.log('Hello World');

Table

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples Tanstack Start Min Content Docs Index (`examples/tanstack-start-min/content/docs/index.mdx`)


title: Hello World icon: Rocket

Hey there! Fumadocs is the docs framework that also works on Tanstack Start!

Heading

Hello World!

CodeBlock

ts
console.log('Hello World');

Table

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples Tanstack Start Openapi Content Docs Index (`examples/tanstack-start-openapi/content/docs/index.mdx`)


title: Hello World icon: Rocket

Hey there! Fumadocs is the docs framework that also works on Tanstack Start!

Heading

Hello World!

CodeBlock

ts
console.log('Hello World');

Table

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples Tanstack Start Spa Content Docs Index (`examples/tanstack-start-spa/content/docs/index.mdx`)


title: Hello World icon: Rocket

Hey there! Fumadocs is the docs framework that also works on Tanstack Start!

Heading

Hello World!

CodeBlock

ts
console.log('Hello World');

Table

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples Tanstack Start Story Content Docs Index (`examples/tanstack-start-story/content/docs/index.mdx`)


title: Hello World icon: Rocket

import { story } from '@/components/callout.story';

Hey there! Fumadocs is the docs framework that also works on Tanstack Start!

Heading

<story.WithControl />

Hello World!

CodeBlock

ts
console.log('Hello World');

Table

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples Tanstack Start Content Docs Index (`examples/tanstack-start/content/docs/index.mdx`)


title: Hello World icon: Rocket

Hey there! Fumadocs is the docs framework that also works on Tanstack Start!

Heading

Hello World!

CodeBlock

ts
console.log('Hello World');

Table

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples Waku I18n Content Docs Index (`examples/waku-i18n/content/docs/index.mdx`)


title: Hello World icon: Rocket

Hey there! Fumadocs is a docs framework that works on Waku!

Heading

Hello World!

CodeBlock

ts
console.log('Hello World');

Table

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Examples Waku Content Docs Index (`examples/waku/content/docs/index.mdx`)


title: Hello World icon: Rocket

Hey there! Fumadocs is a docs framework that works on Waku!

Heading

Hello World!

CodeBlock

ts
console.log('Hello World');

Table

Head Description
hello Hello World
very important Hey
Surprisingly Fumadocs
very long text that looks weird hello world hello world hello world

Apps Docs Content Docs (Framework) Index (`apps/docs/content/docs/(framework)/index.mdx`)


title: Quick Start icon: Rocket

import { CpuIcon, PanelsTopLeftIcon, DatabaseIcon, TerminalIcon } from 'lucide-react';

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:

<Card icon={} title='Fumadocs Core' href='/docs/headless'>

Handles most of the logic, including document search, content source adapters, and Markdown extensions.

The default theme of Fumadocs offers a beautiful look for documentation sites and interactive components.

<Card icon={} title='Content Source'>

The source of your content, can be a CMS or local data layers like Fumadocs MDX (the official content source).

<Card icon={} 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

Markdown/MDX: Markdown is a markup language for creating formatted text. Fumadocs natively supports Markdown and MDX (superset of Markdown).

Bun: A JavaScript runtime, it will be used in our guides for running scripts. If you prefer Node.js, try unrun instead.

Some basic knowledge of React.js would be useful for further customizations.

Fumapress 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).

It enforces a React framework of its own, and offers a better DX in return.

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:

text
You can follow the [Manual Installation](/docs/manual-installation) guide to get started.

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

Some common questions you may encounter.

Make sure to upgrade Fumadocs when you've encountered any problems or trying out new features:
text
```bash title="pnpm"
pnpm update -i -r --latest
```
text
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/*`):

<Files>
  <Folder name="app/docs" defaultOpen className="opacity-50" disabled>
    <File name="layout.tsx" />
  </Folder>
  <Folder name="app/info" defaultOpen>
    <File name="layout.tsx" />
  </Folder>
</Files>

Or rename from `/docs/*` to `/*` using a route group:

<Files>
  <Folder name="app/(docs)" defaultOpen>
    <File name="layout.tsx" />
  </Folder>
</Files>

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]
});
text
<Accordion id='multi-docs' title="How to implement multi-docs?">
    We recommend using [Layout Tabs](/docs/ui/layouts/docs#layout-tabs).
</Accordion>

For Vite

There's some weird pre-bundling problems with Vite: #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

text
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+.
text
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.
text
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`:

<Files>
  <File name="(home)/docs/test/page.tsx" />
  <Folder name="docs" defaultOpen>
    <File name="layout.tsx" />
    <File name="[[...slug]]/page.tsx" />
  </Folder>
</Files>

For `/docs`, you need to change the catch-all route to be non-optional:

<Files>
  <File name="(home)/docs/page.tsx" />
  <Folder name="docs" defaultOpen>
    <File name="layout.tsx" />
    <File name="[...slug]/page.tsx" />
  </Folder>
</Files>

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!

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


Apps Docs Content Docs Asyncapi Index (`apps/docs/content/docs/asyncapi/index.mdx`)


title: Scalar Example index: true


Apps Docs Content Docs Cli Index (`apps/docs/content/docs/cli/index.mdx`)


title: User Guide

Installation

Initialize a config for CLI:

package-install
npx @fumadocs/cli

You can change the output paths of components in the config.

Init

Set up Fumadocs on an existing app, it supports Next.js, React Router, Tanstack Start and Waku.

package-install
npx @fumadocs/cli init

To avoid conflicts with your app, the docs pages are added in a dedicated route group (e.g. app/(docs) on Next.js) with their own provider and layout, route handlers like the search API follow the examples. Only a few edits are made to existing files:

  • Fumadocs MDX plugin in your framework config.
  • Fumadocs UI styles in your global CSS.
  • suppressHydrationWarning on the <html> element of your root layout, for the theme switch.
  • .source in .gitignore.

Pass --i18n to set up internationalization: the docs routes get a locale segment (optional for the default locale where the router supports it), a lib/i18n.ts config and, on Next.js, a proxy scoped to the docs routes. Other features follow the i18n config of your project.

It asks before overwriting a file, pass --yes to skip prompts and --no-install to only write dependencies to package.json.

Features

Configure a feature on your project, it installs the needed components & routes, and wires them into your app.

package-install
npx @fumadocs/cli feature ai --provider openrouter
Feature Description
docs Set up Fumadocs, same as init.
ai Ask AI dialog, --provider.
llms LLM routes: llms.txt, llms-full.txt and per-page Markdown.
mcp MCP server to search and read the docs.
webmcp WebMCP tools for AI agents in the browser, experimental.
og Open Graph image generation with Takumi or next/og (Next.js only), --engine.
search 3rd party search solution, --provider.
feedback Feedback component on docs pages.
epub Route to export EPUB.
lint Configure ESLint (Next.js only), Biome or Oxlint, --linter.

Features adapt to your setup:

  • Fumadocs MDX with the Macro API or a source.config.ts, other content sources can't use llms, mcp and epub.
  • The baseUrl of your docs, src/ directory, next.config.ts, i18n, static export and SPA modes.
  • React Router with file-based routes: the routes to register are printed instead.

The paths of routes are placed under the constants of lib/shared.ts.

See help for the available options:

package-install
npx @fumadocs/cli feature -h

Components

Select and install components.

package-install
npx @fumadocs/cli add

You can pass component names directly.

package-install
npx @fumadocs/cli add banner files

How the magic works?

The CLI fetches the latest version of component from the Fumadocs registry (https://fumadocs.dev/registry).
When you install a component, it is guaranteed to be up-to-date.

In addition, it also transforms import paths.
Make sure to use the latest version of CLI

This is highly inspired by Shadcn UI.

Customize

A simple way to customize Fumadocs layouts.

package-install
npx @fumadocs/cli customize

It installs the layout into layouts and points the imports of your route files at it.

Shadcn UI

On projects with a components.json, the CLI follows the aliases of Shadcn UI and:

  • installs the shadcn preset, so Fumadocs UI adopts your theme colors.
  • reuses the button, popover and collapsible in your ui directory, they share the API of Shadcn UI.
  • imports cn from your utils alias.

Fumadocs' copies are only installed when you don't have them yet.

Tree

Generate files tree for Fumadocs UI Files component, using the tree command from your terminal.

package-install
npx @fumadocs/cli tree ./my-dir ./output.tsx

You can output MDX files too:

package-install
npx @fumadocs/cli tree ./my-dir ./output.mdx

See help for further details:

package-install
npx @fumadocs/cli tree -h

Example Output

tsx title="output.tsx"
import { File, Folder, Files } from 'fumadocs-ui/components/files';

export default (
  <Files>
    <Folder name="app">
      <File name="layout.tsx" />
      <File name="page.tsx" />
      <File name="global.css" />
    </Folder>
    <Folder name="components">
      <File name="button.tsx" />
      <File name="tabs.tsx" />
      <File name="dialog.tsx" />
    </Folder>
    <File name="package.json" />
  </Files>
);

Apps Docs Content Docs Graphql Index (`apps/docs/content/docs/graphql/index.mdx`)


title: Store Example index: true

A store schema documented with Fumadocs GraphQL, its playground sends requests to a mock endpoint.


Apps Docs Content Docs Headless Index (`apps/docs/content/docs/headless/index.mdx`)


title: Introduction icon: SquareBookmark

What is this?

Fumadocs Core offers server-side functions and headless components to build docs on React.js frameworks like Next.js, Waku, and Astro with React islands.

text
It can be used without Fumadocs UI, in other words, it's headless.

For beginners and normal usages, use [Fumadocs UI](/docs/ui).

Installation

Install the core package. Individual integrations may require additional dependencies, listed in their guides.

package-install
fumadocs-core

For some components, a framework provider is needed:

tsx tab="next.js"
import type { ReactNode } from 'react';
import { NextProvider } from 'fumadocs-core/framework/next';

export function RootLayout({ children }: { children: ReactNode }) {
  // or if you're using Fumadocs UI, use `<RootProvider />`
  return <NextProvider>{children}</NextProvider>;
}
tsx tab="react router"
import type { ReactNode } from 'react';
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';

export function Root({ children }: { children: ReactNode }) {
  return <ReactRouterProvider>{children}</ReactRouterProvider>;
}
tsx tab="tanstack start/router"
import type { ReactNode } from 'react';
import { TanstackProvider } from 'fumadocs-core/framework/tanstack';

export function Root({ children }: { children: ReactNode }) {
  return <TanstackProvider>{children}</TanstackProvider>;
}
tsx tab="waku"
import type { ReactNode } from 'react';
import { WakuProvider } from 'fumadocs-core/framework/waku';

export function Root({ children }: { children: ReactNode }) {
  return <WakuProvider>{children}</WakuProvider>;
}
tsx tab="astro"
import type { ReactNode } from 'react';
import { AstroProvider } from 'fumadocs-core/framework/astro';

export function DocsIsland({
  children,
  pathname,
  params,
}: {
  children: ReactNode;
  pathname: string;
  params: Record<string, string | string[] | undefined>;
}) {
  return (
    <AstroProvider pathname={pathname} params={params}>
      {children}
    </AstroProvider>
  );
}

Explore the APIs and components:


Apps Docs Content Docs Mdx Index (`apps/docs/content/docs/mdx/index.mdx`)


title: Getting Started icon: SquareBookmark

Introduction

Fumadocs MDX is a tool to transform content into type-safe data, similar to Content Collections.

It is not a full CMS but rather a content processing layer for React frameworks, you can use it to handle blog posts and other contents.

What is a Collection?

Collection refers to a collection containing a certain type of files. You can define them with the Macro API in your app:

ts title="lib/source.ts"
import { defineDocs } from 'fumadocs-mdx/macro';
import { loader } from 'fumadocs-core/source';

const docs = defineDocs({
  dir: 'content/docs',
});

export const source = loader({
  baseUrl: '/docs',
  source: docs.toFumadocsSource(),
});

Available collections:

<Tabs items={["doc", "meta", 'docs']}>

text
<Tab value='doc'>

Compile Markdown & MDX files into a React component, with useful properties like Table of Contents.

ts
import { defineCollections } from 'fumadocs-mdx/macro';

export const test = defineCollections({
  type: 'doc',
  dir: 'content/docs',
});
text
</Tab>

<Tab value='meta'>

Transform YAML/JSON files into an array of data.

ts
import { defineCollections } from 'fumadocs-mdx/macro';

export const test = defineCollections({
  type: 'meta',
  dir: 'content/docs',
});
text
</Tab>

<Tab value='docs'>

Combination of meta and doc collections, which is needed for Fumadocs.

ts
import { defineDocs } from 'fumadocs-mdx/macro';

export const docs = defineDocs({
  dir: 'content/docs',
  docs: {
    // options for `doc` collection
  },
  meta: {
    // options for `meta` collection
  },
});
text
</Tab>

For example, a doc collection will transform the .md and .mdx files:

You can also define collections in source.config.ts with the Config API, which generates entry files under .source.

Installation

Configure Fumadocs MDX on:

Using Fumadocs MDX with Next.js. Using Fumadocs MDX with Vite. Using Fumadocs MDX with a JavaScript runtime.

FAQ

Built-in Properties

These properties are exported from MDX files by default.

Property Description
frontmatter Frontmatter
toc Table of Contents
structuredData Structured Data, useful for implementing search
extractedReferences For analyzing hrefs references

Customize Frontmatter

Use the schema option to pass a validation schema to validate frontmatter and define its output properties.

Customize MDX Compiler

Fumadocs MDX uses MDX Compiler to compile MDX files into JavaScript files.
The default preset includes a set of plugins and configurations out-of-the-box.

You can customize it on Global Config or Collection Config.


Apps Docs Content Docs Openapi Index (`apps/docs/content/docs/openapi/index.mdx`)


title: Scalar Example index: true


Apps Docs Content Docs Ui Index (`apps/docs/content/docs/ui/index.mdx`)


title: Overview icon: SquareBookmark

Fumadocs UI offers a beautifully designed theme for documentation sites.

It is bundled with many interactive components & layouts for docs sites, allowing low maintenance cost of code while receiving regular UI improvements.

You can also use Fumadocs CLI to install the components locally and have full control.

Learn More


Examples I18n Content Docs Git Index (`examples/i18n/content/docs/git/index.mdx`)


title: Hello World

Hey there!

Heading

Heading

Heading


Apps Docs Content Docs (Framework) Deploying Index (`apps/docs/content/docs/(framework)/deploying/index.mdx`)


title: Deploying

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.

If you are looking to host the app as a SPA app (fully static, hostable on CDN), see Static Build.

There's some extra notes on specific combinations:

Next.js + Cloudflare

Use https://opennext.js.org/cloudflare, Fumadocs doesn't work on Edge runtime.

Next.js + 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:

text
/* Detailed source-code truncated for AI context efficiency. */

This ensures Fumadocs MDX can access your configuration file during builds.


Apps Docs Content Docs (Framework) Guides Index (`apps/docs/content/docs/(framework)/guides/index.mdx`)


title: Guides icon: Book


Apps Docs Content Docs (Framework) Internationalization Index (`apps/docs/content/docs/(framework)/internationalization/index.mdx`)


title: Internationalization

npx @fumadocs/cli init --i18n sets up a new docs with i18n, or configure it manually below.

Overview

You'll have to configure i18n routing on your React framework and Fumadocs.

Fumadocs is not a full-powered i18n library, it's up to you when internationalizing the rest of your app.
You can also use other libraries with Fumadocs like next-intl on Next.js.


Apps Docs Content Docs (Framework) Manual Installation Index (`apps/docs/content/docs/(framework)/manual-installation/index.mdx`)


title: Manual Installation icon: Wrench

You can also let Fumadocs CLI set it up for you:

npm
npx @fumadocs/cli init

Apps Docs Content Docs (Framework) Markdown Index (`apps/docs/content/docs/(framework)/markdown/index.mdx`)


title: Markdown

import { Building, HomeIcon, Rocket } from 'lucide-react';

Introduction

Fumadocs provides many useful extensions to MDX, a markup language. Here is a brief introduction to the default MDX syntax of Fumadocs.

MDX is not the only supported format of Fumadocs. In fact, you can use any renderer such as headless CMS.

See Custom Content Source for details.

MDX

We recommend MDX, a superset of Markdown with JSX syntax.
It allows you to import components, and use them in the document, or even writing JavaScript.

See:

mdx nocopy
import { Component } from './component';

<Component name="Hello" />

## Heading

### Heading

#### Heading

Hello World, **Bold**, _Italic_, ~~Hidden~~

1. First
2. Second
3. Third

- Item 1
- Item 2

> Quote here



| Table | Description |
| ----- | ----------- |
| Hello | World       |

Frontmatter

Fumadocs support YAML-based frontmatter by default, title represents the page title (h1) in Fumadocs UI.

mdx
---
title: This is a document
---

...

For this reason, h1 title (# Heading) is usually unused when writing Markdown/MDX unless you have custom logic for page title rendering.

You can use the schema option to add frontmatter properties.

Internal links use the <Link /> component of your React framework (e.g. next/link) to allow prefetching and avoid hard-reload.

External links will get the default rel="noreferrer noopener" target="_blank" attributes for security.

mdx
[My Link](https://github.github.com/gfm)

This also works: https://github.github.com/gfm.

Cards

Useful for adding links.

mdx
import { HomeIcon } from 'lucide-react';

<Cards>
  <Card
    href="https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating"
    title="Fetching, Caching, and Revalidating"
  >
    Learn more about caching in Next.js
  </Card>
  <Card title="href is optional">Learn more about `fetch` in Next.js.</Card>
  <Card icon={<HomeIcon />} href="/" title="Home">
    You can include icons too.
  </Card>
</Cards>
Learn more about caching in Next.js Learn more about `fetch` in Next.js. } href="/" title="Home"> You can include icons too.

"Further Reading" Section

You can do something like:

tsx title="page.tsx"
import { getPageTreePeers } from 'fumadocs-core/page-tree';
import { source } from '@/lib/source';

<Cards>
  {getPageTreePeers(source.getPageTree(), '/docs/my-page').map((peer) => (
    <Card key={peer.url} title={peer.name} href={peer.url}>
      {peer.description}
    </Card>
  ))}
</Cards>;

This will show the other pages in the same folder as cards.

Callouts

Useful for adding tips/warnings, it is included by default. You can specify the type of callout:

  • info (default)
  • warn/warning
  • error
  • success
  • idea
mdx
<Callout>Hello World</Callout>

<Callout title="Title" type="warn">
  Hello World
</Callout>

<Callout title="Title" type="error">
  Hello World
</Callout>

<Callout title="Title" type="idea">
  Hello World
</Callout>

Hello World

Hello World Hello World Hello World

Headings

An anchor is automatically applied to each heading, it sanitizes invalid characters like spaces. (e.g. Hello World to hello-world)

md
# Hello `World`

TOC Settings

The table of contents (TOC) will be generated based on headings, you can also customize the effects of headings:

md
# Heading [!toc]

This heading will be hidden from TOC.

# Another Heading [toc]

This heading will **only** be visible in TOC, you can use it to add additional TOC items.
Like headings rendered in a React component:

<MyComp />

Custom Anchor

You can add [#slug] to customize heading anchors.

md
# heading [#my-heading-id]

You can also chain it with TOC settings like:

md
# heading [toc] [#my-heading-id]

To link people to a specific heading, add the heading id to hash fragment: /page#my-heading-id.

Codeblock

Syntax Highlighting is supported by default using Rehype Code.

mdx
```js
console.log('Hello World');
```

```js title="My Title"
console.log('Hello World');
```

Line Numbers

Show line numbers, it also works with Twoslash and other transformers.

mdx tab="input"
```ts twoslash lineNumbers
const a = 'Hello World';
//    ^?
console.log(a); // [!code highlight]
```
ts twoslash linenumbers tab="output"
const a = 'Hello World';
//    ^?
console.log(a); // [!code highlight]

You can set the initial value of line numbers.

mdx tab="input"
```js lineNumbers=4
function main() {
  console.log('starts from 4');

  return 0;
}
```
js linenumbers=4 tab="output"
function main() {
  console.log('starts from 4');

  return 0;
}

Shiki Transformers

We support some of the Shiki Transformers, allowing you to highlight/style specific lines.

md tab="input"
```tsx
// highlight a line
Hello World // [\!code highlight]

// highlight a word
// [\!code word:Fumadocs]
Fumadocs

// diff styles
console.log('hewwo'); // [\!code --]
console.log('hello'); // [\!code ++]

// focus
return new ResizeObserver(() => {}) // [\!code focus]
```
tsx tab="output"
// highlight a line
<div>Hello World</div> // [!code highlight]

// highlight a word
// [!code word:Fumadocs]
<div>Fumadocs</div>

// diff styles:
console.log('hewwo'); // [!code --]
console.log('hello'); // [!code ++]

// focus
return new ResizeObserver(() => {}) // [!code focus]

Tab Groups

mdx
```ts tab="Tab 1"
console.log('A');
```

```ts tab="Tab 2"
console.log('B');
```
ts tab="tab 1"
console.log('A');
ts tab="tab 2"
console.log('B');

To add a persist ID, define tab-group at the first codeblock:

mdx
```ts tab="Tab 1" tab-group="my-custom-group"
console.log('A');
```

```ts tab="Tab 2"
console.log('B');
```
ts tab="tab 1" tab-group="my-custom-group"
console.log('A');
ts tab="tab 2"
console.log('B');

Another group:

ts tab="tab 1" tab-group="my-custom-group"
console.log('A');
ts tab="tab 2"
console.log('B');

MDX in Tab Groups

While disabled by default, you use MDX in tab values by configuring the remark plugin:

ts tab="fumadocs mdx" title="source.config.ts"
import { defineConfig } from 'fumadocs-mdx/config';

export default defineConfig({
  mdxOptions: {
    remarkCodeTabOptions: {
      parseMdx: true, // [!code ++]
    },
  },
});
ts tab="mdx compiler"
import { compile } from '@mdx-js/mdx';
import { remarkCodeTab } from 'fumadocs-core/mdx-plugins';

await compile('...', {
  remarkPlugins: [
    [
      remarkCodeTab,
      {
        parseMdx: true, // [!code ++]
      },
    ],
  ],
});
mdx
```ts tab=" Tab 1"
console.log('A');
```

```ts tab="<Rocket /> Tab 2"
console.log('B');
```
ts tab=" tab 1"
 tab 1"">console.log('A');
ts tab=" tab 2"
 tab 2"">console.log('B');

Include

This is only available on Fumadocs MDX.

Reference another file (can also be a Markdown/MDX document).
Specify the target file path in `` tag (relative to the MDX file itself).

mdx title="page.mdx"
<include>./another.mdx</include>

See other usages of include.

NPM Commands

Useful for generating commands for installing packages with different package managers.

md tab="input"
```npm
npm i next -D
```
npm tab="output"
npm i next -D

See remark-npm for details.

Steps

Useful for denoting steps for guides, see remark-steps for enable & configure.

md
### Installation [step]

### Write Code [step]

### Deploy [step]

Installation [step]

Write Code [step]

Deploy [step]

Other Components

You can configure & use the built-in components in your MDX document, such as Tabs, Accordions and Zoomable Image.

Additional Features

You may be interested:


Apps Docs Content Docs (Framework) Search Index (`apps/docs/content/docs/(framework)/search/index.mdx`)


Search UI

See Search UI to learn how to customize the UI of document search.

Search Client

You can choose & configure the search client according to your search engine, it defaults to ZBSearch.

Search Hooks

Search clients come with React hooks, like useFetchSearch() and useAlgoliaSearch(), returning the props of <SearchDialog />:

tsx
import { useFetchSearch } from 'fumadocs-core/search/client';

const search = useFetchSearch();

<SearchDialog {...search} {...props} />;

Along with search and onSearchChange for the search input, they return:

  • data: the last successful search, kept while searching again and after an error.
    • items: its results, with their content also parsed as hast as hastContent. The rows of a table are grouped into a table item.
    • query: the query it searched for.
  • error: set when the last search failed.
  • isLoading: whether a search for search is pending.

For other search engines, pass a memoized search function to experimental_useSearch(), it searches again when the function changes.

tsx
'use client';
import { useCallback } from 'react';
import { experimental_useSearch } from 'fumadocs-core/search/client';
import type { SortedResult } from 'fumadocs-core/search';

export function useMySearch(locale: string) {
  const search = useCallback(
    async (query: string): Promise<SortedResult[]> => {
      const res = await fetch(`/api/my-search?${new URLSearchParams({ query, locale })}`);
      return res.json();
    },
    [locale],
  );

  return experimental_useSearch(search);
}

Community Integrations

A list of integrations maintained by community.


Apps Docs Content Docs Headless Content Collections Index (`apps/docs/content/docs/headless/content-collections/index.mdx`)


title: Content Collections

Content Collections is a library that transforms your content into type-safe data collections.

Setup

Follow their official guide to setup Content Collections for your React framework.

Make sure you have MDX configured:

npm
npm i @content-collections/mdx

To integrate with Fumadocs, add the following to your content-collections.ts.

../../examples/content-collections/content-collections.ts

And pass it to loader().

../../examples/content-collections/lib/source.ts

Done! You can access the pages and generated page tree from Source API.

ts
import { getPage } from '@/lib/source';

const page = getPage(slugs);

// MDX output
page?.data.body;

// Table of contents
page?.data.toc;

// Structured Data, for Search API
page?.data.structuredData;

MDX Options

You can customize MDX options in the transformMDX function.

ts
import { defineCollection } from '@content-collections/core';
import { transformMDX } from '@fumadocs/content-collections/configuration';

const docs = defineCollection({
  transform: (document, context) =>
    transformMDX(document, context, {
      // options here
    }),
});

Import Components

To use components from other packages like Fumadocs UI, pass them to your <MDXContent /> component.

tsx
import { MDXContent } from '@content-collections/mdx/react';
import { getMDXComponents } from '@/components/mdx';

return <MDXContent code={page.data.body} components={getMDXComponents()} />;

You can also import them in MDX Files, but it is not recommended.

Content Collections uses `mdx-bundler` to bundle MDX files.
text
To support importing a package from node modules, Fumadocs added a default value to the `cwd` option of MDX Bundler.
It works good, but we still **do not** recommend importing components in MDX files.

Reasons:

- It requires esbuild to bundle these components, while it should be done by the framework's bundler (e.g. Vite or Turbopack)
- You can refactor the import path of components without changing your MDX files.
- With Remote Sources, it doesn't make sense to add an import in MDX files.

Apps Docs Content Docs Headless Mdx Index (`apps/docs/content/docs/headless/mdx/index.mdx`)


title: MDX Plugins index: true


Apps Docs Content Docs Headless Search Index (`apps/docs/content/docs/headless/search/index.mdx`)


title: Search icon: Search index: true


Apps Docs Content Docs Headless Source Api Index (`apps/docs/content/docs/headless/source-api/index.mdx`)


title: Loader API

What it does?

loader() provides an interface for Fumadocs to integrate with different content sources.

  • loader() is a server-side API, not a build-time magic or browser compatible API.
  • It uses in-memory storage, the files are passed from your content sources.

Usage

You can use it with content sources like Fumadocs MDX, see source for details.

ts
import { loader } from 'fumadocs-core/source';
import { docs } from 'collections/server';

export const source = loader({
  source: docs.toFumadocsSource(),
  baseUrl: '/docs',
});

URL

You can override the base URL, or specify a function to generate URL for each page.

ts
import { loader } from 'fumadocs-core/source';

loader({
  baseUrl: '/docs',
  // or you can customize it with function
  url(slugs, locale) {
    if (locale) return '/' + [locale, 'docs', ...slugs].join('/');
    return '/' + ['docs', ...slugs].join('/');
  },
});

Slugs

Define a custom function to generate slugs from page files.

ts
import { loader } from 'fumadocs-core/source';

loader({
  slugs(file, next) {
    console.log(file.path, file.data);
    // you can build on the default slugs with `next()`
    if (file.path.startsWith('blog/')) return ['blog', ...next()];
    // or return `undefined` to fallback to default value
    return ['my', 'slug'];
  },
});

Icons

Load the icon property specified by page and meta files.

ts
import { loader } from 'fumadocs-core/source';
import { icons } from 'lucide-react';
import { createElement } from 'react';

loader({
  icon(icon) {
    if (!icon) {
      // You may set a default icon
      return;
    }

    if (icon in icons) return createElement(icons[icon as keyof typeof icons]);
  },
});

I18n

Pass the i18n config to loader.

ts title="lib/source.ts"
import { i18n } from '@/lib/i18n';
import { loader } from 'fumadocs-core/source';

export const source = loader({
  i18n, // [!code highlight]
});

With i18n enabled, loader will generate a page tree for every locale.

If translations are missing for a page, it fallbacks to fallbackLanguage.

Output

It outputs a content loader instance.

Get Page

Get page with slugs.

ts
import { source } from '@/lib/source';

source.getPage(['slug', 'of', 'page']);

// with i18n
source.getPage(['slug', 'of', 'page'], 'locale');

Get Pages

Get a list of page available for locale.

ts
import { source } from '@/lib/source';

// from any locale
source.getPages();

// for a specific locale
source.getPages('locale');

Page Tree

ts
import { source } from '@/lib/source';

// without i18n
source.getPageTree();

// with i18n
source.getPageTree('locale');

Get from Node

The page tree nodes contain references to their original file path.
You can find their original page or meta file from the tree nodes.

ts
import { source } from '@/lib/source';

source.getNodePage(pageNode);
source.getNodeMeta(folderNode);

Params

A function to generate output for Next.js generateStaticParams.
The generated parameter names will be slug: string[] and lang: string (i18n only).

ts title="app/[[...slug]]/page.tsx"
import { source } from '@/lib/source';

export function generateStaticParams() {
  return source.generateParams();
}

Language Entries

Get available languages and its pages.

ts
import { source } from '@/lib/source';

// language -> pages
const entries = source.getLanguages();

Client API

Loader API is best when used in RSC environments, which allows passing JSX nodes across the server-client boundary.

For non-RSC environments, it provides a tiny serialization layer.

ts tab="server"
import { source } from '@/lib/source';

// where you pass loader data
async function loader() {
  const pageTree = source.getPageTree();

  return {
    pageTree: await source.serializePageTree(pageTree),
  };
}
tsx tab="client"
import { useFumadocsLoader } from 'fumadocs-core/source/client';

function MyComponent() {
  // `useLoaderData()` receives loader data (provided by your React framework)
  const { pageTree, ...data } = useFumadocsLoader(useLoaderData());

  // now render it (e.g. via Fumadocs UI)
  return <DocsLayout tree={pageTree}>...</DocsLayout>;
}

Dynamic Loader

dynamicLoader() provides extra revalidation functionality to content sources, you should use it with a dynamic source.

ts
import { dynamicLoader } from 'fumadocs-core/source';
import { createMySource } from './my-source';

const source = dynamicLoader(createMySource(), {
  baseUrl: '/docs',
});

export async function getSource() {
  return source.get();
}

When you call await source.get(), it returns a normal content loader object:

ts
const docs = await getSource();

const page = docs.getPage(['...']);

Revalidation

Dynamic sources are cached by the loader.

If your source changes over time, call invalidate() to clear the cache, or revalidate() to update the cache:

ts
source.invalidate();

await source.revalidate();

For multi-source setups, you can invalidate/revalidate a specific source:

ts
source.invalidate('docs');

await source.revalidate('docs');

Apps Docs Content Docs Headless Utils Index (`apps/docs/content/docs/headless/utils/index.mdx`)


title: Utilities index: true


Apps Docs Content Docs Mdx Entry Index (`apps/docs/content/docs/mdx/entry/index.mdx`)


title: Accessing Collections icon: FileUp

Overview

After defining collections in your config file, collection entries are compiled into JavaScript files that your app can access.

While the compilation will be done by Fumadocs MDX's bundler/runtime plugins, it also generates entry files to access the collection outputs.

If you prefer defining collections in app modules instead, see the Macro API.

Entry Files

Collection outputs will be generated under the output directory, you can access them from entry files.

See below for a list of available ways to access outputs.


Apps Docs Content Docs Ui Components Index (`apps/docs/content/docs/ui/components/index.mdx`)


title: Components

Overview

Additional components that you can use:

MDX Components

The default MDX components include Cards, Callouts, Code Blocks and Headings.

ts
import defaultMdxComponents from 'fumadocs-ui/mdx';

Server Component only.

To support links with relative file path in href, override the default a component with:

tsx title="app/docs/[[...slug]]/page.tsx"
import { createRelativeLink } from 'fumadocs-ui/mdx';
import { source } from '@/lib/source';
import { getMDXComponents } from '@/components/mdx';

const page = source.getPage(['...']);

return (
  <MdxContent
    components={getMDXComponents({
      // override the `a` tag
      a: createRelativeLink(source, page),
    })}
  />
);
mdx
[My Link](./file.mdx)

Example: ../../(integrations)/feedback.mdx


Apps Docs Content Docs Ui Layouts Index (`apps/docs/content/docs/ui/layouts/index.mdx`)


title: Layouts

Overview

Fumadocs UI provides essential layouts to display content.

Configurations

Each layout supports a shared set of options.

It is recommended to store these options into a file, and pass them to the layouts.

tsx tab="shared options" title="lib/layout.shared.tsx"
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';

export function baseOptions(): BaseLayoutProps {
  return {
    nav: {
      title: 'My App',
    },
  };
}
tsx tab="docs layout" title="app/docs/layout.tsx"
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';
import { source } from '@/lib/source';
import type { ReactNode } from 'react';

export default function Layout({ children }: { children: ReactNode }) {
  return (
    <DocsLayout
      // [!code highlight]
      {...baseOptions()}
      tree={source.getPageTree()}
    >
      {children}
    </DocsLayout>
  );
}
tsx tab="home layout" title="app/(home)/layout.tsx"
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
import type { ReactNode } from 'react';

export default function Layout({ children }: { children: ReactNode }) {
  return (
    <HomeLayout
      // [!code highlight]
      {...baseOptions()}
    >
      {children}
    </HomeLayout>
  );
}

See detailed docs for links and nav options.


Packages Create App Template Astro Content Docs Index (`packages/create-app/template/astro/content/docs/index.mdx`)


title: Hello World

Welcome to the docs! You can start writing documents in /content/docs.

js
console.log('Hello World');

What's next?

The web framework for content-driven websites The docs framework by Fuma

Apps Docs Content Docs (Framework) Integrations Asyncapi Index (`apps/docs/content/docs/(framework)/integrations/asyncapi/index.mdx`)


title: AsyncAPI

Setup

Install the required packages.

npm
npm i @fumadocs/asyncapi shiki

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

Create the AsyncAPI server instance & <AsyncAPIPage /> 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 the direction of messages to each page item in page tree
  plugins: [asyncapiPlugin()],
});

See createAsyncAPI() & createAsyncAPIPage() for available options.

Generate Pages

<Tabs items={["MDX Files", "Virtual Files"]}>

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 <AsyncAPIPage /> 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 (
    <MdxContent
      components={getMDXComponents({
        // [!code ++:3] add the MDX component
        AsyncAPIPage: async (props) => (
          <AsyncAPIPage {...await asyncapi.preloadAsyncAPIPage(page)} {...props} />
        ),
      })}
    />
  );
}
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 (
    <MdxContent
      components={getMDXComponents({
        // [!code ++] add the MDX component
        AsyncAPIPage: (props) => <AsyncAPIPage {...asyncapiData} {...props} />,
      })}
    />
  );
}

You can also use it without generating real files by integrating into Loader API.

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, getText() returns the operations of page in Markdown, with their payloads as TypeScript types:

ts
import { llms } from 'fumadocs-core/source';
import { source } from '@/lib/source';

export const docsLlms = llms(source, {
  renderPage: async (page) => {
    // [!code ++:3]
    if (page.type === 'asyncapi') {
      return `# ${page.data.title}\n\n${await page.data.getText('processed')}`;
    }

    // your original flow below...
  },
});

And update your page renderer:

<Tabs items={["Server Components", "Without Server Components"]}>

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 (
      <DocsPage full>
        <h1 className="text-[1.75em] font-semibold">{page.data.title}</h1>
        <DocsBody>
          <AsyncAPIPage {...page.data.getAsyncAPIPageProps()} />
        </DocsBody>
      </DocsPage>
    );
  }

  // your original flow below...
}

Pass a client payload from server, then render the page using the <AsyncAPIPage /> component you created above.

For example, in Tanstack Start:

text
/* Detailed source-code truncated for AI context efficiency. */

You can see the full AsyncAPI example.

Run a type check to verify before continuing, e.g.

npm
npm run types:check

Features

The official AsyncAPI integration supports AsyncAPI 3:

  • The direction of messages for readers, the server and channel address of operations
  • Messages with their payload and header schemas, including Avro, Protobuf and RAML schemas
  • Message examples, from the document or generated from schemas
  • Request and reply, correlation IDs
  • Security schemes, including OAuth 2.0 flows and SASL mechanisms
  • Protocol details from bindings, for the selected server
  • A playground for WebSocket, Server-Sent Events and custom transports, with text and binary messages
  • Code samples in JavaScript and Python, for WebSocket, MQTT, Kafka, AMQP and NATS
  • TypeScript definitions of payloads and headers
  • Markdown of pages for LLMs
  • Traits

AsyncAPI 2 documents can be converted with npx @asyncapi/cli convert.

Demo

See the AsyncAPI example.


Apps Docs Content Docs (Framework) Integrations Content Index (`apps/docs/content/docs/(framework)/integrations/content/index.mdx`)


title: Content Source

Introduction

Fumadocs is very flexible. You can integrate with any content source, even without an official adapter.

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.

Not enough? Build your own content source.


Apps Docs Content Docs (Framework) Integrations Graphql Index (`apps/docs/content/docs/(framework)/integrations/graphql/index.mdx`)


title: GraphQL

Setup

Install the required packages.

npm
npm i @fumadocs/graphql graphql

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/graphql/css/preset.css';

Configure Plugin

Create the GraphQL server instance & <GraphQLPage /> component.

ts tab="lib/graphql.ts"
import { createGraphQL } from '@fumadocs/graphql/server';

// note: this is a server-side API
export const graphql = createGraphQL({
  // the GraphQL schema, it accepts:
  // SDL files/URLs (including `extend type`), SDL text,
  // introspection results, and `GraphQLSchema` instances.
  input: ['./schema.graphql'],
});
tsx tab="components/api-page.tsx"
'use client';
import { createGraphQLPage } from '@fumadocs/graphql/ui';

export const GraphQLPage = createGraphQLPage({
  playground: {
    // enable the interactive playground on operation pages (optional)
    url: 'https://api.example.com/graphql',
  },
});

See createGraphQL() & createGraphQLPage() for available options.

Generate Pages

Integrate into Loader API to generate pages, without generating real files.

ts title="lib/source.ts"
import { loader } from 'fumadocs-core/source';
import { docs } from 'collections/server';
import { graphql } from '@/lib/graphql';

export const source = loader(
  // [!code ++:6]
  {
    docs: docs.toFumadocsSource(),
    graphql: await graphql.staticSource({
      baseDir: 'graphql',
    }),
  },
  {
    baseUrl: '/docs',
    // [!code ++] optional: adds a query/mutation/subscription badge to each page item in page tree
    plugins: [graphql.loaderPlugin()],
    // ...
  },
);

It generates a page per operation by default, grouped into folders by their kind (e.g. queries/, mutations/). Operation pages show the types they use in place, pages of named types are opt-in.

It will change the type of your pages, make sure to update all references to your source, and update your page renderer:

tsx title="docs/[[...slug]]/page.tsx"
import { GraphQLPage } from '@/components/api-page';

export default function Page({ slug }) {
  const page = source.getPage(slug);

  // for GraphQL pages
  if (page.type === 'graphql') {
    return (
      <DocsPage toc={page.data.toc} full>
        <DocsTitle>{page.data.title}</DocsTitle>
        <DocsDescription>{page.data.description}</DocsDescription>
        <DocsBody>
          <GraphQLPage {...page.data.getGraphQLPageProps()} />
        </DocsBody>
      </DocsPage>
    );
  }

  // your original flow below...
}

Run a type check to verify before continuing, e.g.

npm
npm run types:check

Features

The official GraphQL integration supports:

  • A page per operation (query/mutation/subscription), with the types it uses explored in place
  • Arguments, fields, deprecations, default values and @oneOf input objects
  • Directives applied in the schema, with their definitions
  • Example requests and responses, generated or from your own operations
  • Code samples in cURL, JavaScript, Go, Python, Java, C# and Rust
  • A playground with a variables form, it runs subscriptions over WebSocket or SSE too
  • Markdown of pages for LLMs
  • Optional pages for named types, with their usages across the schema

Demo

See the GraphQL example.


Apps Docs Content Docs (Framework) Integrations Og Index (`apps/docs/content/docs/(framework)/integrations/og/index.mdx`)


title: OG Image Generation index: true

You can configure Open Graph image metadata, which defines the preview image of docs pages in website/social media, such as:


Apps Docs Content Docs (Framework) Integrations Openapi Index (`apps/docs/content/docs/(framework)/integrations/openapi/index.mdx`)


title: OpenAPI

Setup

Install the required packages.

npm
npm i fumadocs-openapi shiki

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-openapi/css/preset.css';

Configure Plugin

Create the OpenAPI server instance & <OpenAPIPage /> component.

ts tab="lib/openapi.ts"
import { createOpenAPI } from 'fumadocs-openapi/server';

// note: this is a server-side API
export const openapi = createOpenAPI({
  // the OpenAPI schema, you can also give it an external URL.
  input: ['./openapi.json'],
});
tsx tab="components/api-page.tsx"
'use client';
import { createOpenAPIPage } from 'fumadocs-openapi/ui';

export const OpenAPIPage = createOpenAPIPage();
ts tab="lib/source.ts"
import { openapiPlugin } from 'fumadocs-openapi/server';
import { loader } from 'fumadocs-core/source';

export const source = loader({
  // [!code ++] optional: adds a badge to each page item in page tree
  plugins: [openapiPlugin()],
});

See createOpenAPI() & createOpenAPIPage() for available options.

Generate Pages

<Tabs items={["MDX Files", "Virtual Files"]}>

You can generate MDX files directly from your OpenAPI schema.

Create a script:

js title="scripts/generate-docs.ts"
import { generateFiles } from 'fumadocs-openapi';
import { openapi } from '@/lib/openapi';

void generateFiles({
  input: openapi,
  output: './content/docs',
  // we recommend to enable it
  // make sure your endpoint description doesn't break MDX syntax.
  includeDescription: true,
});

Generate docs with the script:

bash
bun ./scripts/generate-docs.ts

Add the OpenAPIPage component to your MDX components.

tsx tab="server components" title="app/docs/[[...slug]]/page.tsx"
import { source } from '@/lib/source';
import { openapi } from '@/lib/openapi';
import { OpenAPIPage } 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 (
    <MdxContent
      components={getMDXComponents({
        // [!code ++:3] add the MDX component
        OpenAPIPage: async (props) => (
          <OpenAPIPage {...await openapi.preloadOpenAPIPage(page)} {...props} />
        ),
      })}
    />
  );
}
tsx tab="without server components" title="docs/[...slug]"
import { source } from '@/lib/source';
import { openapi } from '@/lib/openapi';
import { OpenAPIPage } 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 ++]
    openapiData: await openapi.preloadOpenAPIPage(page),
    // ...
  };
}

// e.g. in your page renderer
export default function Page() {
  // consume loader data
  const { openapiData } = useLoaderData();

  return (
    <MdxContent
      components={getMDXComponents({
        // [!code ++] add the MDX component
        OpenAPIPage: (props) => <OpenAPIPage {...openapiData} {...props} />,
      })}
    />
  );
}

You can also use it without generating real files by integrating into Loader API.

ts title="lib/source.ts"
import { loader } from 'fumadocs-core/source';
import { docs } from 'collections/server';
import { openapi } from '@/lib/openapi';

export const source = loader(
  // [!code ++:6]
  {
    docs: docs.toFumadocsSource(),
    openapi: await openapi.staticSource({
      baseDir: 'openapi',
    }),
  },
  {
    baseUrl: '/docs',
    plugins: [openapi.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 === 'openapi') {
      // e.g. return the stringified OpenAPI schema
      return JSON.stringify(page.data.getSchema().bundled, null, 2);
    }

    // your original flow below...
  },
});

And update your page renderer:

<Tabs items={["Server Components", "Without Server Components"]}>

tsx title="docs/[[...slug]]/page.tsx"
import { OpenAPIPage } from '@/components/api-page';

export default function Page({ slug }) {
  const page = source.getPage(slug);

  // for OpenAPI pages
  if (page.type === 'openapi') {
    return (
      <DocsPage full>
        <h1 className="text-[1.75em] font-semibold">{page.data.title}</h1>
        <DocsBody>
          <OpenAPIPage {...page.data.getOpenAPIPageProps()} />
        </DocsBody>
      </DocsPage>
    );
  }

  // your original flow below...
}

Pass a client payload from server, then render the page using the <OpenAPIPage /> component you created above.

For example, in Tanstack Start. Only import source on the server, staticSource() reads files: define Macro API collections in another module, like lib/collections.ts.

text
/* Detailed source-code truncated for AI context efficiency. */

You can see the full Tanstack Start example.

Run a type check to verify before continuing, e.g.

npm
npm run types:check

Features

The official OpenAPI integration supports:

  • Basic API endpoint information
  • Interactive API playground
  • Example code to send request (in different programming languages)
  • Response samples and TypeScript definitions
  • Request parameters and body generated from schemas

Demo

View demo.


Apps Docs Content Docs (Framework) Integrations Story Index (`apps/docs/content/docs/(framework)/integrations/story/index.mdx`)


title: Story

import { story } from './client.story';

<story.WithControl />

Introduction

You can use Fumadocs Story to display & document components.
It is a simple, docs-focused alternative of Storybook, it is mainly designed for component libraries.

If you are new to the concept of Story, this is a short explanation from Storybook docs:

A story captures the rendered state of a UI component. It's an object with annotations that
describe the component's behavior and appearance given a set of arguments.

Fumadocs Story is not a replacement for Storybook, we still recommend Storybook for proper UI testing.

Installation

Follow the installation guide according to your setup:

Configurations

Variants

You can provide multiple variants of the component with args:

ts
import { defineStory } from '@/lib/story';
import { Callout } from '@/components/callout';

export const story = defineStory({
  Component: Callout,
  args: [
    {
      variant: 'Default',
      initial: {
        title: 'This is a Callout',
      },
    },
    {
      variant: 'Warning',
      initial: {
        title: 'This is a Callout',
      },
      // fixed values for props
      fixed: {
        type: 'warning',
      },
    },
  ],
});

`controls`

Controls are generated from the component props, you can specify the control nodes instead:

tsx
import { defineStory } from '@/lib/story';
import { GraphView } from '@/components/graph-view';

export const story = defineStory({
  Component: GraphView,
  args: {
    controls: {
      node: {
        type: 'object',
        properties: [],
      },
    },
  },
});

`frame`

Components filling the viewport, like layouts, can be rendered in an iframe.
The story panel scales the frame down to fit, with a button to open it in fullscreen.

tsx title="components/my-layout.story.tsx"
import { defineStory } from '@/lib/story';
import { MyLayout } from './my-layout.lazy';
import { Window } from './window';

export const story = defineStory({
  Component: MyLayout,
  frame: {
    src: '/story/my-layout',
    // viewport size, optional
    width: 1280,
    height: 800,
    // wraps the frame, e.g. in a window, optional
    Container: Window,
  },
});

Create a page at src to render the component, it receives the arguments from the story panel:

tsx title="app/story/my-layout/page.tsx"
import { story } from '@/components/my-layout.story';

export default function Page() {
  return <story.Frame />;
}

Pages with the story would load the component too, pass a lazy one (e.g. next/dynamic or React.lazy) so only the frame loads it:

tsx title="components/my-layout.lazy.tsx"
'use client';
import dynamic from 'next/dynamic';

export const MyLayout = dynamic(() => import('./my-layout').then((mod) => mod.MyLayout));
  • The page must be on the same origin as your docs.
  • Arguments are sent with postMessage(), they must be serializable (no functions or JSX).
  • Container must be a client component.

Internationalization

Assuming you have configured Internationalization at UI level:

tsx title="layout.shared.tsx"
import { defineI18n } from 'fumadocs-core/i18n';
import { uiTranslations } from 'fumadocs-ui/i18n';
import { storyTranslations } from '@fumadocs/story/i18n';

const i18n = defineI18n({
  languages: ['en', 'cn'],
  defaultLanguage: 'en',
});

export const translations = i18n
  .translations()
  .extend(uiTranslations())
  // [!code ++]
  .extend(storyTranslations())
  .add({
    cn: {
      displayName: 'Chinese',
      // [!code ++]
      'Add Item(story arguments form)': '新增項目',
    },
  });

See Translations for more details.


Apps Docs Content Docs Mdx (Integrations) Loader Index (`apps/docs/content/docs/mdx/(integrations)/loader/index.mdx`)


title: Runtime Loader

Introduction

By default, Fumadocs MDX requires a bundler to work.

When you are accessing your content without a bundler, such as:

  • A script executed by Node.js or Bun.
  • Any unbundled environment.

You will need a runtime loader. See below for available loader integrations.


Apps Docs Components Agent Instructions (`apps/docs/components/agent-instructions.md`)

Set up a Fumadocs site

Instructions for AI coding agents, follow them in order. Fumadocs docs: https://fumadocs.dev/docs, append .mdx to any docs page URL to get it as Markdown, https://fumadocs.dev/llms.txt lists all pages.

1. Keep the docs site separate

Create the docs as its own app: a new directory or repository, or a new workspace package (e.g. apps/docs) in a monorepo.

Don't add Fumadocs to an existing marketing or product app unless the user explicitly asks for it: Fumadocs UI ships global styles and its own layouts, mixing them into an unrelated app leads to style conflicts, coupled deployments and harder upgrades. If the user does want it inside an existing app, skip step 2 and run npx @fumadocs/cli@latest init --yes in the app (adds the docs in a dedicated route group, supports Next.js, Waku, React Router and Tanstack Start), or follow the manual installation guide of the framework:

2. Scaffold with create-fumadocs-app

Use the official scaffolder, it produces the reference project structure with search, llms.txt and OG images configured. Templates:

  • +next+fuma-docs-mdx (Next.js): the default, use it unless the user prefers another framework.
  • waku, react-router, tanstack-start: when the user already uses that framework.
  • +next+fuma-docs-mdx+static, react-router-spa, tanstack-start-spa: when the site must be fully static (hosted on a CDN).

Run it non-interactively (requires Node.js 22+), --yes uses defaults for unspecified options and --pm selects the package manager:

bash
npx create-fumadocs-app@latest my-docs --template +next+fuma-docs-mdx --install --yes

All options: https://fumadocs.dev/docs/cli/create-fumadocs-app.mdx

Don't hand-write the setup from memory or pin older versions, the generated project is the source of truth. Don't edit the .source/ folder, it is generated by Fumadocs MDX.

3. Verify

Run npm run dev, open http://localhost:3000/docs and make sure pages render. Run npm run build before you finish, fix build errors by reading the docs of the relevant feature rather than removing it.

4. Add content

  • Pages are MDX files in content/docs, the folder structure defines the URLs. Every page needs a title in its frontmatter, and ideally a description.
  • meta.json files order pages and folders: https://fumadocs.dev/docs/page-conventions.mdx and https://fumadocs.dev/docs/navigation.mdx
  • When migrating existing Markdown docs, move them as-is and keep their content, only adjust frontmatter and links. Supported syntax and components: https://fumadocs.dev/docs/markdown.mdx
  • Site name and links live in lib/layout.shared.tsx, GitHub info in lib/shared.ts (Next.js template; other templates put app code under src/, check the generated README).

5. Deploy separately

Deploy the docs app as its own project on the hosting provider: https://fumadocs.dev/docs/deploying.mdx

To serve it under the main domain, use a subdomain like docs.example.com, or route /docs/* from the main site to the docs deployment (e.g. Next.js Multi-Zones). Keep the docs under the default /docs route so both options work.


Apps Docs Public Robots (`apps/docs/public/robots.txt`)

User-agent: *
Allow: /

Host: https://fumadocs.dev

Sitemap: https://fumadocs.dev/sitemap.xml


Apps Docs Content Blog 2024 5 15 (`apps/docs/content/blog/2024-5-15.mdx`)


title: How Fumadocs works authors: [fuma] date: 2024-05-15

1 year ago, I was having fun with Next.js App Router.
While experimenting with it on my toy No Deploy, I planned to build documentation.
However, Nextra does not support App Router.

To handle this, I implemented a small documentation site with solely Contentlayer and the new features from App Router.
It was working great, looked blazing-fast and minimal.
I cloned the logic from No Deploy and built this documentation framework.
With a few months of development, it soon became powerful and stable.

It was originally named next-docs, I renamed it to Fumadocs as it conflicts with Next.js Docs.

Thanks to the support from Next.js community, I've received a lot of suggestions along the way.
Fumadocs is now a framework used by my libraries and some other amazing projects.

My Opinion

In Web development, most "robust" frameworks/libraries are incredibly heavy and fabulous, but it has indeed made our developer experience fancy.

On top of Javascript, people built bundlers, transpilers, and even Typescript.
It feels very surprising that Javascript, a high-level scripting language, is more similar to assembly code in modern Web development.
We rarely use them without things like Webpack. This also applies to CSS, at least in my experience, I seldom use CSS without PostCSS.

While they might be necessary for compatibility and DX, the landing of React Server Component and Next.js App Router made the experience even more mindblowing.
It feels like magic. These cunning magical frameworks, and web development miracles.
This kind of design is insane, but it also makes us mindlessly forget the boundaries.

Beginners use Metadata API, while they have no idea how meta tag works.
They put server-side logic in a server component, while they have no idea how expensive the calculation is.
Even when we looked at the code, we can't predict the result without running it in production mode.
I saw too many of these misconceptions.

This happens on many frameworks, they are overly magical.
I wanted to make it less-magic, and straightforward at least for most Next.js developers.

Fumadocs MDX

As the recommended content source, It is actually a webpack hack.
Since Next.js could only optimize static imports, it first transforms your .map.ts to a file that roughly yields:

ts
export default [import("./my/file.mdx"), ...];

And then transforms MDX files with a custom loader. It makes all magic possible, but it doesn't have the ability of lazy-loading MDX files.
Comparing to Nextra, it might be a suboptimal approach.

Nextra does it even easier, it directly transforms MDX files into pages. Because the Pages Router adapts Javascript files as a single page, it is possible.
In App Router, this is not possible anymore. Therefore, I didn't take this approach.

Fumadocs Core

The core of Fumadocs is a bunch of utilities and MDX plugins.

  • Source API construct page trees from content source, integrated with other content providers.
  • Headless components accelerate Fumadocs UI and other custom UI implementations.
  • MDX plugins bring a perfect developer experience to all integrations.
  • Search utilities make it way easier to implement document search.

In addition, it has also established the definitions of Page Tree and Page Conventions.
Overall, it is not yet a framework without Fumadocs UI.

In my opinion, the most valuable part of the codebase is MDX plugins.
I learned a lot more about ASTs and the eco-system of remark/rehype while working on them.
Absolutely an amazing experience.

Fumadocs UI

The UI implementation of Fumadocs using Tailwind CSS and Radix UI.
Its design system was inspired by Shadcn UI, using CSS variables for color utilities.

Although the structure of Fumadocs UI is even simpler than core, I've used some subtle hacks to solve the problem of "use client" directive.
The bundler I am using, TSX, can't handle nested structures like client components imported from a server component.
Therefore, I made a little hack to build server components and client components as an individual entry, then inject import statements after the process.

Also it took me some time to come up with the preset approach for integrating Fumadocs UI with Tailwind CSS projects.

Docs Generators

We have a few built-in integrations, like fumadocs-openapi which takes an OpenAPI schema and output MDX files.

For the OpenAPI one, it simply parses the schema and convert it to MDX file through string templates.

The Typescript integration does a bit more, it obtains type information with Typescript Compiler API. Based on the information, it yields MDX files.
You can use it inside a server component, which is how <AutoTypeTable /> works.

CI/CD

As a project with very few contributors, I built the CI/CD process as convenient as possible for a better efficiency.
The entire release process is handled by Changesets, and I wrote the scripts to update the template repository automatically.
It worked great so far.

Thanks

The Github repository of Fumadocs has reached 300 stars in 2024 March, it is a new achievement for me.
Welcome to give it a star to support my work!

Original
https://fuma-nama.vercel.app/blog/fumadocs


Apps Docs Content Blog 2024 5 16 (`apps/docs/content/blog/2024-5-16.mdx`)


title: 500 Stars authors: [fuma] date: 2024-05-16

It was surprising when I first saw the video from Web Dev Cody, it felt like I had finally built something worth mentioning, instead of another neglected side project that nobody cares about.

So far I've worked on this project for about 1 year, and it is a precious experience to me. The best lecture that you can't find in a university.

Open Source

I received much feedback, issues and questions. Some are brutal but straightforward, like "please fix it, help me please" "Support XXX please". Some are kind and helpful, willing to provide a PR.
I felt both the good and bad sides of open source.

Sometimes, people are being eager and impulsive because of stress and may spread their frustration on the library maintainers.
Even myself, can be affected by stress or a bad mood.

People may hope for instant answers, and maintainers will face questions like "Why are you running away from my questions?".
I understand a library performing bad can cause your precious time to be wasted, but even if we put all our efforts in maintaining the library, it can't be flawless.

Since then, I feel I'm way more respectful to open-source project maintainers.
At least, I'll check my before before firing an issue on somebody else's Github repository, and try to be as respectful as I can with my poor language skills.

Issues

My favourite dev was Anthony Shew, I mentioned him because the issue he opened is the best I've ever seen in my open-source career.
He actually cared about my vision and opinion about the API design, and gave a really concise and constructive feature request.

Obviously, I was not a perfect, neither an experienced library dev.
Fumadocs wasn't capable of many things, such a well-explained feature request and idea is powerful. His passion is inspiring.

Before setting up the YAML issue template, there were very few issues that actually followed the issue template, most of them don't even provide a reproducible repository, or an explanation.

Open a proper issue, follow the instructions, and give maintainers some positive feedback. This is the biggest motivation you can give to maintainers without spending money.

Docs

It's fun to work on the docs of fumadocs. When first building it, I had very little experience on authoring documentation and tutorial content.
Reading through the feedback from developers, the most common problem is that they can't find the docs of something.

I realized the entire docs is unfriendly for beginners to start with, my friends are senior Next.js devs, and of course they are fine with that.
However, not every dev can understand the docs.

So recently, I started to re-write some of the sections, making it easier and appealing for beginners. Welcome to share your feedback on Github Discussions!

What is Next?

Web Development is an ever-evolving industry, but I believe the spirit of open source won't disappear.
I don't know what I will be building in the future, and that doesn't matter.
Let's build a better web!


Apps Docs Content Blog A Mistake (`apps/docs/content/blog/a-mistake.mdx`)


title: The early mistake of Fumadocs authors: [fuma] date: 2025-08-22

A Mistake

The early mistake of Fumadocs was Fumadocs CLI being too late and not getting enough mentions on docs.

To push the CLI approach further like the recent Shadcn CLI support, there are changes we have to make.
Early adopters will face problems if their customizations went beyond what our abstraction can mitigate.

For example: A small CSS change can still break hacky CSS customizations. We can only ensure backward compatibility for the ones scoped by CSS selectors, or exposed options:

css
#nd-sidebar {
  /* this will work forever */
  background-color: red;
}

For CLI to work better, we need layout components to be entirely independent for modularization. A ideal state is that:

  • Normal users can use exposed options, enjoying UI iterations from upstream.
  • Advanced customizations can use Fumadocs CLI, avoiding upstream changes.

Same as the solution described in this post about UI Customization.

A Good Example

For lighter customizations like nuqs did, a PR to migrate from old versions #1056 can reduce code on user land and migration is simple.

The Problems

When your hacks are heavier, migration is much harder combined with the tech debt of your codebase.

Some problems I observed:

Page Tree API being Neglected

Page tree is actually a crucial idea of Fumadocs, unifying all navigation & linking of docs.

Even in the early days, it's already a robust API supporting features like Root Folder (since v12):

Some projects didn't rely on our file-based routing nor page tree API, which resulted in a lot of hacks around sidebar & loader().

Its idea is simple:

txt tab="server"
files -> Fumadocs -> page tree
txt tab="client"
page tree -> navigation components (e.g. page footer & sidebar)

Using it properly can already reduce a massive amount of work, we have a specs for Page Tree for hardcoded page trees.

CSS Variables being Neglected

Fumadocs is using CSS variables for all colors since the beginning, the naming is identical to Shadcn UI which makes customization easy.

But I noticed some are overriding the Tailwind CSS classes rather than colors, like:

css
#nd-sidebar .color-fd-background\/80 {
  color: red;
}

In fact we always support a simpler way:

css
#nd-sidebar {
  --color-fd-foreground: red;
}

Now we also have a Shadcn UI preset to reuse your colors from main app.

Early Reflections

I think the CLI approach is correct, it'll be the primary way for advanced customization.

My future plans:

  • Continue improving Fumadocs CLI: adding install-time optimization to remove unused files according to your options (e.g. use existing sidebar component instead of installing one).
  • A better versioning policy: Reduce abstraction for mitigating upstream changes, instead version the components directly (e.g. creating a new fumadocs-ui/layouts/v2/page instead of a major UI change).
  • Add Fumadocs-native cloud features: More official integrations like monitoring/analytics feature designed for Fumadocs.

My biggest goal is to build my own dev team or startup, I realized my time is too limited to build the excellent software in my imaginary.
There's rough edges that I never found time to polish, and too many things to do - even if AI has handled the basic work.

That's all I want to say :)

Welcome for questions (via DM/GitHub), I'm happy to improve rough edges whenever I have time.


Apps Docs Content Blog Customise Ui (`apps/docs/content/blog/customise-ui.mdx`)


title: Rethink how we customize UI authors: [fuma] date: 2025-04-15

The Problem

Making a docs framework isn't challenging, the hardest problem is how I can make it perfect for everyone, from the perspective of users and developers.
Undoubtedly, this is a dilemma for all frameworks, or even library authors — You cannot write generic code that works out-of-the-box, there's no piece of software that is straight usable while being generic.

For example, headless UI libraries like Radix UI gave us the concept of generic UI components: bare UI primitives that we can customize according to our aesthetics.
However, you'll need time to tune them, these primitives are unusable without further customizations.

Yep it's 2025, Shadcn UI had led a "revolution" on Web Development. It's a great combination of headless UI and styled defaults that takes an extra step away!
But our problem remains unsolved, it might be preposterous that it becomes "unstyled" again when a vast majority of new sites adopted Shadcn UI.

Personally, I felt overwhelmed by nearly identical, ubiquitous flat designs that are obviously one of the default Shadcn UI styles. It has nothing to do with Shadcn UI, but the rest of developers who don't actually care about aesthetics and uniqueness.
Eventually, Shadcn-like design became the default styles, so once again, we will need to do a bit more to make it look stylish.

What should we do?

As a docs framework started from "being flexible", Fumadocs must be customisable, tuned to meet developers' preferences.
We will need to take customization seriously if DX is what we aim. And most importantly, it should encourage developers with a sense of beauty to customize.

The Solution

Typically, there are two types of developers:

those who want docs that just work

Fumadocs need to be opinionated by default such that it is immediately usable, and we only have to offer simplest options to change details. (e.g. colors, overriding components).

Most developers have no desire to install dozens of components just to change a tiny detail, which makes the Shadcn approach suboptimal.

those who need the perfect docs tailored to their preferences

The kernel of Fumadocs, or the spirit we're pursuing is the latter one. I crafted Fumadocs CLI, a version of Shadcn UI dedicated for Fumadocs, the whole solution is now simplified into a single command:

package-install
npx @fumadocs/cli customize

It accounts for two types:

  • Customize the default styles when the exposed options are inadequate.
  • Roll their own layout from scratch.

The first one is self-evident, people will always have absurd needs that we have not even considered.
We can install the original components, so it's simply identical to the default UI which they've been using.

The latter one is simpler, we can offer the minimal template they can start customising from.

This methodology, inspired by ejection, brings some nice advantages:

Don't break anything when you upgrade Fumadocs

Fumadocs need to be opinionated. Hence, we need to iterate the UI frequently which apparently will break people's hacky customizations.
The charm of Fumadocs CLI is that once you've installed the layout, it stays the same forever regardless of future iterations on Fumadocs UI.

It gives the courage for Fumadocs UI to iterate, and developers to customize unburnt by the fear of upgrading dependencies.

Looks Clean

Just like Shadcn UI and Radix UI, Fumadocs UI has a core package (fumadocs-core), most generic logic will be abstracted by core.

What is Next?

The innovation of Shadcn UI is amazing, yet, I believe there's more problems Fumadocs can solve.

See Flags SDK and BetterAuth, you can't tell at the first glance if they're powered by Fumadocs.
I hope it can continue to be a "breakable" framework known for how customisable it is.


Apps Docs Content Blog Fumadocs Mdx Road Map (`apps/docs/content/blog/fumadocs-mdx-road-map.mdx`)


title: The Future of Fumadocs MDX date: 2025-12-05 authors: [fuma] tags: [Fumadocs MDX]

Overview of Fumadocs MDX

Fumadocs MDX serves as a content processing layer, initially developed for processing MDX files and as an alternative to Contentlayer.

Its primary functions include:

  • Compiling and processing content.
  • Offering interfaces to access compiled content.
  • Ensuring type safety and compatibility across environments, such as browsers and Cloudflare Workers.

Comparison with CMS

Fumadocs MDX is not a content management system (CMS). A CMS encompasses more than content processing, it integrates features such as:

  • Database Integration: For storing content.
  • Version Control: To enable rollbacks, commits, and conflict resolution.
  • Permission Management: To handle user access and roles.
  • Authoring Workflow: Supporting stages like drafting, reviewing, and publishing.

Fumadocs MDX lacks these CMS capabilities independently, but they can be achieved through integration with Git and GitHub. This approach offers distinct benefits over traditional CMS solutions, including:

  • Maintaining a unified history within Git, rather than distributing it across Git and a separate CMS.
  • Composing content and code changes in a single pull request.

GitHub's collaboration tools, such as access controls, pull request reviews, and automation, support the implementation of authoring workflows.

Fumadocs MDX is not intended to evolve into a CMS; it remains a foundational content processing layer.

Roadmap

Fumadocs MDX is closely associated with Fumadocs, yet the content processing layer should ideally operate independently, without reliance on any specific documentation framework.

At present, Fumadocs MDX depends solely on the remark and rehype plugins from `fumadocs-core`.

In the near future, certain components will transition to fuma-content, improving extensibility and introducing more APIs. Such as:

  • Framework Agnostic: Fuma Content should work anywhere, including Vite, Turbopack/Webpack, and even JS runtimes.
  • No Framework Defaults: the MDX preset of Fumadocs will be offered by fumadocs-core, likely as a Fuma Content plugin.
  • Content Adapters: Expanding support beyond MDX and JSON files to include custom collection types, as well as an Obsidian plugin for handling editor-specific syntax.
  • Zero breaking changes to Fumadocs MDX: we adopt Fuma Content under the hood, but surface APIs will not be changed. Ideally, it will be similar to how Vite leverages Rolldown.

API-wise, Fuma Content plugins will distinguish from bundler plugins, but plugins can leverage the built-in abstraction layer to implement loaders.
Otherwise, we will encourage Unplugin for advanced needs.

Although fuma-content focuses on content processing, it provides a foundation for developing a CMS layer, such as plugins for MDX editing or remote databases. It will be a bigger goal.

Fumadocs MDX extends beyond a simple MDX loader, its architecture supports multiple formats.

Although no renaming is planned, the name Fumadocs MDX no longer fully captures its scope.

The Current Progress

Fumadocs MDX already includes primitives for advanced setups, such as the new Workspace API, which enables content processing across multiple repositories.

We are welcome for feedbacks on GitHub Discussions!


Apps Docs Content Blog Make A Blog (`apps/docs/content/blog/make-a-blog.mdx`)


title: Making a Blog with Fumadocs authors: [fuma] date: 2024-12-15

Fumadocs is a docs framework, but it's also a powerful tool to manage content in Next.js. You can use Fumadocs to build a blog site along with docs, on a single Next.js app.

Overview

This guide helps you build a blog site with Fumadocs and Fumadocs MDX.

We will use Fumadocs MDX to manage the content, and implement our own UI with Tailwind CSS & Fumadocs UI.

Configure Content

Define a blogPosts collection.

ts title="source.config.ts"
import { defineCollections } from 'fumadocs-mdx/config';
import { pageSchema } from 'fumadocs-core/source/schema';
import { z } from 'zod';

export const blogPosts = defineCollections({
  type: 'doc',
  dir: 'content/blog',
  // add required frontmatter properties
  schema: pageSchema.extend({
    author: z.string(),
    date: z.string().date().or(z.date()),
  }),
});

Parse the output collection in source.ts:

ts title="lib/source.ts"
import { loader } from 'fumadocs-core/source';
import { toFumadocsSource } from 'fumadocs-mdx/runtime/server';
import { blogPosts } from 'collections/server';

export const blog = loader({
  baseUrl: '/blog',
  source: toFumadocsSource(blogPosts, []),
});

You can now access the content from blog.

Implement UI

Create an index page for blog posts.

By default, there should be a (home) route group with <HomeLayout /> inside.
Let's put the blog pages under it, this way we can get the nice navbar on our blog site.

tsx title="app/(home)/blog/page.tsx"
import Link from 'next/link';
import { blog } from '@/lib/source';

export default function Home() {
  const posts = blog.getPages();

  return (
    <main className="flex-1 w-full max-w-[1400px] mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold mb-8">Latest Blog Posts</h1>
      <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
        {posts.map((post) => (
          <Link
            key={post.url}
            href={post.url}
            className="block bg-fd-secondary rounded-lg shadow-md overflow-hidden p-6"
          >
            <h2 className="text-xl font-semibold mb-2">{post.data.title}</h2>
            <p className="mb-4">{post.data.description}</p>
          </Link>
        ))}
      </div>
    </main>
  );
}

Colors like text-fd-muted-foreground are from the design system of Fumadocs UI, you may also use your own colors, or use Shadcn UI.

And create a page for blog posts.

Note that blog posts won't have nested slugs like /slug1/slug2. We don't need a catch-all route for blog posts.

text
/* Detailed source-code truncated for AI context efficiency. */

And configure metadata:

tsx title="app/(home)/blog/[slug]/page.tsx"
import { notFound } from 'next/navigation';
import { blog } from '@/lib/source';

export async function generateMetadata(props: { params: Promise<{ slug: string }> }) {
  const params = await props.params;
  const page = blog.getPage([params.slug]);

  if (!page) notFound();

  return {
    title: page.data.title,
    description: page.data.description,
  };
}

Write Posts

The UI is now complete, you can write some blog posts under the content/blog directory, like:

mdx title="content/blog/hello.mdx"
---
title: Hello World
author: Fuma Nama
date: 2024-12-15
---

## Hello World

This is an example!

After spinning up the development server with next dev, you should see the blog posts under /blog route.


Apps Docs Content Blog Mdx 12 (`apps/docs/content/blog/mdx-12.mdx`)


title: Fumadocs MDX v12 date: 2025-09-20 authors: [fuma] tags: [Release, Fumadocs MDX]

Overview

Fumadocs MDX v12 mainly aims to unify Fumadocs MDX on Vite and Next.js.

It contains renames to certain APIs (import paths are unaffected), migrations may be necessary.

Breaking Changes

Next.js

Removed content on page data in favour of getText().

ts tab="before"
import { source } from '@/lib/source';

const page = source.getPage(['...']);
console.log(page.data.content);
ts tab="after"
import { source } from '@/lib/source';

const page = source.getPage(['...']);
console.log(await page.data.getText('raw'));

API Renames on page data:

  • _file -> info.
  • _file.absolutePath -> info.fullPath.
ts tab="before"
import { source } from '@/lib/source';

const page = source.getPage(['...']);
console.log(page._file, page._file.absolutePath);
ts tab="after"
import { source } from '@/lib/source';

const page = source.getPage(['...']);
console.log(page.info, page.info.fullPath);

Improvements

Vite

Added APIs for info to access raw file info.

ts
import { source } from '@/lib/source';

const page = source.getPage(['...']);
console.log(page.info);

`getText()`

You can use it to access:

  • Raw Markdown content.
  • Processed Markdown before being compiled into HAST (HTML).

This is useful for llms.txt generation when used with docs generations tools & remark plugins.

ts
import { source } from '@/lib/source';

const page = source.getPage(['...']);
console.log(await page.data.getText('raw'));
console.log(await page.data.getText('processed'));

To access processed Markdown content, you need to enable includeProcessedMarkdown in collection config.

ts title="source.config.ts"
import { defineDocs } from 'fumadocs-mdx/config';

export default defineDocs({
  docs: {
    postprocess: {
      includeProcessedMarkdown: true,
    },
  },
});

Apps Docs Content Blog Mdx V10 Summary (`apps/docs/content/blog/mdx-v10-summary.mdx`)


title: Fumadocs MDX v10 Summary date: 2024-11-24 authors: [fuma] tags: [Fumadocs MDX]

Features

  • More customizations via source.config.ts, a new config file for your content.
  • Introduced collections.
  • Turbopack support.
  • Build-time data validation support.

Migrate from v9

Refactor Imports

Refactor the import in next.config.

From:

ts
import createMDX from 'fumadocs-mdx/config';

const withMDX = createMDX();

/** @type {import('next').NextConfig} */
const config = {
  reactStrictMode: true,
};

export default withMDX(config);

To:

ts
import { createMDX } from 'fumadocs-mdx/next';

const withMDX = createMDX();

/** @type {import('next').NextConfig} */
const config = {
  reactStrictMode: true,
};

export default withMDX(config);

Remove `mdx-components.tsx`

mdx-components.tsx is no longer used. It now allows only MDX components passed from MDX body's components prop.

tsx
import defaultMdxComponents from 'fumadocs-ui/mdx';

const MDX = page.data.body;

// set your MDX components with `components` prop
<MDX components={{ ...defaultMdxComponents }} />;

This encourages explicit import of MDX components.

Previously, mdx-components.tsx worked by injecting an import to every compiled Markdown/MDX file.
It's somewhat unnecessary because you can always import the components explicitly, or replace default HTML tags like img from MDX body's components prop.

Define Collections

Collection is now introduced on source config file (source.config.ts).
It refers to a collection of files/content, such as Markdown files or JSON/YAML files.

Every collection has its own config, you can have customized Zod schema to validate its data, or collection-level MDX options for content collections.

You can create a source config file, and add the following:

ts
import { defineDocs } from 'fumadocs-mdx/config';

export const { docs, meta } = defineDocs({
  dir: 'content/docs',
});

defineDocs declares two collections, one with doc type that scans content files (e.g. md/mdx), one with meta type that scans meta files (e.g. json).

Now you can generate types using fumadocs-mdx command, it's recommended to set it as a postinstall script.

json title="package.json"
{
  "scripts": {
    "postinstall": "fumadocs-mdx"
  }
}

When starting the development server, a .source folder will be generated, it contains all parsed collections/files.
You can add it to .gitignore, it will replace the old .map file.

To access the collections, import them from the folder with their original name in source.config.ts.

ts
import { docs, meta } from '@/.source';

Now to integrate it with Fumadocs framework, change your source.ts from:

ts
import { map } from '@/.map';
import { createMDXSource } from 'fumadocs-mdx/runtime/next';
import { loader } from 'fumadocs-core/source';

export const { getPage, getPages, pageTree } = loader({
  baseUrl: '/docs',
  rootDir: 'docs',
  source: createMDXSource(map),
});

to:

ts
import { docs, meta } from '@/.source';
import { createMDXSource } from 'fumadocs-mdx/runtime/next';
import { loader } from 'fumadocs-core/source';

export const source = loader({
  baseUrl: '/docs',
  source: createMDXSource(docs, meta),
});
  • we now recommend exporting the output of loader directly as a single variable.
  • schema option is no longer defined in source.ts, it's handled by source.config.ts.
  • it takes two collections: docs (content) and meta (meta.json).
    You can leave meta as an empty array if it is unused, so you can define only a collection for docs.

`page.data`

When using with loader, you no longer need data.frontmatter to access frontmatter data.
It is merged into the page.data object.

ts
page.data.frontmatter.title; // [!code --]
page.data.title; // [!code ++]

MDX Options

Instead of passing them to next.config file, define a global config in source.config.ts:

ts
import { defineConfig } from 'fumadocs-mdx/config';

export default defineConfig({
  mdxOptions: {
    // here!
  },
});

Collection Schema

The schema option of collection allow you to customize the validation schema, it accepts a Zod type.

For defineDocs, see schema.

Multiple Types

Same as before, you can call loader multiple times for different types (e.g. for docs and blog).

ts
import { createMDXSource } from 'fumadocs-mdx/runtime/next';
import { loader } from 'fumadocs-core/source';
import { meta, docs, blog as blogPosts } from '@/.source';

export const source = loader({
  baseUrl: '/docs',
  source: createMDXSource(docs, meta),
});

export const blog = loader({
  baseUrl: '/blog',
  // as mentioned before, you can leave `meta` an empty array
  source: createMDXSource(blogPosts, []),
});

export type DocsPage = (typeof source)['$inferPage'];
export type DocsMeta = (typeof source)['$inferMeta'];

and the corresponding source.config.ts:

ts
import { defineDocs, defineCollections, frontmatterSchema } from 'fumadocs-mdx/config';
import { z } from 'zod';

export const { docs, meta } = defineDocs({
  dir: 'content/docs',
});

export const blog = defineCollections({
  type: 'doc',
  dir: 'content/blog',
  schema: frontmatterSchema.extend({
    author: z.string(),
    date: z.string().date().or(z.date()).optional(),
  }),
});

Further Readings

You can read the latest docs of Fumadocs MDX for details.

The built-in content source, build a better content experience on Next.js.

Apps Docs Content Blog Mdx V10 (`apps/docs/content/blog/mdx-v10.mdx`)


title: Fumadocs MDX v10 date: 2024-09-06 authors: [fuma] tags: [Release, Fumadocs MDX]

The Problem

Fumadocs MDX worked great for docs. But we also want to prioritize flexibility and code organization.

Previously, it was a simple Webpack loader that turns MDX into JavaScript.
You pass the MDX processor options to the loader, it turns them into JavaScript files.
Then, a .map.ts will be exported:

ts
export const map = {
  'docs/index.mdx': import('./docs/index.mdx'),
  'docs/guide.mdx': import('./docs/guide.mdx'),
};

Your Next.js app will import the map file, and access the available MDX files.

This model works, but we started to see some problems:

  • No built-in way to define multiple collections:

    For example, we have a directory for blog posts and a directory for docs pages.

    On Fumadocs MDX, all these resources are transformed into a single object exported by .map.ts:

    ts
    export const map = {
      'blog/post.mdx': import('./blog/post.mdx'),
      'docs/index.mdx': import('./docs/index.mdx'),
    };

    We implemented a solution using Source API rootDir option, but this is not the ideal way.
    This also gave us another problem:

  • Different MDX Options for each collection:

    Same as the above example, we have a /blog directory for blog posts.
    If we want to add a remark plugin that only work on blog posts, this is impossible with Fumadocs MDX.

    Once you apply a remark plugin, it's effective on all MDX files, including the MDX files in the docs directory.

  • Turbopack Compatibility:

    Turbopack doesn't allow passing unserializable options to loaders.
    However, the entire MDX, remark and rehype ecosystem use functions for plugins.
    Functions are not serializable, we cannot migrate Fumadocs to Turbopack unless we find a seamless solution that supports Turbopack.

  • Compile-time validation:

    All schema validation cannot be done during build time because the MDX loader actually, doesn't know the collections you defined in source.ts.

    Furthermore, the Zod schema is passed to the source adapter in source.ts rather than the loader:

    ts
    import { loader } from 'fumadocs-core/source';
    import { createMDXSource } from 'fumadocs-mdx/runtime/next';
    
    export const source = loader({
      source: createMDXSource(map, {
        // schema
      }),
    });

    This loses some performance optimizations that can be possibly done on bundler level.

  • Untyped:

    The exported map object from .map.ts file has a type of unknown, it is only typed when using it with Source API loader.

    This avoided the complexity to auto-generate types, but I wanted to make it typed and usable without Source API.

The Solution

Taking some references from Content Collections and Velite, I found it would be great to have a config file for Fumadocs MDX.

`source.config.ts`

We can make the syntax similar to Content Collections and other tools, to make the adoption process easier.
To define a collection:

ts
import { z } from 'zod';

export const blog = defineCollections({
  dir: './blog',
  schema: z.object({
    // the schema
  }),
  mdxOptions: {
    // remark plugins?
  },
});

The MDX loader reads the config file, find the corresponding collection of the file, validate, and compile it using the options from collection.

This allows us to pass MDX options normally without breaking Turbopack's rules.

The Implementation

As the config file is written in TypeScript, we will need a bundler to read it.
I used esbuild, it is a performant bundler written in Go.

After bundling the config file, a dynamic import will work as expected.

js
await import('./source.compiled.mjs');

`.map` file

We need a place to import the compiled collections.
Previously, we simply generate a .map.ts file with Webpack plugins.
It declares the types, with no actual data.

ts
export declare const map: unknown;

A loader will be used to transform .map.ts file into the output aforementioned:

ts
export const map = {
  'docs/index.mdx': import('./docs/index.mdx'),
  'docs/guide.mdx': import('./docs/guide.mdx'),
};

The generated .map.ts never changes because it doesn't depend on the config file.
No matter how you configure it, there'll be only a map object exported, with a type of unknown.

Now, we need to generate types for every collection, and the types may change as we change the collections.
The previous approach is no longer applicable.

I renamed the .map.ts file to .source/index, both index.d.ts and index.js are generated by Fumadocs MDX, instead of using a loader.

A map file generator is implemented, it reads the config file and generate output based on exported collections.

Auto-reload

We want to watch for changes:

  • When a input file added/removed, add or remove relevant entries from the .source/index.js file.
  • When the config file changed, re-compile affected files, and update generate types at .source/index.d.ts.

I chose chokidar to watch file changes, it worked well.
The file watcher lives on next.config.mjs, it's independent to the MDX loader.

To notify bundlers when config files changed, we added a hash.

ts
export const collection1 = [import('./docs/index.mdx?hash=hashOfConfigFile')];

The file will be re-compiled as the config hash changes.

To optimize performance, we also added the collection name.

ts
export const collection1 = [
  import('./docs/index.mdx?hash=hashOfConfigFile&collection=collection1'),
];

The loader obtains the collection of input file from resource query, without taking extra steps to detect its associated collection.

Result

A .source/index file will be generated, it is fully typed.
The files will be re-compiled as you modify the config file.

  • Turbopack supported, we have a small example in the repo using Turbopack.
  • Multiple Collections, with its own MDX options.
  • Runtime + Build-time validation & transformation.

Questions

I think there is still room for improvement:

  • Use a Turbopack-native thing to bundle the config file?
  • Lazy bundle/import the main body of MDX file?

Please give me feedback about the redesign of Fumadocs MDX ;)


Apps Docs Content Blog Mdx V13 (`apps/docs/content/blog/mdx-v13.mdx`)


title: Fumadocs MDX v13 authors: [fuma] tags: [Release, Fumadocs MDX] date: 2025-10-23

Overview

Fumadocs MDX refined its API surface for better cross-framework compatibility and flexibility.

Plugins API

We introduced Plugins API, enabling extensible modifications to MDX configurations without bundler-specific code—ideal for file generation (e.g., types, indexes) or config alterations.

There's a json-schema plugin natively provided by Fumadocs MDX.

ts title="source.config.ts"
import jsonSchema from 'fumadocs-mdx/plugins/json-schema';
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';

export const docs = defineDocs({
  dir: 'content/docs',
});

export default defineConfig({
  plugins: [jsonSchema()],
});

Migration

Next.js

createMDXSource and resolveFiles are now relocated to fumadocs-mdx/runtime/next.

This should not affect majority of projects, as docs.toFumadocsSource() is now the preferred way.

Vite

The source.generated.ts file is now moved to .source/index.ts, you are recommended to import the .source folder (with a path alias).

json title="tsconfig.json"
{
  "compilerOptions": {
    "paths": {
      // [!code ++]
      "@/.source": [".source"]
    }
  }
}

Migrate:

  • run dev server/typegen to generate a .source folder.

  • import it over the original source.generated.ts.

  • note that both docs and create-fumadocs-app are updated to .source folder.

We also supported index file generation targeting other runtimes like Bun/Node.js.

ts title="vite.config.ts"
import { defineConfig } from 'vite';
import mdx from 'fumadocs-mdx/vite';
import * as MdxConfig from './source.config';

export default defineConfig({
  plugins: [
    mdx(MdxConfig, {
      generateIndexFile: {
        runtime: 'node',
      },
    }),
    // ...
  ],
});

Bug Fixes

  • remark-include: fix Markdown stringify logics.
  • postprocess.includeMDAST support.
  • (Vite) support useContent on client loader for lint avoidance.

Apps Docs Content Blog Mdx V14 (`apps/docs/content/blog/mdx-v14.mdx`)


title: Fumadocs MDX 14 authors: [fuma] tags: [Release, Fumadocs MDX] date: 2025-11-16

We are pleased to announce Fumadocs MDX 14. This update introduces several major enhancements designed to:

  • Improve flexibility, taking the plugin approach further.
  • Improve performance & memory consumption.
  • Improve browser/dynamic compilation support.

New Model

The output is now separated into:

  • .source/server: the content data available for server (e.g. RSC, SSR), should not be referenced in client code.
  • .source/browser: the data available for browser, with built-in serialization, chunking and loader layer.
  • .source/dynamic: interface for dynamic content compilation, only generated for collections with dynamic: true.

Breaking Changes

Entry Files

Fumadocs MDX now generates multiple entry files in .source folder.

To migrate, add/update the path aliases in tsconfig.json for new entry files.

json title="tsconfig.json"
{
  "compilerOptions": {
    "paths": {
      // [!code ++]
      "collections/*": ["./.source/*"]
    }
  }
}

Import your collections from the entry files instead of @/.source:

ts tab="server/rsc"
// [!code --]
import { docs } from '@/.source';
// [!code ++]
import { docs } from 'collections/server';

// access collections on server-side, e.g.
import { loader } from 'fumadocs-core/source';

export const source = loader({
  source: docs.toFumadocsSource(),
});
tsx tab="browser/non-rsc"
// non-RSC environment (e.g. browser)
// [!code --:20]
import { docs } from '@/.source';

const clientLoader = createClientLoader(docs.doc, {
  id: 'docs',
  component({ toc, frontmatter, default: MDX }) {
    return (
      <DocsPage toc={toc}>
        <DocsTitle>{frontmatter.title}</DocsTitle>
        <DocsDescription>{frontmatter.description}</DocsDescription>
        <DocsBody>
          <MDX
            components={{
              ...defaultMdxComponents,
            }}
          />
        </DocsBody>
      </DocsPage>
    );
  },
});

// [!code ++:20]
import browserCollections from 'collections/browser';

const clientLoader = browserCollections.docs.createClientLoader({
  component({ frontmatter, default: MDX }) {
    return (
      <DocsPage toc={toc}>
        <DocsTitle>{frontmatter.title}</DocsTitle>
        <DocsDescription>{frontmatter.description}</DocsDescription>
        <DocsBody>
          <MDX
            components={{
              ...defaultMdxComponents,
            }}
          />
        </DocsBody>
      </DocsPage>
    );
  },
});

The new browser createClientLoader() should work seamlessly with old usage.

See Browser Entry for more examples of browserCollections.

Fumadocs Adapter

The adapter for loader() API is also changed.

For docs collection, create.sourceAsync() (Vite) is now consistent with Next.js:

ts
import { docs } from 'collections/server';
import { loader } from 'fumadocs-core/source';

export const source = loader({
  baseUrl: '/docs',
  // [!code --]
  source: await create.sourceAsync(docs.doc, docs.meta),
  // [!code ++]
  source: docs.toFumadocsSource(),
});

For doc collections, createMDXSource() (Next.js) is renamed to toFumadocsSource().

If you use create.sourceAsync() (Vite) for doc collections, you should also switch to toFumadocsSource().

ts
import { blogPosts } from 'collections/server';
import { toFumadocsSource } from 'fumadocs-mdx/runtime/server';
import { loader } from 'fumadocs-core/source';

export const blog = loader({
  baseUrl: '/blog',
  // [!code --] for Next.js
  source: createMDXSource(blogPosts, []),
  // [!code --] for Vite
  source: await create.sourceAsync(blogPosts, []),
  // [!code ++]
  source: toFumadocsSource(blogPosts, []),
});

Async & Dynamic Mode

On Next.js, Async Mode now aligns with the behaviour on Vite to generate async imports. This enables server-side chunking to improve memory usage.

ts title="source.config.ts"
import { defineDocs } from 'fumadocs-mdx/config';

export const docs = defineDocs({
  dir: 'content/docs',
  docs: {
    // This no longer forces dynamic compilation! [!code ++]
    async: true,
  },
});

Instead, we introduced a dynamic option to enable dynamic compilation. This is available for both Vite and Next.js.

ts title="source.config.ts"
import { defineDocs } from 'fumadocs-mdx/config';

export const docs = defineDocs({
  dir: 'content/docs',
  docs: {
    // [!code ++]
    dynamic: true,
  },
});

By enabling Async or Dynamic modes, the relevant collections are considered lazy-loaded, offering a load() function to load compiled properties.

Updated Signature for `postInstall()`

It now accepts the framework plugin options, you can customize more properties:

ts tab="before"
import { postInstall } from 'fumadocs-mdx/next';
// or
import { postInstall } from 'fumadocs-mdx/vite';

postInstall('source.config.ts');
ts tab="after"
import { postInstall } from 'fumadocs-mdx/next';
// or
import { postInstall } from 'fumadocs-mdx/vite';

postInstall({ configPath: 'source.config.ts' });

`getDefaultMDXOptions()` -> `applyMdxPreset()`

To support a broader range of presets and accommodate dynamic modes, getDefaultMDXOptions() has been renamed to applyMdxPreset().

ts title="source.config.ts" tab="before"
import { defineCollections, getDefaultMDXOptions } from 'fumadocs-mdx/config';
import { myPlugin } from './remark-plugin';

export const blog = defineCollections({
  type: 'doc',
  mdxOptions: getDefaultMDXOptions({
    remarkPlugins: [myPlugin],
    // Alternatively, use a function to customize plugin order:
    remarkPlugins: (v) => [myPlugin, ...v],
  }),
});
ts title="source.config.ts" tab="after"
import { defineCollections, applyMdxPreset } from 'fumadocs-mdx/config';
import { myPlugin } from './remark-plugin';

export const blog = defineCollections({
  type: 'doc',
  mdxOptions: applyMdxPreset({
    remarkPlugins: [myPlugin],
    // Alternatively, use a function to customize plugin order:
    remarkPlugins: (v) => [myPlugin, ...v],
  }),
});

Introduction of `lastModified` Plugin

The lastModifiedTime option has been replaced by a dedicated lastModified plugin.

This shift promotes a plugin-based architecture for better extensibility.

ts title="source.config.ts"
import { defineConfig } from 'fumadocs-mdx/config';
import lastModified from 'fumadocs-mdx/plugins/last-modified';

export default defineConfig({
  // [!code ++]
  plugins: [lastModified()],
});

Removal of Multiple `dir` Support

Support for specifying multiple directories in a single collection has been discontinued. Instead, utilize the files property to filter and include specific files.

ts title="source.config.ts"
import { defineDocs } from 'fumadocs-mdx/config';

export const docs = defineDocs({
  dir: 'content/guides',
  docs: {
    files: ['./i-love-fumadocs/**/*.{md,mdx}'],
  },
});

Disabling Default Generation of `extractedReferences`

The automatic generation of extractedReferences is no longer enabled by default. To retain this functionality, explicitly activate it via the postprocess option.

ts title="source.config.ts"
import { defineDocs } from 'fumadocs-mdx/config';

export const docs = defineDocs({
  dir: 'content/docs',
  docs: {
    postprocess: {
      // [!code ++]
      extractLinkReferences: true,
    },
  },
});

Vite: `generateIndexFile` -> `index`

The generateIndexFile option of our Vite plugin has been renamed to index.

ts title="vite.config.ts"
import { defineConfig } from 'vite';
import mdx from 'fumadocs-mdx/vite';
import * as MdxConfig from './source.config';

export default defineConfig({
  plugins: [
    mdx(MdxConfig, {
      index: {
        // options
      },
    }),
  ],
});

Patch Changes

  • Resolved an issue with meta file validation when using Bun.
  • Introduced optimizations to reduce unnecessary entry files re-generations.

Thank you for your continued support of Fumadocs. If you encounter any issues during the update, welcome to reach out through our GitHub Discussions.


Apps Docs Content Blog Mdx V15 (`apps/docs/content/blog/mdx-v15.mdx`)


title: Fumadocs MDX 15 authors: [fuma] tags: [Release, Fumadocs MDX] date: 2026-05-07

Overview

This is a boring major release mainly to reduce package size. Breaking changes:

  • fumadocs-core must be 16.7.0 or above.
  • next.config must be ESM.

For Next.js users, if you encounter any error when upgrading, it could be due to ESM compatibility issues, you might be accidentally using CommonJS in next.config.
Consider to rename your config to next.config.mjs or next.config.mts.

If you prefer to use ESM in your entire project, set this in your package.json:

json title="package.json"
{
  "name": "my-app",
  // [!code ++]
  "type": "module"
}

For other unexpected breakages, please open a bug report on our GitHub repository.


Apps Docs Content Blog New Conventions (`apps/docs/content/blog/new-conventions.mdx`)


title: Adding new Conventions date: 2025-04-08 authors: [fuma]

`mdx-component.tsx`

Sometimes there are questions raised by beginners confused about where to add MDX components.
Although it's simply located in page.tsx, which shouldn't be difficult to find, I think some kind of conventions may help, so I decided to add back mdx-components.tsx.

I love to have fewer files, but considering create-fumadocs-app should also act as a starting point for beginners who may not be familiar with MDX, it's much more helpful to provide it by default.

Existing Projects

The convention is optional (only to make the learning curve smoother).
For existing users, you probably knew how to pass MDX components, and no further change is needed.

But you can still make one:

tsx title="mdx-components.tsx"
import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';

export function getMDXComponents(components?: MDXComponents): MDXComponents {
  return {
    ...defaultMdxComponents,
    ...components,
  };
}

And use it in your MDX component.

tsx title="page.tsx"
import { getMDXComponents } from '@/mdx-components';

<MDXContent
  components={getMDXComponents({
    // extend it
  })}
/>;

Deprecating ``

We now prefer a single <RootProvider /> for everything, replacing the need for `` with a i18n prop.

tsx
<RootProvider
  i18n={
    // i18n provider props
  }
>
  {children}
</RootProvider>

This allows Root Provider to handle the hierarchy correctly without revealing too much complexity, for example, the correct order should be:

tsx
<FrameworkProvider>
  <ThemesProvider>
    <I18nProvider>
      <SidebarProvider>
        <SearchProvider>{children}</SearchProvider>
      </SidebarProvider>
    </I18nProvider>
  </ThemesProvider>
</FrameworkProvider>

Changing the hierarchy of different providers may result in problems since they actually rely on each other, now it is all integrated into <RootProvider />, including I18n configuration.

The new Internationalization guide requires Fumadocs 15.2.3 or above.


Apps Docs Content Blog Openapi V10 (`apps/docs/content/blog/openapi-v10.mdx`)


title: Fumadocs OpenAPI v10 authors: [fuma] tags: [Release, Fumadocs OpenAPI] date: 2025-11-06

We are pleased to announce the release of Fumadocs OpenAPI v10.
This major release focuses on enhancing customization capabilities and simplifying API surfaces, it is also an opportunity to drop older/deprecated APIs.

We have introduced multiple breaking changes that require attention during upgrades.

Breaking Changes

Redesign of `createOpenAPI` Usage

This redesign separates API page and server concerns, removes disablePlayground option in favor of playground.enabled, introduces client configuration support, and prefers client configs for adapters.

  1. Isolation of API Page and Server:

    Previously, all options are located in createOpenAPI().

    ts title="lib/openapi.ts"
    import { createOpenAPI } from 'fumadocs-openapi/server';
    import path from 'node:path';
    
    export const openapi = createOpenAPI({
      input: [path.resolve('./scalar.yaml')],
      proxyUrl: '/api/proxy',
      mediaAdapters: {/* adapters */},
      shikiOptions: {/* options */},
    });

    Now, you need to separate API page components and the server declaration.

    ts tab="lib/openapi.ts"
    import { createOpenAPI } from 'fumadocs-openapi/server';
    import path from 'node:path';
    
    export const openapi = createOpenAPI({
      input: [path.resolve('./scalar.yaml')],
      proxyUrl: '/api/proxy',
    });
    ts tab="components/api-page.tsx"
    import { openapi } from '@/lib/openapi';
    import { createAPIPage } from 'fumadocs-openapi/ui';
    
    export const APIPage = createAPIPage(openapi, {
      mediaAdapters: {/* adapters */},
      shikiOptions: {/* options */},
    });
  2. More Options for Playground:
    Use playground.enabled instead of disablePlayground.

    typescript title="components/api-page.tsx"
    export const APIPage = createAPIPage(openapi, {
      playground: {
        enabled: false,
      },
    });
  3. Client Configuration Support:

    The options are now split into server/client configs, some options are moved to the client-side config,

    typescript tab="components/api-page.tsx"
    import client from './api-page.client';
    
    export const APIPage = createAPIPage(openapi, {
      client,
    });
    typescript tab="components/api-page.client.tsx"
    'use client';
    import { defineClientConfig } from 'fumadocs-openapi/ui/client';
    
    export default defineClientConfig({
      playground: {
        transformAuthInputs: (inputs) => [/* modified inputs */],
      },
    });
  4. Client-Side Media Adapters:
    Also forward your media adapters via client config.

    typescript title="components/api-page.client.tsx"
    'use client';
    import { defineClientConfig } from 'fumadocs-openapi/ui/client';
    import { adapters } from './my-media-adapters';
    
    export default defineClientConfig({
      mediaAdapters: adapters,
    });

Renaming Options

The option content.showExampleInFields has been renamed to schemaUI.showExample.

Removal of Centralized `renderer` and `fields` APIs

Fumadocs OpenAPI now prefers per-feature customizations, eliminating the centralized renderer API. Custom render functions are now specified directly in the content option when creating an API page.

tsx title="components/api-page.tsx"
import { openapi } from '@/lib/openapi';
import { createAPIPage } from 'fumadocs-openapi/ui';

export const APIPage = createAPIPage(openapi, {
  content: {
    renderResponseTabs,
    renderAPIExampleLayout,
    renderAPIExampleUsageTabs,
  },
});

For migrating Playground fields options, utilize client-side render APIs:

tsx title="components/api-page.client.tsx"
'use client';
import { defineClientConfig } from 'fumadocs-openapi/ui/client';

export default defineClientConfig({
  playground: {
    renderParameterField: (fieldName, field) => {
      /* custom implementation */
    },
  },
});

Additional layout customizations are available:

tsx title="components/api-page.tsx"
import { openapi } from '@/lib/openapi';
import { createAPIPage } from 'fumadocs-openapi/ui';

export const APIPage = createAPIPage(openapi, {
  content: {
    renderResponseTabs: (tabs) => <div>{/* custom tabs */}</div>,
    renderAPIExampleLayout: ({ selector, usageTabs, responseTabs }) => (
      <div>{/* custom layout */}</div>
    ),
    renderAPIExampleUsageTabs: (generators) => <div>{/* custom tabs */}</div>,
    renderPageLayout: ({ operations, webhooks }) => <div>{/* custom page */}</div>,
    renderOperationLayout: (slots) => <div>{/* custom operation */}</div>,
    renderWebhookLayout: ({ header, authSchemes, parameters, body, responses, callbacks }) => (
      <div>{/* custom webhook */}</div>
    ),
  },
});

`generateFiles()` Requires OpenAPI Server

File generation is now handled within the OpenAPI server. The input field for generateFiles must reference the server instance rather than a string array of file paths.

ts tab="before"
import { openapi } from '@/lib/openapi';
void generateFiles({
  input: ['./products.yaml'],
  output: './content/docs',
});
ts tab="after"
import { generateFiles } from 'fumadocs-openapi';
import { openapi } from '@/lib/openapi';

void generateFiles({
  input: openapi,
  output: './content/docs',
});

New Features

Storage Key Prefix

v10 introduced the storageKeyPrefix option to prevent state bleed across multiple OpenAPI instances by isolating localStorage keys.

tsx title="components/api-page.client.tsx"
'use client';
import { defineClientConfig } from 'fumadocs-openapi/ui/client';

export default defineClientConfig({
  storageKeyPrefix: 'fumadocs-openapi-custom-',
});

Bug Fixes

  • TypeScript Schema Output Correction: Resolved incorrect TypeScript schema generation. Code formatting is disabled in this process to enhance performance.

We recommend reviewing your implementation against these changes to leverage the improved flexibility and performance.
If you encounter issues, welcome to contribute feedback to our GitHub repository.


Apps Docs Content Blog Openapi V11 (`apps/docs/content/blog/openapi-v11.mdx`)


title: Fumadocs OpenAPI v11 authors: [fuma] tags: [Release, Fumadocs OpenAPI] date: 2026-06-11

We are pleased to announce the release of Fumadocs OpenAPI v11.
This major release unifies the RSC and client APIs, removes deprecated options accumulated since v10, and simplifies the server surface.

This release requires attention during upgrades. Run a type check after migrating:

npm
npm run types:check

If you haven't migrated to v10 yet, read the Fumadocs OpenAPI v10 announcement first.

Unify RSC & Client APIs

createAPIPage() and createClientAPIPage() are replaced by a single createOpenAPIPage() API. The component no longer accepts an OpenAPIServer instance or a separate client config, all options live in one client component.

The fumadocs-openapi/ui/client subpath export is removed. Merge any defineClientConfig() options into createOpenAPIPage() directly.

tsx tab="before" title="components/api-page.tsx"
import { openapi } from '@/lib/openapi';
import { createAPIPage } from 'fumadocs-openapi/ui';
import client from './api-page.client';

export const APIPage = createAPIPage(openapi, {
  client,
  mediaAdapters: {/* ... */},
});
tsx tab="before" title="components/api-page.client.tsx"
'use client';
import { defineClientConfig } from 'fumadocs-openapi/ui/client';

export default defineClientConfig({
  playground: {
    transformAuthInputs: (inputs) => inputs,
  },
  storageKeyPrefix: 'my-openapi-',
});
tsx tab="after" title="components/api-page.tsx"
'use client';
import { createOpenAPIPage } from 'fumadocs-openapi/ui';

export const OpenAPIPage = createOpenAPIPage({
  mediaAdapters: {/* ... */},
  playground: {
    transformAuthInputs: (inputs) => inputs,
  },
  storageKeyPrefix: 'my-openapi-',
});

api-page.tsx must be a client component. Note that for existing non-RSC usages, old APIs are only deprecated, migrating to the new createOpenAPIPage() is optional, and API surface is identical.

Pass Page Props from Server

The server is responsible for passing OpenAPI data to <OpenAPIPage />.

<Tabs items={['Virtual Files', 'MDX Files']}>

Use page.data.getOpenAPIPageProps() when rendering OpenAPI pages from virtual files:

tsx title="docs/[[...slug]]/page.tsx"
import { OpenAPIPage } from '@/components/api-page';

export default function Page({ slug }) {
  const page = source.getPage(slug);

  if (page.type === 'openapi') {
    return (
      <DocsPage full>
        <DocsTitle>{page.data.title}</DocsTitle>
        <DocsBody>
          <OpenAPIPage {...page.data.getOpenAPIPageProps()} />
        </DocsBody>
      </DocsPage>
    );
  }

  // ...
}

Use openapi.preloadOpenAPIPage() when rendering generated MDX files:

tsx title="app/docs/[[...slug]]/page.tsx"
import { openapi } from '@/lib/openapi';
import { OpenAPIPage } from '@/components/api-page';

export default async function Page({ slug }) {
  const page = source.getPage(slug);

  return (
    <MdxContent
      components={getMDXComponents({
        OpenAPIPage: async (props) => (
          <OpenAPIPage {...await openapi.preloadOpenAPIPage(page)} {...props} />
        ),
      })}
    />
  );
}

Server & Loader

`getSchema()` Return Value

getSchema() on virtual pages and the OpenAPI server no longer includes a dereferenced document. It now returns the bundled schema only.

ts tab="before"
const schema = page.data.getSchema();

// dereferenced paths, components, etc.
schema.dereferenced.paths;
ts tab="after"
const schema = page.data.getSchema();

// bundled (non-dereferenced) document
schema.bundled;

Dereferencing happens at render time inside <OpenAPIPage /> for better performance.

`input` Option

The whole-map factory form of input is removed. Use a record instead: each value can be a file path, URL, inline document, or a per-entry async function.

ts tab="before" title="lib/openapi.ts"
import { createOpenAPI } from 'fumadocs-openapi/server';

export const openapi = createOpenAPI({
  input: async () => {
    const res = await fetch('https://example.com/openapi.json');
    return {
      my_api: await res.json(),
    };
  },
});
ts tab="after" title="lib/openapi.ts"
import { createOpenAPI } from 'fumadocs-openapi/server';

export const openapi = createOpenAPI({
  input: {
    my_api: async () => {
      const res = await fetch('https://example.com/openapi.json');
      return res.json();
    },
  },
});

Customization Callbacks

Several callbacks now receive richer context about the current operation.

`generateCodeSamples`

tsx tab="before"
import { createCodeSample } from 'fumadocs-openapi/server';

export const OpenAPIPage = createOpenAPIPage({
  generateCodeSamples: (method) => [
    createCodeSample({
      label: 'Custom',
      lang: 'js',
      generate(data) {
        return `fetch('${data.url}')`;
      },
    }),
  ],
});
tsx tab="after"
export const OpenAPIPage = createOpenAPIPage({
  generateCodeSamples: ({ operation, method, pathItem }) => [
    {
      label: 'Custom',
      lang: 'js',
      generate(data) {
        return `fetch('${data.url}')`;
      },
    },
  ],
});

`renderOperationLayout`

tsx tab="before"
export const OpenAPIPage = createOpenAPIPage({
  content: {
    renderOperationLayout: (slots, ctx, method) => <div>{slots.header}</div>,
  },
});
tsx tab="after"
export const OpenAPIPage = createOpenAPIPage({
  content: {
    renderOperationLayout: (slots, { operation, method, pathItem, ctx }) => (
      <div>{slots.header}</div>
    ),
  },
});

`playground.render`

tsx tab="before"
export const OpenAPIPage = createOpenAPIPage({
  playground: {
    render: (method) => <MyPlayground method={method} />,
  },
});
tsx tab="after"
export const OpenAPIPage = createOpenAPIPage({
  playground: {
    render: ({ operation, method, pathItem, path, ctx }) => (
      <MyPlayground operation={operation} method={method} path={path} />
    ),
  },
});

Dropped Deprecated APIs

The following APIs are removed in v11. Use the replacements listed below.

Removed Replacement
transformerOpenAPI() openapiPlugin()
generateTypeScriptSchema() generateTypeScriptDefinitions()
playground.requestTimeout playground.fetchOptions.requestTimeout
allowedUrls (proxy) allowedOrigins or filterRequest
groupStyle (generateFiles / staticSource) folderStyle

Loader Plugin

ts tab="before" title="lib/source.ts"
import { transformerOpenAPI } from 'fumadocs-openapi/server';

export const source = loader({
  pageTree: {
    transformers: [transformerOpenAPI()],
  },
});
ts tab="after" title="lib/source.ts"
import { openapiPlugin } from 'fumadocs-openapi/server';

export const source = loader({
  plugins: [openapiPlugin()],
});

Proxy Server

ts tab="before"
export const { GET, POST } = openapi.createProxy({
  allowedUrls: ['https://example.com'],
});
ts tab="after"
export const { GET, POST } = openapi.createProxy({
  allowedOrigins: ['https://example.com'],
});

Playground Request Timeout

tsx tab="before"
export const OpenAPIPage = createOpenAPIPage({
  playground: {
    requestTimeout: 30,
  },
});
tsx tab="after"
export const OpenAPIPage = createOpenAPIPage({
  playground: {
    fetchOptions: {
      requestTimeout: 30,
    },
  },
});

TypeScript Definitions

tsx tab="before"
export const OpenAPIPage = createOpenAPIPage({
  generateTypeScriptSchema: async (method, statusCode, contentType, ctx) => {
    // ...
  },
});
tsx tab="after"
export const OpenAPIPage = createOpenAPIPage({
  generateTypeScriptDefinitions: async (schema, ctx) => {
    // ...
  },
});

`generateFiles()` Context

The documents field is removed from the beforeWrite and index.items callback context. Access schemas from the OpenAPI server instead.

ts tab="before" title="scripts/generate-docs.ts"
void generateFiles({
  input: openapi,
  output: './content/docs',
  beforeWrite(files) {
    const doc = this.documents['petstore.yaml'];
    // ...
  },
});
ts tab="after" title="scripts/generate-docs.ts"
void generateFiles({
  input: openapi,
  output: './content/docs',
  async beforeWrite(files) {
    const schemas = await openapi.getSchemas();
    const doc = schemas['petstore.yaml'];
    // ...
  },
});

Similarly, rename groupStyle to folderStyle when generating meta.json files:

ts tab="before"
void generateFiles({
  meta: {
    groupStyle: 'separator',
  },
});
ts tab="after"
void generateFiles({
  meta: {
    folderStyle: 'separator',
  },
});

Deprecated APIs

The following APIs are deprecated but still available in v11, migration is recommended.

Virtual Files

The data methods on page.data are unified into getOpenAPIPageProps().

Before After
getAPIPageProps() getOpenAPIPageProps()
getClientAPIPageProps() getOpenAPIPageProps()

Apps Docs Content Blog Openapi V12 (`apps/docs/content/blog/openapi-v12.mdx`)


title: Fumadocs OpenAPI v12 authors: [fuma] tags: [Release, Fumadocs OpenAPI] date: 2026-09-20

We are pleased to announce the release of Fumadocs OpenAPI v12.

An API page used to be one component with a long list of render options. v12 splits it in two:

  • a headless layer that owns the state and logic,
  • the UI it renders through, which you can install and edit.

Options remain for the small tweaks, the rest is yours to replace.

Read the Fumadocs OpenAPI v11 announcement first.

Headless API Pages

Everything outside fumadocs-openapi/ui renders nothing on its own.

Path Module
fumadocs-openapi the page: the document, its servers and your components
fumadocs-openapi/operation an operation: its details, example requests and code usages
fumadocs-openapi/playground the auth state and the fetcher of the playground
fumadocs-openapi/requests encoding the data of an example into a real request
fumadocs-openapi/requests/generators turning that request into code usages

createOpenAPIRenderer() takes your components and returns an <OpenAPIPage /> that accepts the props of generated pages:

tsx title="components/api-page.tsx"
'use client';
import { createOpenAPIRenderer } from 'fumadocs-openapi';
import { Operation } from '@/components/my-operation';
import { SchemaUI } from '@/components/my-schema';

export const OpenAPIPage = createOpenAPIRenderer({
  components: { Operation, SchemaUI },
});

Markdown, CodeBlock and Heading are filled in when you leave them out, so a custom UI starts from two components.

Under the page, hooks read its state:

Hook Returns
useOpenAPI() the dereferenced document (doc) and request options
useComponents() the components passed to the page
useServer() the selected server, its variables, and resolveUrl(pathname)
useOperation() the operation with its details resolved
useExampleRequests() the example requests, the selected one, select() and update()
useCodeUsage(id) the code generated for the selected example
useResponseExamples() responses with example values of their preferred media type

See Headless for the full surface.

Install the UI

The entire UI of API pages, built on that layer, installed with Fumadocs CLI:

npm
npx @fumadocs/cli add openapi/page

Your components/api-page.tsx is no longer needed:

tsx title="mdx-components.tsx"
import { OpenAPIPage } from '@/components/api-page'; // [!code --]
import { OpenAPIPage } from '@/components/openapi/page'; // [!code ++]

Or take one part of it, and pass the rest through components:

tsx title="components/api-page.tsx"
'use client';
import { createOpenAPIPage } from 'fumadocs-openapi/ui';
import { Operation } from '@/components/openapi/operation';
import { Schema } from '@/components/api/schema';

export const OpenAPIPage = createOpenAPIPage({
  components: { Operation, SchemaUI: Schema },
});
Part Install with
Full page openapi/page
Operation UI openapi/operation
API Playground openapi/playground
Schema UI api-docs/schema

What you install is UI, not a fork:

  • the request pipeline of the playground stays in the package, an edited playground still drives the real one.
  • Select and Input follow the Shadcn UI API, a project that already has them keeps its own.

`@fumadocs/json-schema`

The JSON Schema utilities of API pages are their own package, with no Fumadocs dependencies:

ts
import { dereference, matches, mergeAllOf, sample, stringify } from '@fumadocs/json-schema';
import { bundle } from '@fumadocs/json-schema/bundle';
  • bundle() is a separate entry because it reads files and URLs, everything else runs in the browser.
  • @fumadocs/json-schema/react turns a schema into the data an API page draws. generateSchemaUI() flattens it, so your UI walks a map from $root instead of recursing.
  • @fumadocs/api-docs is no longer published, its UI is now bundled into the integrations or installed with the CLI.
Before Now
ParsedSchema JsonSchema
NoReference / NoReferenceSwallow Dereferenced / DereferencedShallow
dereferenceShallow(schema) dereference(schema)
matchesSchema(schema, value) matches(schema, value)
typeMatches(value, type) matchesType(value, type)
schemaToString(schema, FormatFlags.UseAlias) stringify(schema, { alias: true })
Before Now
@fumadocs/api-docs/schema/* @fumadocs/json-schema
@fumadocs/api-docs/components/schema* npx @fumadocs/cli add api-docs/schema
@fumadocs/api-docs/components/* (the UI) installed with the component that uses it
@fumadocs/api-docs/i18n the integration's own Translations covers its keys
@fumadocs/api-docs/css/preset.css already included by the integration's preset

The CLI still installs the Schema UI, now under api-docs/schema.

Smaller Bundles

createOpenAPIBaseRenderer() is the renderer with nothing built in, it bundles only what you pass:

tsx
createOpenAPIBaseRenderer({
  shiki,
  codeUsages: createCodeUsageGeneratorRegistry().register(curl),
  components: { Operation, SchemaUI },
});

createOpenAPIRenderer() and fumadocs-openapi/ui register the full Shiki bundle, every code usage generator and TypeScript definitions for you.

The package entry is client-safe too. generateFiles() reads and writes files, so it ships stubbed under the browser condition, and a client component importing the renderer no longer pulls node:fs into the bundle.

Migration

Run a type check after upgrading:

npm
npm run types:check

Components installed from v11 with Fumadocs CLI, like the API playground, use the old hooks. Reinstall them.

Options

Render options that returned a component are components now:

diff
createOpenAPIPage({
-  schemaUI: { render: (props) => <Schema {...props} /> },
-  renderHeading: (props, depth) => <Heading depth={depth} {...props} />,
-  renderCodeBlock: (props) => <CodeBlock {...props} />,
-  renderMarkdown: (md) => <Markdown md={md} />,
+  components: { SchemaUI: Schema, Heading, CodeBlock, Markdown },
 });
  • playground.provider is removed, the page provides the auth state of the playground.

  • playground.render and generateTypeScriptDefinitions no longer receive ctx. Read the document from useOpenAPI().doc, or the doc passed to generateTypeScriptDefinitions.

  • operation.APIExampleSelector is removed, install openapi/operation and edit the selector in usage-tabs.tsx.

  • `` reads the operation from useOperation(), its route, method, operation and pathItem props are gone:

    tsx
    playground: {
      render: () => <PlaygroundClient writeOnly readOnly={false} />,
    }
  • The ctx of content render options is the render options of the page, what useRenderContext() returns. ctx.schema, ctx.SchemaUI and ctx._default_processMarkdown are removed, read them from useOpenAPI().doc and useComponents().

Hooks

The hooks of fumadocs-openapi/ui are replaced by the headless ones:

v11 v12
useRenderContext() useOpenAPI() (schema is renamed to doc), useComponents(), useRenderContext()
useServerContext() useServer()
useOperationContext() useOperation(), useExampleRequests(), useExampleRequest() (on /operation)
diff
- const { route, examples, example, setExample, setExampleData } = useOperationContext();
+ const { path } = useOperation();
+ const { items, selected, select, update } = useExampleRequests();
+ // data of the selected example, replaces `addListener()`
+ const data = useExampleRequest();

useStorageKey() is removed, read the prefix from the page:

tsx
const { storageKeyPrefix } = useOpenAPI();
localStorage.getItem(`${storageKeyPrefix}my-key`);

Removed & Moved

Removed Use
fumadocs-openapi/ui/create-client createOpenAPIPage() from fumadocs-openapi/ui
ApiPageProps OpenAPIPageProps
OperationItem and WebhookItem of fumadocs-openapi/ui import them from fumadocs-openapi
getAPIPageProps() and getClientAPIPageProps() getOpenAPIPageProps()
defineI18nOpenAPI() i18n.translations().extend(openapiTranslations())
APIPage of MDX components OpenAPIPage, generated files only render it
Was Now
fumadocs-openapi/playground/client fumadocs-openapi/ui/playground/client
fumadocs-openapi/scalar fumadocs-openapi/ui/scalar

GenerateTypeScriptDefinitionsContext comes from the package entry, next to the runtime option it types.

Also in v12

  • Code samples from x-codeSamples and generateCodeSamples are rendered when their id isn't a built-in generator.
  • SchemaData.infoTags entries are data the UI renders: { label, value, block? } or { label, list }. Custom nodes ({ node }) still work, code reading tag.node must handle all shapes.

Thanks for supporting Fumadocs, share your upgrade experience on GitHub :)


Apps Docs Content Blog Openapi V9 (`apps/docs/content/blog/openapi-v9.mdx`)


title: Fumadocs OpenAPI v9 date: 2025-05-25 authors: [fuma] tags: [Release, Fumadocs OpenAPI]

Overview

This breaking change is mainly made to address problems of generateFiles() function.

Previously, it was a simple function to generate multiple MDX files/pages from your OpenAPI schema, same as every docs generator.
We tested it on public OpenAPI schemas such as Unkey, Vercel and examples from Scalar and Swagger's Museum schema.

Since these schemas were written for different docs solutions, or even their custom ones.
We made generateFiles() too complicated to be capable of producing best results for various schema styles, and it's even harder to customize the generated file paths of API docs.

This update also include:

  • Better UI for schema & playground body input
  • Improved type-safety for generateFiles()

Breaking Changes

The algorithm for generating file paths is changed:

  • meta.json will no longer be generated, add as your wanted.
  • per: operation: The generated path will be identical to your operation id. If not defined, it takes your endpoint path instead.

Group By

The behaviour of groupBy API is also changed.

value output
tag {tag}/{operationId ?? endpoint_path}.mdx
route {endpoint_path}/{method}.mdx (ignores name option)
none {operationId ?? endpoint_path}.mdx (default)

Customize Output

The usage of name option is renewed, it can be used to customize the output path of files.

ts
import { generateFiles } from 'fumadocs-openapi';

void generateFiles({
  name: (output, document) => {
    // page info
    console.log(output);
    // parsed OpenAPI schema
    console.log(document);
    return 'my-dir/filename';
  },
});

Migration

You can start using the latest algorithm, or keep the current behaviour with:

ts
import { generateFiles } from 'fumadocs-openapi';

void generateFiles({
  name: {
    algorithm: 'v1',
  },
});

Note that it won't create meta.json anymore even with algorithm set to v1, you can keep your current generated meta.json files.


Apps Docs Content Blog V12 After (`apps/docs/content/blog/v12-after.mdx`)


title: Fumadocs 12.0.5 date: 2024-06-13 authors: [fuma] tags: [Release]

Some Improvements

After releasing Fumadocs 12, some improvements were made to the new UI.

TOC Popover

Previously, this was only available on larger devices. Now, it's collapsed to a popover on smaller devices.

To make the sidebar look even better, we moved navigation links to the links menu (the three-dots button at the top corner).

The recommended design is:

  • Docs related links: Use <RootToggle /> or include it in page trees.
  • Other links: Use the Links API of docs layout.

Removed the duplicated page name from breadcrumbs, now it only shows the folder names.
Also, you can enable includeSeparators to show separators on your breadcrumbs component.

The new breadcrumbs look cleaner and match the design even better.

Adaption

Some docs sites such as https://yeecord.com and https://turbo.build have adopted the new UI, I'm very excited about it.
Removing the navbar seemed to be a very risky move to me, almost no documentation framework has done it, and I was expecting some complaints about the new design.

Luckily, most people are satisfied with the new UI.
The new design was originally inspired by Arc, their sidebar looked very impressive to me.
Although I soon discovered the docs site of Linear also doesn't have a navbar, Arc is still the first inspiration of the redesign.

I'm grateful that many people gave me their feedback about the new design, so that I can keep making Fumadocs a better framework to use.
Let's make better docs.


Apps Docs Content Blog V12 (`apps/docs/content/blog/v12.mdx`)


title: Fumadocs 12 date: 2024-06-09 authors: [fuma] tags: [Release]

What's New?

Fumadocs v12 mainly aims to improve the UI, prioritizing content and reading experience.

New UI

After observing large docs sites like https://turbo.build, I found that the navbar took too much space on the screen, while it only contains links to Blog, Showcase, GitHub, etc.
They are not necessarily related to the docs, placing them at the top of our screen doesn't bring a better reading experience.

All these links are now moved to the sidebar, allowing a clean, minimal view of the docs.

I also noticed the docs start to look messy as your content grows. To ameliorate this, the sidebar now includes a border and background by default.
This helps readers to distinguish navigation elements and content easier.

The sidebar will always be placed at the left of the screen, having a bigger space on large viewports.

I believe the new UI still has room for improvement. Welcome to report UI issues, or leave feedback!

Better Multi Page Trees

We supported multiple page trees at a very early version of Fumadocs. However, it lacks proper explanations and guides to configure it.
In the past, you needed to implement a navigation element to switch between page trees and mark the folder as a root folder.

Now, you can use the <RootToggle /> component directly, and the docs has a better explanation of it.

Page Tree Post Processors

You can now attach properties to page trees easily with the Source API.

ts
import { loader } from 'fumadocs-core/source';

export const utils = loader({
  pageTree: {
    attachFile(node) {
      // modify the node
      return node;
    },
  },
});

Improved i18n Support

The language switch is now added to Docs Layout.

tsx
import { DocsLayout } from 'fumadocs-ui/layouts/docs';

export default function Layout({ children }: { children: React.ReactNode }) {
  return <DocsLayout i18n>{children}</DocsLayout>;
}

Note that <LanguageSelect /> component is now replaced by <LanguageToggle />, make sure to remove references to the old component.

Breaking

UI

Remove deprecated option enableThemeProvider from Root Provider. Use theme.enabled instead.

Core

Remove deprecated option minWidth from Sidebar component. Use blockScrollingWidth instead

Fixes

UI

  • Fix problems with twoslash codeblocks
  • Apply typography styles to headings
  • Support translation for theme label

OpenAPI

  • Fix nullable types cannot be detected

Core

  • Remark Headings: Support code syntax in headings

Create Fumadocs App

  • Add layout.config.tsx file for sharing layout options

Apps Docs Content Blog V13 (`apps/docs/content/blog/v13.mdx`)


title: Fumadocs v13 authors: [fuma] tags: [Release] date: 2024-07-28

Introduction

Fumadocs v13 has been released. It aims to fix the CSS pollution problem and remove deprecated APIs.

New Features

Better Code Blocks

Now supports keeping the original background generated by Shiki on CodeBlock component.

tsx
import { Pre, CodeBlock } from 'fumadocs-ui/components/codeblock';

<MDX
  components={{
    pre: ({ ref: _ref, ...props }) => (
      <CodeBlock keepBackground {...props}>
        <Pre>{props.children}</Pre>
      </CodeBlock>
    ),
  }}
/>;

Callout

Callout is now a default MDX component, you can use it in MDX files without an import, or manually adding it to MDX components.

mdx
<Callout type="warn">Hello World</Callout>

New Headless TOC

The headless component of Table of Contents (TOC) now has a separate scroll container provider.

tsx
import * as Base from 'fumadocs-core/toc';

return (
  <Base.AnchorProvider>
    <Base.ScrollProvider>
      <Base.TOCItem />
      <Base.TOCItem />
    </Base.ScrollProvider>
  </Base.AnchorProvider>
);

The anchor provider can be placed anywhere.

Opt-out of Container

Now supports disabling the default container styles of Tailwind CSS added by Fumadocs UI.

js
import { createPreset } from 'fumadocs-ui/tailwind-plugin';

/** @type {import('tailwindcss').Config} */
export default {
  presets: [
    createPreset({
      modifyContainer: false,
    }),
  ],
};

Admonition Syntax

In Docusaurus, there's an Admonition syntax.

For people migrating from Docusaurus, you can enable the new remark plugin to support the Admonition syntax.

See Remark Admonition.

Breaking Changes

Prefix to colors, animations, and utilities

Previously, the Tailwind CSS plugin of Fumadocs UI added colors (including primary, secondary) which conflicted with Shadcn UI and other design systems.
A fd- prefix is added to them to allow the flexibility to have a separated design system on docs.

To use the Fumadocs UI colors on your primary app, enable the addGlobalColors option.

js
import { createPreset } from 'fumadocs-ui/tailwind-plugin';

/** @type {import('tailwindcss').Config} */
export default {
  presets: [
    createPreset({
      addGlobalColors: true,
    }),
  ],
};

This adds the colors without the fd- prefix.

Or you can just reference them with the fd- prefix, like bg-fd-background.

Tailwind CSS Plugin ESM-only

Since Tailwind CSS supports TypeScript configuration, it makes sense to migrate the entire plugin to ESM.

Use ESM syntax in your configuration.

ts
import { createPreset } from 'fumadocs-ui/tailwind-plugin';

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    './app/**/*.{ts,tsx}',
    // others
  ],
  presets: [createPreset()],
};

Remove `RollButton` component

RollButton was introduced because there were no "Table Of Contents" on mobile viewports.
Now users can use the TOC Popover to switch between headings, it is no longer a suitable design for Fumadocs UI.

You may copy the last implementation of RollButton.

Better i18n

Now the I18nProvider component requires the locale prop instead of parsing it from pathname.
This fixed some bugs with the I18n middleware.

We also changed the usage of translations to reduce extra translations loaded on the client side.
You have to pass the active translations directly.

locales is passed as the available options on the Language Select component.

tsx
import { RootProvider } from 'fumadocs-ui/provider';
import type { ReactNode } from 'react';
import { I18nProvider } from 'fumadocs-ui/i18n';

export default function Layout({
  params: { lang },
  children,
}: {
  params: { lang: string };
  children: ReactNode;
}) {
  return (
    <html lang={lang}>
      <body>
        <I18nProvider
          locale={lang}
          // options
          locales={[
            {
              name: 'English',
              locale: 'en',
            },
            {
              name: 'Chinese',
              locale: 'cn',
            },
          ]}
          // translations
          translations={
            {
              cn: {
                toc: '目錄',
              },
            }[lang]
          }
        >
          <RootProvider>{children}</RootProvider>
        </I18nProvider>
      </body>
    </html>
  );
}

Code Block Usage

The previous usage was confusing, some props are passed directly to pre while some are not.

With v13, pass all props to the CodeBlock component.
This also includes class names, you may change your custom styles if necessary.

tsx
import { Pre, CodeBlock } from 'fumadocs-ui/components/codeblock';

<MDX
  components={{
    // HTML `ref` attribute conflicts with `forwardRef`
    pre: ({ ref: _ref, ...props }) => (
      <CodeBlock {...props}>
        <Pre>{props.children}</Pre>
      </CodeBlock>
    ),
  }}
/>;

Remove Deprecated APIs

  • Remove deprecated fumadocs-ui/components/api components.
  • Replace secondary link items with icon link items.
  • Rename id prop on Tabs component to groupId.

Bug Fixes

UI

  • Fixed empty folder animation problems caused by Radix UI.

Apps Docs Content Blog V14 (`apps/docs/content/blog/v14.mdx`)


title: Fumadocs v14 authors: [fuma] tags: [Release] date: 2024-09-19

Why?

Taking some inspirations from Shadcn UI, I decided to make Fumadocs easier to use, with more APIs that simplify the design.

This is a large update.

New Features

Fumadocs CLI

Clone Fumadocs UI components to your workspace and modify them.

package-install
npm install @fumadocs/cli --save-dev
bash
pnpm fumadocs add

See docs for details.

Previously, multiple page tree is implemented with Fumadocs UI RootToggle component.
You have to add it to the sidebar banner which isn't as intuitive as other APIs.

With Sidebar Tabs, by creating a root folder, Fumadocs will immediately add a tabs component to the sidebar.

json title="meta.json"
{
  "root": true,
  "title": "Tab Name",
  "description": "Some text about the tab",
  "icon": "IconName"
}

Orama

We migrated the built-in search from Flexsearch to Orama, the same search engine that powers the Node.js docs site.
It is open source, and also have their Cloud integration.

No changes required.
You can use our new createFromSource API to create the route handler, which offers i18n support without any configurations.

ts
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';

export const { GET } = createFromSource(source);

In favour of this, the new search client of Fumadocs is introduced with support for different API clients.
This includes Algolia, Orama (static and dynamic with route handlers).

ts
import { useDocsSearch } from 'fumadocs-core/search/client';
import { oramaStaticClient } from 'fumadocs-core/search/client/orama-static';

const client = useDocsSearch({
  client: oramaStaticClient(),
});

See Search API.

For sites with static export, you cannot use route handlers.
We now support client-side search using search indexes generated during build time.

See Static Search.

Less Dependencies

I'm always working to reduce the dependencies required for Fumadocs, that is one reason why we separated Fumadocs into different packages.

Fumadocs UI now bundles icons from lucide-react, for Next.js apps that is not using Lucide, they can avoid downloading the entire icon library.

And swr is no longer used for search client, we implemented a light useQuery hook with extra care on React performance optimization.

New Metadata Image API

To improve code organization, we made a Metadata Image API on Fumadocs Core.
It is a tiny wrapper over Next.js Metadata API, composes seamlessly with Source API.

ts title="lib/metadata.ts"
import { createMetadataImage } from 'fumadocs-core/server';
import { source } from '@/lib/source';

export const metadataImage = createMetadataImage({
  imageRoute: '/docs-og',
  source,
});
ts title="route.ts"
import { generateOGImage } from 'fumadocs-ui/og';
import { metadataImage } from '@/lib/metadata';

export const GET = metadataImage.createAPI((page) => {
  return generateOGImage({
    title: page.data.title,
    description: page.data.description,
    site: 'My App',
  });
});

export function generateStaticParams() {
  return metadataImage.generateParams();
}
tsx title="page.tsx"
import { source } from '@/lib/source';
import { metadataImage } from '@/lib/metadata';
import { notFound } from 'next/navigation';

export function generateMetadata({ params }: { params: { slug?: string[] } }) {
  const page = source.getPage(params.slug);
  if (!page) notFound();

  return metadataImage.withImage(page.slugs, {
    title: page.data.title,
    description: page.data.description,
  });
}

In favour of this, the getImageMeta function from fumadocs-ui/og has been removed.

Shorthands

As you may notice, we introduced more abstractions to reduce the setup steps required for enabling some features.
It is also required for the code generator from Fumadocs CLI to work.

Like the generateParams function, it enables zero-configuration i18n support for Next.js generateStaticParams.

ts
import { source } from '@/lib/source';

export function generateStaticParams() {
  return source.generateParams();
}

Better Card Component

Fumadocs UI Card can now support usage without href.
You can pass children as React node, Typography styles will be applied correctly.

mdx
<Card icon={<Database />} title='Content Source'>

The input/source of your content, it can be a CMS, or local data layers like [Content Collections](https://www.content-collections.dev) and [Fumadocs MDX](/docs/mdx).

</Card>

Better Category Component

The original DocsCategory component was copied from our official docs, which is a very simple implementation that doesn't take page tree into account.

Now it accepts the source object via from prop.

tsx title="page.tsx"
import { source } from '@/lib/source';
import { DocsCategory } from 'fumadocs-ui/page';

const page = source.getPage(params.slug);

<DocsCategory page={page} from={source} />;

By default, it takes the locale property from page to find the corresponding page tree to traverse.
You can also force a page tree.

tsx title="page.tsx (i18n enabled)"
import { source } from '@/lib/source';
import { DocsCategory } from 'fumadocs-ui/page';

const page = source.getPage(params.slug, params.lang);

<DocsCategory page={page} from={source} tree={source.pageTree['en']} />;

OpenAPI Tag Display Name

Change the display name with x-displayName.

yaml title="openapi.yaml"
tags:
  - name: test
    description: this is a tag.
    x-displayName: My Test Name

Better TypeScript Docs Automation

The AutoTypeTable component now supports a type prop, you can use TypeScript inside the field:

mdx
<AutoTypeTable
  path="file.ts"
  name="B"
  type={`
import { ReactNode } from "react"
export type B = ReactNode | { world: string }
`}
/>

And code highlighting in typedoc is also supported with Shiki.

We highly recommend to use createTypeTable instead of importing the component in MDX files, this allows a single instance of TypeScript Compiler API to be shared.
See Auto Type Table.

Next.js 15

Fumadocs v14 is compatible with Next.js 15, supporting sync and async usages of dynamic APIs.

Next.js 14 is also supported, notice that guides/tutorials in the docs are mainly for Next.js 15.

UI Improvements

The navigation menu on Home layout is redesigned, with better animation and flexibility.

See the new API.

You can escape Tailwind CSS Typography styles for the a tag with data-card attribute.

jsx
<a data-card="">
  No styles applied <code>But it does</code>
</a>

Disable Theme Switch

You can disable the default theme switcher with disableThemeSwitch on layouts.

Breaking Changes

Move `dir` option from `defineDocs`

ts
import { defineDocs } from 'fumadocs-mdx/config';

export const { docs, meta } = defineDocs({
  dir: 'my/content/dir', // define once
});

Refactor Imports

Previous

ts
import { DocsLayout } from 'fumadocs-ui/layout';
import { HomeLayout } from 'fumadocs-ui/home-layout';
import { HomeLayoutProps } from 'fumadocs-ui/home-layout';

New Import Path

ts
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';

Twoslash UI

We moved Twoslash UI components to fumadocs-twoslash.
This isolates some logic from Fumadocs UI, allowing a better code organization.

Import the CSS styles and Popup component differently.

ts
import 'fumadocs-twoslash/twoslash.css';

import { Popup } from 'fumadocs-twoslash/ui';

It requires Tailwind CSS.

Remove Deprecated APIs

The previous createI18nSearchAPIExperimental is now renamed to createI18nSearchAPI.
It takes the i18n config instead of scattering the options everywhere.

The types from fumadocs-core/search/shared is moved to fumadocs-core/server.


Apps Docs Content Blog V15 2 (`apps/docs/content/blog/v15-2.mdx`)


title: Fumadocs v15.2 date: 2025-03-28 authors: [fuma] tags: [Release]

Overview

Fumadocs 15.2 introduces a layer over fumadocs-core to extend support to other React frameworks, including React Router and Tanstack Start. Other frameworks can also be supported using the FrameworkProvider from fumadocs-core/framework/base.

Why?

The main focus of Fumadocs is flexibility, I aim to support more frameworks as long as it doesn't change the fundamentals of Fumadocs, like the separation of core and UI package.

The React.js ecosystem is a joy to work with, porting Fumadocs to other frameworks isn't as difficult as I thought.
It's also my goal to make it work not only for Next.js devs, but also everyone in the ecosystem who is using a SSR-compatible React framework.

Breaking Changes

This is a minor release, it shouldn't break any previous usages unless you're relying on the lower level APIs from fumadocs-core without fumadocs-ui.

Fumadocs Core now requires a FrameworkProvider. It's as simple as wrapping it with appropriate provider:

tsx
import { NextProvider } from 'fumadocs-core/framework/next';

export function Provider({ children }) {
  return <NextProvider>{children}</NextProvider>;
}

If you're using Fumadocs UI, there's no need to change. RootProvider included it by default, and allows you to provide your own framework with:

tsx
import { RootProvider } from 'fumadocs-ui/provider/base';

export function Provider({ children }) {
  // now it doesn't add the Next.js provider
  return <RootProvider>{children}</RootProvider>;
}

Please report issues if you find any public APIs are broken unexpectedly.

Compilation Time

15.2 also included some small performance improvements to Fumadocs, the time taken for Turbopack to start dev server and compile its first docs page is about 2-3 seconds.

This change also potentially allows Fumadocs to run on Vite, production build of minimal React Router setup only takes around 4s on a Macbook.

Try it out

Upgrade from your existing Next.js docs:

bash
pnpm update -i -r

If you want to try the React Router example:

bash
pnpm create fumadocs-app

Future Plans

15.2 doesn't support Astro, and probably won't unless better React.js support is provided by Astro.

  • transition:persist is needed for Fumadocs UI layouts, however this will also affect its children (e.g. MDX content), causing page content to be persisted even after navigation.
  • React contexts cannot work across islands, this changes the usage of Fumadocs significantly and hence require a redesign of APIs.

Fumadocs will continue to be Next.js first, the docs won't be changed to generalize usages of other frameworks. Instead, a new docs site will be developed for other frameworks (planned).


Apps Docs Content Blog V15 (`apps/docs/content/blog/v15.mdx`)


title: Fumadocs v15 authors: [fuma] tags: [Release] date: 2025-01-24

Overview

The purpose of v15 is mainly to support Tailwind CSS v4.
Tailwind CSS v4 is a complete redesign of its API and internals, to fully adhere to the new CSS-first config style, a breaking change is required.

Fumadocs v15 has no other significant changes other than migrating the config to Tailwind CSS v4.

Breaking Changes

Before making the switch, upgrade your site to Tailwind CSS v4 following their upgrade guide.
You can remove the unused tailwind.config.js file and fully rely on the new CSS-first config.

Add @import to the presets exported by Fumadocs UI, and include the fumadocs-ui package in the source.

css title="tailwind css"
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';

/* relative to the CSS file, make sure it's correct for your app */
@source '../node_modules/fumadocs-ui/dist/**/*.js';

Since v15, you will no longer pass options in JavaScript to customize plugins.
Instead, you can use the new alternatives in Tailwind CSS v4.

`addGlobalColors: true`

Forward the colors again:

css
@theme {
  --color-primary: var(--color-fd-primary);
  /* same for other colors */
}

CSS Variables

Fumadocs no longer use --fd-<color> like --fd-primary: 0 0% 0% for colors, it directly defines and uses colors in @theme.
Instead of the previous format, it uses hsl():

css
@theme {
  --color-fd-primary: hsl(0, 0%, 100%);
}

Steps

Previous Tailwind CSS utilities like steps & step are renamed to fd-steps and fd-step to avoid conflicts.

Typography

Typography utilities including prose, prose-* modifiers will continue to work. Please report problems if they are no longer working or have unexpected change in behaviours.

Improvements

v15 also includes some minor improvements to the UI and OpenAPI integration.

Code Block Tabs

In the past, you needed to write the <Tabs /> manually when separating code blocks into tabs.

mdx
<Tabs items={["Tab 1", "Tab 2"]}>

```ts tab="Tab 1"
console.log('Hello World');
```

```ts tab="Tab 2"
console.log('Hello World');
```

</Tabs>

Now you can do:

mdx
```ts tab="Tab 1"
console.log('Hello World');
```

```ts tab="Tab 2"
console.log('Hello World');
```

Note that the previous usage still works for those who want to customize or pass props to the <Tabs /> component.

Reversed Items in `meta.json`

The rest item ... in the pages property of meta.json now supports reversed order:

json
{
  "pages": ["z...a"]
}

OpenAPI Playground

v15 improves the playground UI (inspired by the minimalism of Scalar), and brought Scalar API Client support to Fumadocs OpenAPI.

You can enable the Scalar API Client using useScalar option in createOpenAPI():

ts
import { createOpenAPI } from 'fumadocs-openapi/server';
import { APIPlayground } from 'fumadocs-openapi/scalar';

export const openapi = createOpenAPI({
  renderer: {
    APIPlayground,
  },
});

And install & configure their @scalar/api-client-react:

package-install
@scalar/api-client-react
css title="global.css"
@import '@scalar/api-client-react/style.css' layer(base);
Be careful that you must configure Tailwind CSS first, using the pre-built stylesheet of Fumadocs UI will conflict with their `style.css` because both libraries use Tailwind CSS for styling.

Future Plans

This update should be a simple migration for most developers updating to Tailwind CSS v4.

In the future, we also want to make further improvements to Fumadocs MDX:

  • remove transform API - you can easily do the same with .map() on runtime like:
ts
import { blog } from "@/.source"

export const updatedBlog = blog.map(...)
  • remove Manifest API - it was designed to export search indexes, but now it's recommended to implement using route handlers.

  • Mention more about our MDX Remote package, it will be the primary solution to handle large docs sites with performance needs that bundlers couldn't do, including lazy compilation of MDX files.


Apps Docs Content Blog V16 17 (`apps/docs/content/blog/v16-17.mdx`)


title: Fumadocs 16.17 authors: [fuma] tags: [Release] date: 2026-10-09 ogImage: /blog/v16-17.png

import { MarkdownLayer } from './v16-17/layer';
import { Pipeline } from './v16-17/pipeline';
import { RenderToMarkdown } from './v16-17/render';
import { TableRecords } from './v16-17/search';
import { PerfChart } from './v16-17/perf';

We are pleased to announce the release of Fumadocs 16.17.

This release rethinks how Fumadocs turns your pages into Markdown, and builds on top of it:

  • A new stringification pipeline: Markdown that gives humans and agents the right context.
  • Better search: records that keep the structure of your content.
  • Better performance: Markdown and search records are generated over 10x faster.

The Markdown Form of a Page

A page is read in more places than a browser. Agents fetch its Markdown from .md routes, llms.txt and MCP servers, readers copy it into ChatGPT, and your search indexes it.

In Fumadocs, Remark LLMs generates the Markdown, and Remark Structure generates the search records.

For these readers, the Markdown is the page. It should be informative: the content you wrote, not how we render it.

16.17 rethinks the layer that generates it.

The Old Pipeline

Both plugins used to stringify the syntax tree, after the other remark plugins. By then, the tree was already transformed for rendering:

This is what 16.16 generated from a page of 9 lines:

mdx tab="input"
## Installation

```npm
npm i fumadocs-core
```



<auto-type-table path="./props.ts" name="Options" />
md tab="16.16 output"
## Installation [#installation]

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>

  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm i fumadocs-core
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add fumadocs-core
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add fumadocs-core
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add fumadocs-core
    ```
  </CodeBlockTab>
</CodeBlockTabs>

<img alt="Preview" src="__img0" />

<TypeTable
  id="type-table-props.ts-Options"
  type="{
  &#x22;id&#x22;: &#x22;props.ts-Options&#x22;,
  &#x22;name&#x22;: &#x22;Options&#x22;,
  &#x22;description&#x22;: &#x22;&#x22;,
  &#x22;entries&#x22;: [
    {
      &#x22;name&#x22;: &#x22;dir&#x22;,
      &#x22;description&#x22;: &#x22;The directory of pages.&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;string&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;baseUrl&#x22;,
      &#x22;description&#x22;: &#x22;The base URL of pages.&#x22;,
      &#x22;tags&#x22;: [
        {
          &#x22;name&#x22;: &#x22;defaultValue&#x22;,
          &#x22;text&#x22;: &#x22;'/'&#x22;
        }
      ],
      &#x22;type&#x22;: &#x22;string | undefined&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;string&#x22;,
      &#x22;required&#x22;: false,
      &#x22;deprecated&#x22;: false
    }
  ]
}"
/>
  • Generated elements pollute the Markdown. <CodeBlockTabs> and `` are how we render the page, not your content. __img0 doesn't even exist outside the bundle.
  • Components are not rendered. A JSX element is only its tag and props, its content lives in the React component. The type table became a JSON blob.
  • Your Markdown is reformatted. - lists become *, _emphasis_ becomes *emphasis*, and 2 * 3 is escaped into 2 \* 3.
  • It is slow. The whole tree is serialized for every page, and again for every search record.

We fixed it in two parts.

Components, Rendered

Since 16.15, fumadocs-core/server renders React Server Components into Markdown. A component opts in by calling asMarkdown(), and returns its Markdown form:

tsx title="components/callout.tsx"
import { asMarkdown, md } from 'fumadocs-core/server';

export function Callout({ title, children }) {
  if (asMarkdown()) return md.linePrefix('> ')`**${title}**\n${children}`;

  return <div className="callout">...</div>;
}

With output: 'function', Remark LLMs exports the Markdown as a component instead of a string. Markdown stays as you wrote it, JSX elements are rendered by your MDX components:

ts title="source.config.ts"
import { defineDocs } from 'fumadocs-mdx/config';

export const docs = defineDocs({
  docs: {
    postprocess: {
      includeProcessedMarkdown: { output: 'function' },
    },
  },
});
ts
import { getMDXComponents } from '@/components/mdx';

const markdown = await page.data.getText('processed', {
  components: getMDXComponents(),
});

Components that never call asMarkdown(), including client components, are kept as JSX with their props. See Markdown Rendering.

Source + Edits

The other part is where the Markdown comes from.

In 16.17, it is sliced from your source instead of stringified from the tree. Every remark plugin maintains two pipelines:

  • the syntax tree, transformed for rendering.
  • edits to your source, for the generated Markdown.

The rules are simple:

  • A node with a position outputs its source.
  • A node inserted by plugins outputs nothing, the plugin describes its Markdown instead.

The same page, in 16.17:

md
## Installation [#installation]

```npm
npm i fumadocs-core
```



### Options

| Prop       | Type     | Description                           |
| ---------- | -------- | ------------------------------------- |
| `dir`      | `string` | The directory of pages.               |
| `baseUrl?` | `string` | The base URL of pages. Default: `'/'` |

Plugins record their edits with replaceSource():

ts title="remark-note.ts"
import type { Root } from 'mdast';
import type { VFile } from 'vfile';
import { visit } from 'unist-util-visit';
import { replaceSource } from 'fumadocs-core/mdx-plugins/stringifier';

export function remarkNote() {
  return (tree: Root, file: VFile) => {
    visit(tree, 'mdxJsxFlowElement', (node) => {
      if (node.name !== 'Note') return;

      replaceSource(file, node, (s) => `**Note:**\n\n${s.inner(node)}`);
      // transform the node for rendering...
    });
  };
}
  • embedSource() embeds content parsed from another source, like included files.
  • A namespace limits an edit to some outputs, like search for search records.

It brings:

  • Quality: Markdown stays as you wrote it, local images keep their addresses, and generated nodes leave no noise.
  • Performance: no tree is serialized, Remark LLMs is up to 45x faster.

The Sätteri compiler has worked this way since @fumadocs/satteri 0.5, now the remark plugins of Fumadocs Core behave the same. See Stringification for details.

Search records come from the same pipeline, so they are Markdown as you wrote it. The search dialog renders them with typography, and elements like <Callout /> and <TypeTable /> as components.

Tables, by Row

A table records each row, as a table of its header and that row. Type tables from auto-type-table record a row for each prop. In the search dialog, the matched rows of a table are grouped back into a table.

Highlights with CSS

Search results no longer wrap matches in <mark>, the content stays as indexed. The search dialog highlights matches with the CSS Custom Highlight API:

css
::highlight(fd-search) {
  color: var(--color-fd-primary);
  text-decoration: underline;
}

Custom search UIs can use useHighlightQuery() from fumadocs-core/search/client.

Search Hooks

Each search client has its own hook, like useFetchSearch() and useAlgoliaSearch(), returning the props of <SearchDialog />:

tsx
import { useFetchSearch } from 'fumadocs-core/search/client';

const search = useFetchSearch();

<SearchDialog {...search} {...props} />;

useDocsSearch() is deprecated, see Search.

Mixedbread

sync() from fumadocs-core/search/mixedbread uploads a file for each page, whose chunks are its title, headings and paragraphs. Results link to their headings, and render tables like other integrations.

See Mixedbread.

Better Performance

Document Remark LLMs Remark Structure Default plugins
0.5 MB 49 → 1.1 ms 49 → 2.4 ms 119 → 9.4 ms
2 MB 184 → 4.1 ms 196 → 11.1 ms 482 → 34.1 ms
5 MB 498 → 14.1 ms 594 → 30.3 ms 1,496 → 87.5 ms

Default plugins are the remark plugins of Fumadocs MDX, with Remark LLMs enabled.

  • The remark and rehype plugins of Fumadocs Core no longer slow down quadratically on large documents, like rehypeToc() taking 17 ms instead of 239 ms on a 5 MB page.
  • The search dialog parses results with micromark and the HTML parser of browsers, instead of a remark & rehype pipeline.
  • Twoslash analyzes code blocks about 2x faster, and truncates types longer than 500 characters in popups. Add .twoslash to your .gitignore.

Refactored & Deprecated APIs

16.17 is a minor release. Re-sync your search indexes to pick up the new records, including Mixedbread stores synced with mxbai store sync.

The internal APIs of stringification and search are refactored, for plugins and search UIs built on them:

Before Now
data._stringify replaceSource() & embedSource() from fumadocs-core/mdx-plugins/stringifier
defaultStringifier() createStringifier()
placeholder() the mdxAsPlaceholder option
stringify & allowedMdxAttributes of Remark plugins filterElement, and replaceSource() for custom Markdown
useSearchList() getActive(), setActive() & subscribeActive() from useSearch()
contentWithHighlights & renderHighlights useHighlightQuery()
  • Remark Structure defaults types to table instead of tableCell.
  • <SearchDialogListItem /> renders a div, and its renderMarkdown receives content.
  • fumadocs-mdx, fumadocs-docgen and fumadocs-obsidian require fumadocs-core 16.17.0.

Deprecated APIs keep working:

  • useDocsSearch(): use the hook of your search client.
  • createContentHighlighter(): matches are highlighted with CSS.

Also in 16.17

  • Fumadocs OpenAPI 12.4: auth providers for the API playground (playground.authProviders), the OAuth redirect URI with a copy button, and fixed code samples in every language.
  • index() and indexNode() of llms() return a string again when it receives a loader.
  • The advanced search server returns up to 60 results when the request has no limit.

Thanks for supporting Fumadocs, share your upgrade experience on GitHub :)


Apps Docs Content Blog V16 2 (`apps/docs/content/blog/v16-2.mdx`)


title: Fumadocs 16.2 authors: [fuma] tags: [Release] date: 2025-11-29

We are pleased to announce the release of Fumadocs 16.2, introducing a better layout system for Fumadocs UI.

The New Layout System

Handling layouts in documentation frameworks can present significant challenges.

As a solution, Fumadocs UI implemented a Grid System that is composable, flexible, cohesive and predictable.
See the linked docs for more details.

In short, the layout consists of a grid container, and layout components using position: sticky:

css
#nd-docs-layout {
  grid-template:
    'sidebar header toc'
    'sidebar toc-popover toc'
    'sidebar main toc' 1fr / minmax(var(--fd-sidebar-col), 1fr) minmax(0, var(--fd-page-col))
    minmax(min-content, 1fr);

  --fd-docs-row-1: var(--fd-banner-height, 0px);
  --fd-docs-row-2: calc(var(--fd-docs-row-1) + var(--fd-header-height));
  --fd-docs-row-3: calc(var(--fd-docs-row-2) + var(--fd-toc-popover-height));
}

The container defines the offsets of each row, layout components can leverage them according to their grid area:

css
.layout-component {
  position: sticky;
  top: var(--fd-docs-row-1);
  height: calc(var(--fd-docs-height) - var(--fd-docs-row-1));
}

Layout components are also responsible to declare their heights via CSS variables, allowing container to calculate the right row offsets.

css
#nd-docs-layout:has(.layout-component) {
  --fd-header-height: 56px;
}

For user-land customizations, we recommend the Fumadocs CLI customize command, or hooking the relevant grid areas like:

css
.custom-component {
  /* depends on the layout, usually we have toc, toc-popover, sidebar, header, main */
  grid-area: main;
}

Why the Change?

Previously, Fumadocs UI relied on position: fixed and mathematical computations (inspired by Vitepress). The older method often lacked composability and difficult for beginners to manage.

The new system fixed most of the flaws, allowing easier visualizations, while reducing the complexity of previous solutions.

Breaking Changes

Global Styles

While Fumadocs 16.2 preserves compatibility in several areas, notably:

  • The --fd-layout-width CSS variable continues to function as expected.
  • All ID and data attributes remain unchanged.

However, the shift to the new grid system may impact existing customizations, particularly those dependent on the prior fixed-position logic. We recommend reviewing your implementation carefully and testing thoroughly.

If you have already installed layout components via Fumadocs CLI, replace preset.css with preset-legacy.css to keep legacy styles & avoid unexpected breakages:

css title="global.css"
/* [!code --] */
@import 'fumadocs-ui/css/preset.css';
/* [!code ++] */
@import 'fumadocs-ui/css/preset-legacy.css';

No Longer Exposing Internal Components

Layout components such as Root Toggle, Language Toggle, and Theme Toggle are no longer directly exposed.
This decision enables Fumadocs UI to iterate these elements without introducing unexpected breaking changes to usage outside of API surfaces.

Migration Steps: Override the relevant layout components with your own implementations, or utilize the Fumadocs CLI commands (add or customize) to install them locally.

Removal of Internal Contexts

The following context modules are made private:

  • fumadocs-ui/contexts/sidebar
  • fumadocs-ui/contexts/layout

If you have already installed <DocsLayout /> via Fumadocs CLI, import the legacy contexts instead:

ts
// [!code --:2]
import {} from 'fumadocs-ui/contexts/sidebar';
import {} from 'fumadocs-ui/contexts/layout';

// [!code ++:2]
import {} from 'fumadocs-ui/legacy/sidebar';
import {} from 'fumadocs-ui/legacy/layout';

And wrap your app under the legacy <SidebarProvider />:

tsx
import type { ReactNode } from 'react';
import { SidebarProvider } from 'fumadocs-ui/legacy/sidebar';

export function RootLayout({ children }: { children: ReactNode }) {
  return <SidebarProvider>{children}</SidebarProvider>;
}

The legacy contexts are made exclusively for layout components installed locally. If you still
import layout components (e.g. <DocsLayout />) from fumadocs-ui, migrate your usages as follows:

tsx
// [!code --]
import { useSidebar } from 'fumadocs-ui/contexts/sidebar';
// [!code ++]
import { useSidebar } from 'fumadocs-ui/components/sidebar/base';

Ensure the contexts are only accessed within <DocsLayout />.

Minor Changes

The changes below are backward compatible.

Importing Page Layouts by Type

The notebook layout is now isolated from default layout, including their page components.

Make sure to import the appropriate page layout based on your docs layout:

ts
// For docs layout
import { DocsPage } from 'fumadocs-ui/layouts/docs/page';
// For notebook layout
import { DocsPage } from 'fumadocs-ui/layouts/notebook/page';

Migration Steps: Update your imports accordingly. Although the default fumadocs-ui/page will redirect to the correct layout, we strongly advise explicit imports for clarity and future-proofing.

Removal of Unused Styles

Styles related to the abandoned container utility, and --spacing-fd-container variable, have been dropped as they are no longer utilized.

Migration Steps: Remove references to these styles from your codebase.

Conclusion

Fumadocs 16.2 aims to offer greater flexibility for developers. Welcome to share your thoughts about the new design on GitHub Discussion!


Apps Docs Content Blog V16 (`apps/docs/content/blog/v16.mdx`)


title: Fumadocs v16 authors: [fuma] tags: [Release] date: 2025-10-22

Overview

We are pleased to announce the release of Fumadocs v16, a significant update that introduces breaking changes designed to refine our API surface, improve compatibility with Vite frameworks, and boost overall performance.

Major Changes

Fumadocs 16 removed substantial amount of deprecated APIs accumulated since v15.2 Vite support.

  • Removal of the Sidebar API: The fumadocs-core/sidebar module has been deprecated and removed. For sidebar implementations, we recommend adopting pre-built solutions from Shadcn UI.

  • Restructuring of Server-Side Exports: Exports from fumadocs-core/server have been redistributed to more specialized modules:

    • getGithubLastEdit -> fumadocs-core/content/github.
    • getTableOfContents -> fumadocs-core/content/toc.
    • PageTree and related utilities -> fumadocs-core/page-tree.
    • TOCItemType and TableOfContents -> fumadocs-core/toc.
    • removed createMetadataImage API, use Next.js Metadata API instead.
  • Default Switch to Shiki's JavaScript Regex Engine: To ensure seamless compatibility with environments like Cloudflare Workers, the JavaScript engine is now the default over the WASM-based Oniguruma. This change affects rehype-code (via the engine option) and fumadocs-core/highlight.

  • Minimum React.js Version Raised to 19.2.0: React 19.2 brings crucial performance improvements to Fumadocs UI.

    • Next.js Projects: Next.js projects using Fumadocs UI must use Next.js 16 or later, as Next.js uses its own canary channel for React.js.
  • fumadocs-ui/provider is removed:

    • Use fumadocs-ui/provider/next for root provider.
    • Use individual exports for contexts from fumadocs-ui/contexts/*.
  • fumadocs-ui/page:

    • Removed <DocsCategory />, use getPageTreePeers() instead:

      tsx title="page.tsx"
      import { getPageTreePeers } from 'fumadocs-core/page-tree';
      import { source } from '@/lib/source';
      
      <Cards>
        {getPageTreePeers(source.getPageTree(), '/docs/my-page').map((peer) => (
          <Card key={peer.url} title={peer.name} href={peer.url}>
            {peer.description}
          </Card>
        ))}
      </Cards>;
    • Removed the breadcrumbs.full option in <DocsPage />.

  • fumadocs-core/search/algolia: The document option is renamed to indexName.

  • fumadocs-core/search: Removed the deprecated createFromSource() signature in useSearch().

  • fumadocs-core/highlight: Removed withPrerenderScript and loading options from useShiki(), use React Suspense API instead.

  • fumadocs-core/i18n: Removed createI18nMiddleware (import from fumadocs-core/i18n/middleware instead).

  • fumadocs-core/source:

    • Removed transformers, pageTree.attach* options in loader(): use Loader Plugin instead.
    • Removed the page.file property: use page.path instead.
    • Removed internal utilities FileInfo and parseFilePath: use PathUtils instead.
  • Migration to Orama Cloud's New SDK: The @orama/core package replaces the prior client. Update your synchronization code as follows:

    ts
    import { sync } from 'fumadocs-core/search/orama-cloud';
    import { OramaCloud } from '@orama/core';
    
    // [!code highlight:4]
    const orama = new OramaCloud({
      projectId: '<project id>',
      apiKey: '<private api key>',
    });
    
    await sync(orama, {
      index: '<data source id>',
      documents: records,
    });

    You can also continue to use the legacy Orama Cloud client:

    ts
    // [!code --]
    import { oramaCloudClient } from 'fumadocs-core/search/client/orama-cloud';
    // [!code ++]
    import { oramaCloudLegacyClient } from 'fumadocs-core/search/client/orama-cloud-legacy';
    
    useSearch({
      // [!code --]
      client: oramaCloudClient(...),
      // [!code ++]
      client: oramaCloudLegacyClient(...),
    })

    Refer to the Orama Cloud documentation for further details.

Styling Changes

Fumadocs UI has included some aggressive styling changes that you should know about.

  • The default --fd-layout-width CSS variable is set to 1600px for optimal content scaling.
    You can override it if needed.

    css
    :root {
      --fd-layout-width: 100vw;
    }

Fumadocs OpenAPI 9.6

A redesigned schema UI emphasizes client-side rendering (CSR) for recursive components, reducing bundle sizes and enhancing performance in complex JSON schemas.

Non-required fields now include a reset button, with support for content.showExampleInFields.

What's Next?

Fumadocs v16 is a new start for Fumadocs. We appreciate your feedback and contributions—please share your experiences on GitHub. Thank you for supporting Fumadocs :)


Apps Docs Content Blog Why Docs (`apps/docs/content/blog/why-docs.mdx`)


title: Why do you need docs? date: 2024-05-26 authors: [fuma]

People often ask me, do we really need a framework to build docs sites? Well, You don't.

Documentation sites are so important in software development,
internal docs for developers in your company to understand the architecture,
docs for frameworks,
docs for web standards...

Building docs seems simple, but can be difficult.

There are so many paradigms to build docs, but writing beginner-friendly docs could be difficult.
As a result, people tend to use powerful docs frameworks, making the docs site interactive and straightforward.

Over-Engineered

For the docs of a small library/API service, you probably don't need to setup a Next.js site and spend time writing your site.
Neither Nextra nor Fumadocs could be better than GitHub wiki and Swagger docs in this case.

They offer a good, decent UI, basic functionalities, and a proper workflow of authoring docs.
The DX is good enough, I can't think of a reason to switch to a full-powered docs framework just to make your docs look fancy.

I'll just recommend writing your docs in markdown, make it accessible on your GitHub repository.

Why Framework?

Now you may wonder, why major services and frameworks have their own docs sites built with docs frameworks?

Of course, Usually using things like GitHub Wiki is adequate, but it is not always true.
Let's take Component Library for example, you cannot showcase your components with Markdown.
You will constantly find an ordinary Markdown document lacks capability and flexibility.

Documentation frameworks aim to solve this problem, with the ability to integrate with major libraries like React.js and Vue.js.
Good examples are Vitepress, Mintlify and Nextra - They made writing docs more convenient and effective, while offering a better, dedicated experience to readers.

For anything more than a simple library or API service, it is worth trying.

Reinvent the Wheels

I would never recommend building a "custom docs site" on your own, without a proper docs framework.
Despite the Don't re-invent the wheels principle, your hand-made docs site actually takes way more effort to make it decent.

  1. Document Search
  2. A user-friendly navigation experience
  3. Reading experience
  4. UI/UX Design

Implementing them properly already sounds nerve-racking, right?

The docs itself, is definitely not your first priority. You should never spend your precious time re-inventing the wheels - it isn't worth it.

From my perspective, I'd rather use GitHub Wiki than re-inventing the wheels.
Why don't pick a decent solution? It saves your indispensable time, and help reduce the shitty docs sites on the internet.

What do we focus at Fumadocs?

I personally value reading experience more than a fancy eye-catching UI.
You may notice, we do not have animations everywhere, and we avoided many fancy designs.

Fanciness of UI should stay only in landing page, a docs site should focus on content.
Navigation elements are helpers to browse your site, never should they take up too much space on the screen.

One thing I hated the most is the design of two sidebars, it is confusing and meaningless.
You can just organize all items to a single, clean sidebar, but people instead added two hamburger buttons to the navbar.

My favourite docs site is still Linear docs, looks good and simple.

Conclusion

  1. You don't need a full-powered docs framework for a small library
  2. Don't make a docs site on your own, use a proper docs framework
  3. Fumadocs focuses on reading experience
  4. You should focus on content, too

Examples Astro Content Docs Test (`examples/astro/content/docs/test.mdx`)


title: Components

Code Block

js
console.log('Hello World');

Cards


Examples Asyncapi Content Docs Auth (`examples/asyncapi/content/docs/auth.mdx`)


title: Authentication

Security requirements are defined per operation in the AsyncAPI document. Supported schemes depend on the protocol and server configuration.

Security Schemes

Check the Authorization section on each operation page for the applicable security schemes and scopes.

When connecting to a broker or gateway, configure credentials according to your environment (for example API keys, SASL, or TLS client certificates).


Examples Base Ui Content Docs Test (`examples/base-ui/content/docs/test.mdx`)


title: Components

Code Block

js
console.log('Hello World');

Cards


Examples Content Collections Content Docs Test (`examples/content-collections/content/docs/test.mdx`)


title: Hello World

Hey there!

Heading

Heading

js
console.log('Hello World');

Heading


Examples I18n Content Docs Test (`examples/i18n/content/docs/test.mdx`)


title: Hello World

Hey there!

Heading

Heading

js
console.log('Hello World');

Heading


Examples Next Local Md Content Docs Test (`examples/next-local-md/content/docs/test.mdx`)


title: Components

Code Block

js
console.log('Hello World');

Cards

no


Examples Next Min Content Docs Test (`examples/next-min/content/docs/test.mdx`)


title: Test

Hey there!

Cards

CodeBlock

js
console.log('Hello World');

List

  • Hello
  • World

Examples Next Min Content Docs Test2 (`examples/next-min/content/docs/test2.mdx`)


title: Fumadocs

Overview

Fumadocs is a docs framework.


Examples Next Static Content Docs Test (`examples/next-static/content/docs/test.mdx`)


title: Components

Code Block

js
console.log('Hello World');

Cards


Examples Next Content Docs Test (`examples/next/content/docs/test.mdx`)


title: Components

Code Block

js
console.log('Hello World');

Cards


Examples Openapi Content Docs Auth (`examples/openapi/content/docs/auth.mdx`)


title: 'Authentication'

You'll need to authenticate your requests to access some of the endpoints in the Unkey API. In this guide, we'll look at how authentication works.

Bearer Token

When requesting resources, you will need your root key — you will find it in the Dashboard. Here's how to add the root key to the request header using cURL:

bash
curl https://api.unkey.dev/v1/... \
  -H "Authorization: Bearer unkey_xxx"

Always keep your root key safe and reset it if you suspect it has been compromised.


--- METRICS ---