Fumadocs

Content

Write docs pages, and render them with MDX components.

Pages

Each MDX file in content/docs is a page, with the same page conventions and Markdown features as other Fumadocs apps.

URLs start with your source's baseUrl. With Astro's base, include it in baseUrl, like /my-base/docs.

Docs pages are generated at build time, or rendered on request with output: 'server' like other Astro pages.

Frontmatter

content/docs/setup.mdx
---
title: Setup
description: Install the app.
# use all available width
full: true
---

To add custom fields, extend the collection schema:

src/content.config.ts
import { defineCollection } from 'astro:content';
import { pageSchema } from 'fumadocs-core/source/schema';
import { metaLoader, pageLoader } from '@fumadocs/astro/loaders';
import { z } from 'astro/zod';

export const collections = {
  docs: defineCollection({
    loader: pageLoader(),
    schema: pageSchema.extend({ draft: z.boolean().optional() }), 
  }),
  meta: defineCollection({ loader: metaLoader() }),
};

MDX Components

Pages render with Fumadocs UI components by default. To use your own, create a module whose default export is the MDX components:

import defaultMdxComponents from 'fumadocs-ui/mdx';
import type { MDXComponents } from 'mdx/types';

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

export default getMDXComponents();

Rendering

The content of each page renders as one React island, so components share a React tree and hydrate like on other frameworks: code tabs, accordions, and JSX props like icon={<Icon />} work.

Parts that React can't render are rendered by Astro, inside the island:

  • Highlighted code blocks.
  • Images, processed by Astro's image pipeline.
  • Components imported in the MDX file, Astro components included. Add client:load to hydrate framework components.
  • {expressions}, they can't use your MDX components module.

MDX component props are serialized into the island and must be JSON-serializable. The island is its own React tree, so use wrapper to add providers.

Components outside the docs page island, like Graph View, need a framework provider of their own:

import { AstroProvider } from 'fumadocs-core/framework/astro';
import { navigate } from 'astro:transitions/client';
import { GraphView, type GraphViewProps } from './graph-view';

export function Graph({ pathname, ...props }: GraphViewProps & { pathname: string }) {
  return (
    <AstroProvider pathname={pathname} navigate={navigate}>
      <GraphView {...props} />
    </AstroProvider>
  );
}

Lazy Components

Every page loads your MDX components module. Wrap large components like <OpenAPIPage /> in lazy() so other pages skip them:

src/components/mdx.tsx
import { lazy } from 'react';

const OpenAPIPage = lazy(() => import('./api-page').then((mod) => ({ default: mod.OpenAPIPage })));

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

Markdown

Configure Markdown on the integration. Astro's markdown config only applies to other Markdown files:

astro.config.mjs
import { remarkMdxMermaid } from '@fumadocs/satteri/remark-mdx-mermaid';

fumadocs({
  // syntax highlighting
  rehypeCodeOptions: {
    themes: { light: 'catppuccin-latte', dark: 'catppuccin-mocha' },
  },
  // Sätteri plugins, from `@fumadocs/satteri` or your own
  mdastPlugins: [remarkMdxMermaid()],
});

The built-in plugins accept the same options as Fumadocs MDX (see Built-in Plugins). See Configuration for all options. Include and heading IDs are enabled by default.

For Math, add @import 'katex/dist/katex.css'; to src/styles/global.css.

Collections

The integration processes content/docs by default. To add another directory like a blog, add it to collections:

fumadocs({
  collections: {
    docs: { dir: 'content/docs' },
    blog: { dir: 'content/blog' }, 
  },
});

Each collection accepts integration options like its own components. For blog pages, see Blog.

Pages from other content sources join by passing a record of sources to loader(), like the pages generated by OpenAPI.

Other Content Sources

Other content sources work as your source. Pass their loader, for example with Local Markdown:

src/fumadocs.config.ts
import { defineConfig } from '@fumadocs/astro/config';
import { loader } from 'fumadocs-core/source';
import { localMd } from '@fumadocs/local-md';

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

export default defineConfig({
  source: loader(await docs.staticSource(), { baseUrl: '/docs' }), 
});

Local Markdown, Local HTML, Obsidian and Python need no setup. Notion and Sanity render with a plugin:

import { notionPlugin } from '@fumadocs/astro/plugins/notion';
import { notion } from './lib/notion'; // your NotionIntegration

// `components` and `highlightCode` for rendering
notionPlugin(notion, { components, highlightCode });

For a source with dynamicLoader(), pass await getSource().

Pages render with your MDX components, with relative links resolved. Add source-specific components like fumadocs-obsidian/ui so they render and hydrate.

How is this guide?

Last updated on