# Fumadocs (Framework Mode): Comparisons URL: /docs/comparisons Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/comparisons.mdx How is Fumadocs different from other existing frameworks? ## Nextra [#nextra] Fumadocs is highly inspired by Nextra. For example, the Routing Conventions. That is why `meta.json` also exists in Fumadocs. Nextra is more opinionated than Fumadocs, as a side effect, you have to configure things manually compared to simply editing a configuration file. Fumadocs works great if you want more control over everything, such as adding it to an existing codebase or implementing advanced routing. ### Feature Table [#feature-table] | Feature | Fumadocs | Nextra | | ------------------- | ------------ | ------------------------- | | Static Generation | Yes | Yes | | Cached | Yes | Yes | | Light/Dark Mode | Yes | Yes | | Syntax Highlighting | Yes | Yes | | Table of Contents | Yes | Yes | | Full-text Search | Yes | Yes | | i18n | Yes | Yes | | Last Git Edit Time | Yes | Yes | | Page Icons | Yes | Yes, via `_meta.js` files | | RSC | Yes | Yes | | Remote Source | Yes | Yes | | SEO | Via Metadata | Yes | | Built-in Components | Yes | Yes | | RTL Layout | Yes | Yes | ### Additional Features [#additional-features] Features supported via 3rd party libraries like [TypeDoc](https://typedoc.org) will not be listed here. | Feature | Fumadocs | Nextra | | -------------------------- | -------- | ------ | | OpenAPI Integration | Yes | No | | TypeScript Docs Generation | Yes | No | | TypeScript Twoslash | Yes | Yes | ## Mintlify [#mintlify] Mintlify is a documentation service, as compared to Fumadocs, it offers a free tier but isn't completely free and open source. Fumadocs is not as powerful as Mintlify, for example, the OpenAPI integration of Mintlify. As the creator of Fumadocs, I wouldn't recommend switching to Fumadocs from Mintlify if you're satisfied with the current way you build docs. However, I believe Fumadocs is a suitable tool for all React.js developers who want to have elegant docs. ## Docusaurus [#docusaurus] Docusaurus is a powerful framework based on React.js. It offers many cool features with plugins and custom themes. ### Lower Complexity [#lower-complexity] As Fumadocs is designed to integrate with React frameworks, you may need more knowledge of React.js to get started. In return, Fumadocs have better customizability. For a simple docs, Docusaurus might be a better choice if you don't need any framework-specific functionality. ### Plugins [#plugins] You can easily achieve many things with plugins, their ecosystem is indeed larger and maintained by many contributors. In comparison, the flexibility of Fumadocs allows you to implement them on your own, it may take longer to tune it to your satisfaction. # Fumadocs (Framework Mode): Deploying URL: /docs/deploying Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/deploying/index.mdx Deploy your Fumadocs app ## Overview [#overview] Your Fumadocs app is powered by the underlying React framework (e.g. Next.js), you will need to check the relevant docs of your React framework for deployment guides. - [Next.js](https://nextjs.org/docs/app/getting-started/deploying) - [React Router](https://reactrouter.com/start/framework/deploying) - [Tanstack Start](https://tanstack.com/start/latest/docs/framework/react/guide/hosting) - [Waku](https://waku.gg/#deployment) If you are looking to host the app as a SPA app (fully static, hostable on CDN), see [Static Build](/docs/deploying/static). There's some extra notes on specific combinations: ### Next.js + Cloudflare [#nextjs--cloudflare] Use https://opennext.js.org/cloudflare, Fumadocs doesn't work on Edge runtime. ### Next.js + Docker [#nextjs--docker] If you want to deploy your Fumadocs app using Docker with **Fumadocs MDX configured**, make sure to add the `source.config.ts` file and your `next.config.js` file to the `WORKDIR` in the Dockerfile. The following snippet is taken from the official [Next.js Dockerfile Example](https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile): ```zsh title="Dockerfile" # syntax=docker.io/docker/dockerfile:1 FROM node:18-alpine AS base # Install dependencies only when needed FROM base AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat WORKDIR /app # Install dependencies based on the preferred package manager [!code highlight] COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* source.config.ts next.config.* ./ RUN \ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm ci; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ else echo "Lockfile not found." && exit 1; \ fi # Rebuild the source code only when needed FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . # Next.js collects completely anonymous telemetry data about general usage. # Learn more here: https://nextjs.org/telemetry # Uncomment the following line in case you want to disable telemetry during the build. # ENV NEXT_TELEMETRY_DISABLED=1 RUN \ if [ -f yarn.lock ]; then yarn run build; \ elif [ -f package-lock.json ]; then npm run build; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ else echo "Lockfile not found." && exit 1; \ fi # Production image, copy all the files and run next FROM base AS runner WORKDIR /app ENV NODE_ENV=production # Uncomment the following line in case you want to disable telemetry during runtime. # ENV NEXT_TELEMETRY_DISABLED=1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT=3000 # server.js is created by next build from the standalone output # https://nextjs.org/docs/pages/api-reference/config/next-config-js/output ENV HOSTNAME="0.0.0.0" CMD ["node", "server.js"] ``` This ensures Fumadocs MDX can access your configuration file during builds. # Fumadocs (Framework Mode): Static Build URL: /docs/deploying/static Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/deploying/static.mdx Output static website with Fumadocs. ## Setup [#setup] By default, Fumadocs use a server-first approach which always requires a running server to serve. You can output a static build by configuring your React framework. ### Search [step] [#search] #### Built-in Search [#built-in-search] 1. [Configure Search Server](/docs/headless/search/orama#static-export). 2. [Configure Search UI/Client](/docs/search/orama#static). After the configurations, your app will statically store the search indexes, and search will be computed on browser instead. #### Cloud Solutions [#cloud-solutions] Since the search functionality is powered by remote servers, static export works without configuration. ### Deployment [step] [#deployment] #### Next.js [#nextjs] You can enable Next.js static export, it allows you to export the app as a static HTML site without a Node.js server. ```js title="next.config.mjs" /** * @type {import('next').NextConfig} */ const nextConfig = { output: 'export', // Optional: Change links `/me` -> `/me/` and emit `/me.html` -> `/me/index.html` // trailingSlash: true, // Optional: Prevent automatic `/me` -> `/me/`, instead preserve `href` // skipTrailingSlashRedirect: true, }; ``` See [Next.js docs](https://nextjs.org/docs/app/guides/static-exports) for limitations and details. #### React Router [#react-router] By configuring SPA mode, you can serve the `build/client` directory as a static website. On SPA mode, all your server-side loaders must be pre-rendered. See [React Router Docs](https://reactrouter.com/how-to/spa) for details. ```ts title="react-router.config.ts" import type { Config } from '@react-router/dev/config'; import { glob } from 'node:fs/promises'; import { createGetUrl, getSlugs } from 'fumadocs-core/source'; const getUrl = createGetUrl('/docs'); export default { // disable SSR [!code highlight] ssr: false, // [!code ++:9] async prerender({ getStaticPaths }) { const paths: string[] = [...getStaticPaths()]; for await (const entry of glob('**/*.mdx', { cwd: 'content/docs' })) { paths.push(getUrl(getSlugs(entry))); } return paths; }, } satisfies Config; ``` #### Tanstack Start [#tanstack-start] Configure SPA mode and pre-rendering, refer to [SPA Mode](https://tanstack.com/start/latest/docs/framework/react/guide/spa-mode) for deploying SPA apps with Tanstack Start. ```ts title="vite.config.ts" import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ // ...other plugins tanstackStart({ spa: { enabled: true, // Tanstack Router will automatically crawl your pages [!code ++:4] prerender: { enabled: true, }, // if you have any hidden paths that's not visible on UI, you can add them explicitly. [!code highlight:5] pages: [ { path: '/docs/test', }, ], }, }), ], }); ``` #### Waku [#waku] Waku can serve your site statically when all pages are configured with `static` render mode. See [Deployment](https://waku.gg/#deployment) for details. # Fumadocs (Framework Mode): Visual Editor URL: /docs/editor Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/editor.mdx Write Markdown and MDX visually, with your files as the source of truth. ## Introduction [#introduction] [Fumadocs Editor](https://editor.fumadocs.dev) is a visual editor for Fumadocs MDX. ### Start with Studio [#start-with-studio] Studio opens your local docs in a browser without adding an editor to your app. From your Fumadocs project, run: ```bash npx @fumadocs-editor/studio ``` Studio requires **Node.js 24 or later**. It looks for `content/docs`, or the parent directories when missing. To select a different content folder: ```bash npx @fumadocs-editor/studio --root ./docs ``` Edits autosave to disk, and changes from your code editor merge into the open document. The file list follows your `meta.json` ordering. Review the changes in Git and publish them through your site's usual deployment workflow. > Studio edits existing files. Create, rename, and delete files in your code editor or file manager instead. ### Use as a Library [#use-as-a-library] Use `@fumadocs-editor/ui` to build an editing interface inside your own app. See the docs for details: Install the editor, import its styles, and render your first editable document. Configure the content folder, custom components, and styles. Connect an embedded editor to files through your development server. ### Write with Components [#write-with-components] Built-in editing support includes Fumadocs components such as Callout, Cards, Steps, Accordions, and Files. Custom components can define their own editable regions with [component specs](https://editor.fumadocs.dev/components). The editor works on your Markdown and MDX files. Continue using [Fumadocs MDX](/docs/mdx) to load them and your React framework to render the published site. # Fumadocs (Framework Mode): Access Control URL: /docs/guides/access-control Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/guides/access-control.mdx Limit the access of content. ## Overview [#overview] Thanks to the flexible design, it is simple to implement access control with Fumadocs. ## Loader API [#loader-api] If you are using [`loader()`](/docs/headless/source-api) API, you can create the loaders on-demand, and filter files from content source to restrict accessible content. For example, this setup with Fumadocs MDX allows only MDX files with frontmatter `permission: public` to be shown. ```ts title="lib/source.ts" import { docs } from 'collections/server'; import { loader, update } from 'fumadocs-core/source'; const filteredSource = update(docs.toFumadocsSource()) .files((files) => files.filter((file) => { // keep meta files (e.g. `meta.json`) if (file.type === 'meta') return true; // access file data (type-safe) return file.data.permission === 'public'; }), ) .build(); export const source = loader(filteredSource, { baseUrl: '/docs', }); ``` Or with more complicated setup, you may turn `source` into a factory function. ```ts tab="lib/source.ts" import { docs } from 'collections/server'; import { loader, update } from 'fumadocs-core/source'; // uncached, it's better to cache it in-memory for real use case. export function createSource(permission: 'public' | 'admin') { const filteredSource = update(docs.toFumadocsSource()) .files((files) => files.filter((file) => { if (file.type === 'meta') return true; return file.data.permission === permission; }), ) .build(); return loader(filteredSource, { baseUrl: '/docs', }); } ``` ```ts tab="References" // [!code --] import { source } from '@/lib/source'; // [!code ++:4] import { createSource } from '@/lib/source'; const user = await getUser(); const source = createSource(user.permission); const page = source.getPage(params.slugs); ``` ### Advantages [#advantages] What is special with this approach is that **the access is limited at input level**, rather than at rendering phase or build-time. This guarantees the protected content is filtered entirely, and without compromise on flexibility. ## Custom Implementation [#custom-implementation] Without using Loader API, you can also implement your access control at framework-level (e.g. via Next.js routing & proxy). For example, to limit access to certain pages: ```tsx title="page.tsx" import { notFound } from 'next/navigation'; import { source } from '@/lib/source'; export default async function Page({ params }) { const { slugs } = await params; const user = await getUser(); const page = source.getPage(slugs); if (!page) notFound(); if (page.data.permission !== user.permission) notFound(); // render page... } ``` Note that it is up to you to manage the page tree (sidebar items), search and other details. This approach offers the best flexibility, but it doesn't rely on Fumadocs itself for access control, the setup may take longer and be more complicated. It is better to consult the docs of your React.js framework when implementing. # Fumadocs (Framework Mode): Customize UI URL: /docs/guides/customize-ui Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/guides/customize-ui.mdx A complete guide on how to customize Fumadocs UI. ## Introduction [#introduction] Fumadocs UI provides pre-built components for simplicity, serving as a middleground between "installing all UI code to your codebase" and an opinionated package. While Fumadocs UI itself has many designs around flexibility & backward compatibility, if used incorrectly, the measures may take no effect. > It is also recommended to feed this guide to your AI agents, if they may edit your UI. ### Pick the Right Component [step] [#pick-the-right-component] For layout UIs, there's different built-in layout components you can try: The layout of documentation A compact version of Docs Layout Shared layout for other pages An aggressively minimal layout for docs A docs layout with a floating, translucent sidebar and header A less compact version of Docs Layout ### Props [step] [#props] You can first check if the options of components can suffice your needs, for example: ```tsx {/* content */} ``` Or styling the container via props: ```tsx {/* content */} ``` Detailed documentation for each component is in the [Fumadocs UI](/docs/ui) section, you can take it as a reference. Most options support passing JSX elements to allow your own components. This approach should suffice most use cases, such as adding sections below table of contents or in sidebar. ### CSS [step] [#css] When the styling is more complicated, some components of Fumadocs UI attaches its own `id` & `data-` attributes for selecting. You can target them in your CSS selectors: ```css #nd-docs-layout #nd-subnav { background-color: purple; } [data-toc-popover] { background-color: purple; } ``` **Avoid invasive styles, or anything that presume a very specific DOM structure**, as it is difficult to guarantee backward compatibility, especially with newer UI features. ```css title="Bad Example" /* avoid targeting elements like this! */ [data-toc-popover] > div { background-color: purple; } ``` CSS approach can be risky as only the `id` & `data-` attributes can be versioned under its own life cycles, prefer other approaches if possible. ### Install via CLI [step] [#install-via-cli] For advanced needs beyond the exposed options, or if your custom CSS styles became overly invasive, Fumadocs CLI is a powerful tool. You can find similar prompts under the relevant component docs, indicating the component is installable, such as: For **layout components** (which often composes multiple components), Fumadocs CLI has a special `customize` command with granular choices. ```npm npx @fumadocs/cli@latest customize ``` Despite installing the entire layout component, you can choose to install a **slot** (a replaceable part of the layout component). Run the command above and you can view the full options. When installed, make sure to use the installed types over the types exported from Fumadocs UI. For example, if you have installed `layout/docs`, update your imports: ```tsx // [!code --] import type { DocsLayoutProps } from 'fumadocs-ui/layouts/docs'; // [!code ++] import type { DocsLayoutProps } from '@/components/layout/docs'; ``` This ensure your local components will be decoupled from Fumadocs UI. While Fumadocs CLI offers full control for the UI code, it is worth mentioning that: - You won't be able to receive UI updates for the installed the components. - You can re-install the component with Fumadocs CLI, but prior customisations will be erased. (You can choose which file to overwrite) - Fumadocs CLI will assume the latest Fumadocs Core + UI to be used. ## Other Questions [#other-questions] Feel free to reach out in [GitHub Discussions](https://github.com/fuma-nama/fumadocs/discussions) for questions, or if you have encountered unexpected breakages. # Fumadocs (Framework Mode): Export EPUB URL: /docs/guides/export-epub Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/guides/export-epub.mdx Export your documentation to EPUB format for e-readers ## Introduction [#introduction] The EPUB integration lets you export your Fumadocs documentation as an EPUB file, making it easy for readers to download and read your docs on e-readers, tablets, or any EPUB-compatible app. It converts your MDX content to HTML, resolves images, and produces a standards-compliant EPUB with a table of contents. ## Setup [#setup] Fumadocs CLI can set it up for you, it creates the export route protected by an `EXPORT_SECRET` and enables `includeProcessedMarkdown`: ```package-install npx @fumadocs/cli feature epub ``` Or manually: ### Installation [#installation] ```npm npm install fumadocs-epub ``` ### Enable Processed Markdown [#enable-processed-markdown] EPUB export requires `includeProcessedMarkdown` in your docs collection config: ```ts title="source.config.ts" import { defineDocs } from 'fumadocs-mdx/config'; export const docs = defineDocs({ docs: { postprocess: { // [!code ++] includeProcessedMarkdown: true, }, }, }); ``` ### Create Export Route [#create-export-route] `createEpubExportAPI` builds the route handler. It is guarded by `secret`: without a matching `Authorization: Bearer ` header the request is rejected, and the route answers `503` when the secret itself is unset. ```ts tab="Next.js" title="app/export/epub/route.ts" import { source } from '@/lib/source'; import { createEpubExportAPI } from 'fumadocs-epub'; export const revalidate = false; export const { GET } = createEpubExportAPI({ source, title: 'My Documentation', author: 'My Team', description: 'Documentation for my project', cover: '/cover.png', secret: process.env.EXPORT_SECRET, }); ``` ```ts tab="React Router" title="app/routes/export.epub.ts" import type { Route } from './+types/export.epub'; import { source } from '@/lib/source'; import { createEpubExportAPI } from 'fumadocs-epub'; const api = createEpubExportAPI({ source, title: 'My Documentation', secret: process.env.EXPORT_SECRET, }); export function loader({ request }: Route.LoaderArgs) { return api.GET(request); } ``` ```ts tab="React Router" title="app/routes.ts" import { route, type RouteConfig } from '@react-router/dev/routes'; export default [ // [!code ++] route('export/epub', 'routes/export.epub.ts'), ] satisfies RouteConfig; ``` ```ts tab="Tanstack Start" title="src/routes/export/epub.ts" import { createFileRoute } from '@tanstack/react-router'; import { source } from '@/lib/source'; import { createEpubExportAPI } from 'fumadocs-epub'; const api = createEpubExportAPI({ source, title: 'My Documentation', secret: process.env.EXPORT_SECRET, }); export const Route = createFileRoute('/export/epub')({ server: { handlers: { GET: ({ request }) => api.GET(request), }, }, }); ``` ```ts tab="Waku" title="pages/_api/export/epub.ts" import { source } from '@/lib/source'; import { createEpubExportAPI } from 'fumadocs-epub'; export const { GET } = createEpubExportAPI({ source, title: 'My Documentation', secret: process.env.EXPORT_SECRET, }); ``` For a public endpoint, or to build the file yourself (e.g. in a script), call `exportEpub()` directly and return the buffer. ### Using the CLI [#using-the-cli] You can also export EPUB via the Fumadocs CLI: ```bash fumadocs export epub --framework next # or: astro | tanstack-start | react-router | waku ``` This fetches from your running server (Next.js) or copies from the build output (other frameworks). Run a production build first for non-Next.js frameworks. ## Options [#options] Both `exportEpub` and `createEpubExportAPI` accept these options: | Option | Type | Description | | -------------- | -------------------- | ------------------------------------------------------------------------------- | | `title` | `string` | **Required.** Book title | | `author` | `string \| string[]` | Author name(s). Default: `'anonymous'` | | `description` | `string` | Book description | | `language` | `string` | Language code (e.g. `'en'`). Default: `'en'` | | `publisher` | `string` | Publisher name. Default: `'anonymous'` | | `isbn` | `string` | ISBN | | `cover` | `string` | Cover image. Supports `file://`, `http(s)://`, `/public/...`, or relative paths | | `outputPath` | `string` | If set, writes the EPUB to file in addition to returning the buffer | | `includePages` | `(page) => boolean` | Filter: include only pages where this returns `true` | | `excludePages` | `(page) => boolean` | Filter: exclude pages where this returns `true` | | `css` | `string` | Custom CSS for the EPUB. Uses `defaultEpubStyles` if omitted | | `publicDir` | `string` | Public directory for resolving `/public/...` image paths. Default: `./public` | `createEpubExportAPI` adds two more: `secret` (required, guards the route) and `filename` (the downloaded file name, default `docs.epub`). ### Custom CSS [#custom-css] You can override the default EPUB styles: ```ts import { exportEpub, defaultEpubStyles } from 'fumadocs-epub'; const buffer = await exportEpub({ source, title: 'My Docs', author: 'Me', css: `${defaultEpubStyles} /* Custom overrides */ body { font-size: 1.1em; } `, }); ``` ### Filtering Pages [#filtering-pages] Use `includePages` and `excludePages` to control which docs are exported: ```ts const buffer = await exportEpub({ source, title: 'My Docs', author: 'Me', // Only include docs under /docs/getting-started includePages: (page) => page.path.startsWith('getting-started'), // Exclude specific paths excludePages: (page) => page.path === 'changelog', }); ``` ### Image Resolution [#image-resolution] Images in your MDX are resolved automatically: - **Relative paths** (e.g. `./image.png`) — resolved relative to the page file - **Public paths** (e.g. `/og.png`) — resolved from the `public` directory - **Remote URLs** (e.g. `https://...`) — embedded as-is # Fumadocs (Framework Mode): Export PDF URL: /docs/guides/export-pdf Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/guides/export-pdf.mdx Export docs pages as PDF documents ## Overview [#overview] In general, we strongly recommend you to download the entire website (HTML files, etc) for offline browsing. In case if you want to export the page as a PDF, you can follow this guide. ## Setup [#setup] Use Puppeteer to export PDF files. ```ts title="scripts/export-pdf.ts" import puppeteer from 'puppeteer'; import fs from 'node:fs/promises'; import path from 'node:path'; const browser = await puppeteer.launch(); const outDir = 'pdfs'; // update this [!code highlight] const urls = ['/docs/ui', '/docs/ui/customizations']; async function exportPdf(pathname: string) { const page = await browser.newPage(); await page.goto('http://localhost:3000' + pathname, { waitUntil: 'networkidle2', }); await page.pdf({ path: path.join(outDir, pathname.slice(1).replaceAll('/', '-') + '.pdf'), width: 950, printBackground: true, }); console.log(`PDF generated successfully for ${pathname}`); await page.close(); } await fs.mkdir(outDir, { recursive: true }); await Promise.all(urls.map(exportPdf)); await browser.close(); ``` Add the following to your Tailwind CSS file to disable navigation elements when printing: ```css @media print { #nd-docs-layout { --fd-sidebar-width: 0px !important; } #nd-sidebar { display: none; } } ``` You can now run the script: ```bash bun ./scripts/export-pdf.ts ``` ### Invisible Contents [#invisible-contents] For invisible contents in accordions/tabs, you may need to temporarily override the MDX components. For example: ```tsx title="components/mdx.tsx" import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; // you may use environment variable here [!code highlight] const isPrinting = true; function PrintingAccordion(props: React.ComponentProps) { return (

{props.title}

{props.children}
); } function PrintingAccordions(props: React.ComponentProps) { return
{props.children}
; } export function getMDXComponents(components?: MDXComponents) { return { ...defaultMdxComponents, // updated accordions: Accordion: isPrinting ? PrintingAccordion : Accordion, Accordions: isPrinting ? PrintingAccordions : Accordions, ...components, } satisfies MDXComponents; } ``` # Fumadocs (Framework Mode): Guides URL: /docs/guides Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/guides/index.mdx Useful guides for extending Fumadocs. # Fumadocs (Framework Mode): RSS Feed URL: /docs/guides/rss Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/guides/rss.mdx Generate a RSS feed for your docs/blog. ## Setup [#setup] You can create the feed object: ```ts title="lib/rss.ts" import { Feed } from 'feed'; import { source } from '@/lib/source'; const baseUrl = 'https://fumadocs.dev'; export function getRSS() { const feed = new Feed({ title: 'Fumadocs Blog', id: `${baseUrl}/blog`, link: `${baseUrl}/blog`, language: 'en', image: `${baseUrl}/banner.png`, favicon: `${baseUrl}/icon.png`, copyright: 'All rights reserved 2025, Fuma Nama', }); for (const page of source.getPages()) { feed.addItem({ id: page.url, title: page.data.title, description: page.data.description, link: `${baseUrl}${page.url}`, date: new Date(page.data.lastModified), author: [ { name: 'Fuma', }, ], }); } return feed.rss2(); } ``` And expose it using a server loader/route handler like: ```ts tab="Next.js" title="app/rss.xml/route.ts" import { getRSS } from '@/lib/rss'; export const revalidate = false; export function GET() { return new Response(getRSS()); } ``` ```ts tab="React Router" title="app/routes/rss.ts" import { getRSS } from '@/lib/rss'; export async function loader() { return new Response(getRSS()); } ``` ```ts tab="React Router" title="app/routes.ts" import { route, type RouteConfig } from '@react-router/dev/routes'; export default [ // [!code ++] route('rss.xml', 'routes/rss.ts'), ] satisfies RouteConfig; ``` ```ts tab="Tanstack Start" title="src/routes/rss[.]xml.ts" import { createFileRoute } from '@tanstack/react-router'; import { getRSS } from '@/lib/rss'; export const Route = createFileRoute('/rss.xml')({ server: { handlers: { GET: async () => new Response(getRSS()), }, }, }); ``` ```ts tab="Waku" title="pages/_api/rss.xml.ts" import { getRSS } from '@/lib/rss'; export function GET() { return new Response(getRSS()); } export async function getConfig() { return { render: 'static' as const, } as const; } ``` ### Next.js Metadata [#nextjs-metadata] You can add an alternates object to the metadata object with your feed’s title and URL. ```ts title="app/layout.tsx" import type { Metadata } from 'next'; export const metadata: Metadata = { alternates: { types: { 'application/rss+xml': [ { title: 'Fumadocs Blog', url: 'https://fumadocs.dev/blog/index.xml', }, ], }, }, }; ``` # Fumadocs (Framework Mode): Quick Start URL: /docs Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/index.mdx Getting Started with Fumadocs ## Introduction [#introduction] Fumadocs (Foo-ma docs) is a **documentation framework**, designed to be fast, flexible, and composes seamlessly into your React framework. It consists of multiple layers: } title='Fumadocs Core' href='/docs/headless'> Handles most of the logic, including document search, content source adapters, and Markdown extensions. } title='Fumadocs UI' href='/docs/ui'> The default theme of Fumadocs offers a beautiful look for documentation sites and interactive components. } title='Content Source'> The source of your content, can be a CMS or local data layers like [Fumadocs MDX](/docs/mdx) (the official content source). } title='Fumadocs CLI' href='/docs/cli'> A command line tool to install UI components and automate things, useful for customizing layouts. Read our in-depth [What is Fumadocs](/docs/what-is-fumadocs) introduction. ### Terminology [#terminology] **Markdown/MDX:** Markdown is a markup language for creating formatted text. Fumadocs natively supports Markdown and MDX (superset of Markdown). **[Bun](https://bun.sh):** A JavaScript runtime, it will be used in our guides for running scripts. If you prefer Node.js, try [unrun](https://gugustinette.github.io/unrun/guide/getting-started.html#cli) instead. Some basic knowledge of React.js would be useful for further customizations. [Fumapress](https://press.fumadocs.dev) is a site generator that manages more features on top of Fumadocs. - Automatic routing, `llms.txt`, MCP and more basic features. - Based on Vite ([Waku](https://waku.gg)). It enforces a React framework of its own, and offers a better DX in return. ## Automatic Installation [#automatic-installation] A minimum version of Node.js 22 required. ```npm npm create fumadocs-app ``` It will ask you the built-in template to use: - **Framework**: Next.js, Astro (with React), Waku, React Router, Tanstack Start. - **Content source**: Fumadocs MDX. A new fumadocs app should be initialized, with following features configured by default: - [LLM integration](/docs/integrations/llms). - [Dynamic metadata image](/docs/integrations/og). You can follow the [Manual Installation](/docs/manual-installation) guide to get started. ### Enjoy! [#enjoy] Create your first MDX file in the docs folder. ```mdx title="content/docs/index.mdx" --- title: Hello World --- ## Yo what's up ``` Run the app in development mode and see http://localhost:3000/docs. ```npm npm run dev ``` ## FAQ [#faq] Some common questions you may encounter. Make sure to upgrade Fumadocs when you've encountered any problems or trying out new features: ```bash title="pnpm" pnpm update -i -r --latest ``` Routing is handled by your React framework, you need to change the routing structure first. For example, in Next.js, rename the route (`/docs/*` -> `/info/*`): Or rename from `/docs/*` to `/*` using a route group: Finally, update the base URL of pages in `source.ts`: ```ts title="lib/source.ts" import { loader } from 'fumadocs-core/source'; export const source = loader({ baseUrl: '/info', // to the new value [!code highlight] }); ``` We recommend using [Layout Tabs](/docs/ui/layouts/docs#layout-tabs). ### For Vite [#for-vite] There's some weird pre-bundling problems with Vite: [#3910](https://github.com/vitejs/vite/issues/3910). Make sure to exclude Fumadocs from pre-bundling and add it to `noExternal`: ```ts title="vite.config.ts" import { defineConfig } from 'vite'; export default defineConfig({ resolve: { // add other Fumadocs deps as needed noExternal: ['fumadocs-core', 'fumadocs-ui', 'fumadocs-openapi', '@fumadocs/base-ui'], }, }); ``` ### For Next.js [#for-nextjs] If you have similar Turbopack errors like: ``` JSON file not found: FileSystemPath { ... path: "lib/locales/sources/file.json.json" } ``` It is usually because `require(esm)` is not supported ([#3312](https://github.com/fuma-nama/fumadocs/issues/3312)), please upgrade to Node.js 22+. On Node.js 23.1, if you encountered this error in production build: ``` Could not find a production build in the '.next' directory. ``` It is a known problem, see [#1021](https://github.com/fuma-nama/fumadocs/issues/1021), you can upgrade your Node.js version to 23.6.0+. Next.js turns dynamic route into static routes when `generateStaticParams` is configured. Hence, it is as fast as static pages. You can [enable Static Exports](/docs/deploying/static) on Next.js to get a static build output. Same as managing layouts in Next.js App Router, remove the original MDX file from content directory (`/content/docs`). This ensures duplicated pages will not cause errors. Now, You can add the page to another route group, which isn't a descendant of docs layout. For example, to replace `/docs/test`: For `/docs`, you need to change the catch-all route to be non-optional: ## Learn More [#learn-more] New to here? Don't worry, we are welcome for your questions. If you find anything confusing, please give your feedback on [Github Discussion](https://github.com/fuma-nama/fumadocs/discussions)! ### Writing Content [#writing-content] For authoring docs, make sure to read: Fumadocs has some additional features for authoring content. Learn how to customize navigation structure. Learn how to organise content. ### Special Needs [#special-needs] # Fumadocs (Framework Mode): createAsyncAPIPage() URL: /docs/integrations/asyncapi/api-page Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/asyncapi/api-page.mdx The component for rendering AsyncAPI docs content ## Overview [#overview] Fumadocs AsyncAPI uses a `` component to render page contents, it should be a client component. ```tsx title="components/api-page.tsx" 'use client'; import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui'; export const AsyncAPIPage = createAsyncAPIPage({ // config }); ``` ### Custom Layout [#custom-layout] You can customize how operations and message examples are laid out. ```tsx title="components/api-page.tsx" 'use client'; import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui'; export const AsyncAPIPage = createAsyncAPIPage({ content: { renderOperationLayout(slots) { return (
{slots.header} {slots.description} {slots.channel} {slots.parameters} {slots.messages}
); }, }, }); ``` ### Schema UI [#schema-ui] Customize how JSON schemas are rendered, or disable schema examples. ```tsx title="components/api-page.tsx" 'use client'; import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui'; export const AsyncAPIPage = createAsyncAPIPage({ schemaUI: { showExample: true, render(options, ctx) { // fully custom schema renderer return ctx.SchemaUI(options); }, }, }); ``` ### Internationalization [#internationalization] Assuming you have configured [Internationalization](/docs/internationalization) at UI level: ```tsx title="layout.shared.tsx" import { defineI18n } from 'fumadocs-core/i18n'; import { uiTranslations } from 'fumadocs-ui/i18n'; import { asyncapiTranslations } from '@fumadocs/asyncapi/i18n'; const i18n = defineI18n({ languages: ['en', 'cn'], defaultLanguage: 'en', }); export const translations = i18n .translations() .extend(uiTranslations()) // [!code ++] .extend(asyncapiTranslations()) .add({ cn: { displayName: 'Chinese' // [!code ++] 'Messages(operation page)': '消息', }, }) ``` See [Translations](/docs/ui/translations) for more details. ### Syntax Highlighting [#syntax-highlighting] AsyncAPI pages use Shiki for code blocks. You can customize the highlighter and themes. ```tsx title="components/api-page.tsx" 'use client'; import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui'; import { defaultShikiFactory } from 'fumadocs-core/highlight/shiki/full'; export const AsyncAPIPage = createAsyncAPIPage({ shiki: defaultShikiFactory, shikiOptions: { themes: { light: 'github-light', dark: 'github-dark', }, }, }); ``` ### Custom Components [#custom-components] Override the default heading and code block components. ```tsx title="components/api-page.tsx" 'use client'; import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui'; export const AsyncAPIPage = createAsyncAPIPage({ components: { Heading({ id, depth, ...props }) { const Tag = `h${depth}` as const; return ; }, CodeBlock({ lang, code }) { return
{code}
; }, }, }); ``` ## Customise UI [#customise-ui] For customisations beyond the available options, you can install the UI into your codebase with [Fumadocs CLI](/docs/cli). ### Full UI [#full-ui] The entire UI of API pages, built on [the headless layer](/docs/integrations/asyncapi/headless). ```npm npx @fumadocs/cli add asyncapi/page ``` It installs `` itself, your `components/api-page.tsx` is no longer needed. ```tsx title="mdx-components.tsx" import { AsyncAPIPage } from '@/components/api-page'; // [!code --] import { AsyncAPIPage } from '@/components/asyncapi/page'; // [!code ++] ``` ### Operation [#operation] The UI of a single operation, including its channel, messages and bindings. ```npm npx @fumadocs/cli add asyncapi/operation ``` ```tsx title="components/api-page.tsx" 'use client'; import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui'; import { Operation } from '@/components/asyncapi/operation'; export const AsyncAPIPage = createAsyncAPIPage({ components: { Operation, }, }); ``` ### Schema UI [#schema-ui-1] The UI of JSON schemas, shared with the other API integrations. Its state comes from [``](/docs/integrations/openapi/headless#schema-ui). ```npm npx @fumadocs/cli add api-docs/schema ``` ```tsx title="components/api-page.tsx" 'use client'; import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui'; import { Schema } from '@/components/api/schema'; export const AsyncAPIPage = createAsyncAPIPage({ components: { SchemaUI: Schema, }, }); ``` # Fumadocs (Framework Mode): generateFiles() URL: /docs/integrations/asyncapi/generate-files Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/asyncapi/generate-files.mdx Generate pages from AsyncAPI schema. The `generateFiles()` function generates MDX files for API documentation. Note that the page content is still rendered by the `` component, the docs generator only arranges the operations to render for each page. ## Usage [#usage] It takes an AsyncAPI instance. ```ts import { generateFiles } from '@fumadocs/asyncapi'; import { asyncapi } from '@/lib/asyncapi'; void generateFiles({ input: asyncapi, // The output directory. output: '/content/docs', }); ``` ### `per` [#per] Customize the content of pages, default to `operation`. Operation refers to an AsyncAPI operation with a specific action like `receiveLightMeasurement:receive`. | mode | content | file path | | --------- | ------------------------------ | ------------------- | | tag | operations with same tag | `{tag_name}.mdx` | | file | operations in same schema file | `{file_name}.mdx` | | operation | each operation | `{operationId}.mdx` | | custom | see below | N/A | ```ts import { generateFiles } from '@fumadocs/asyncapi'; void generateFiles({ per: 'tag', }); ``` When set to `custom`, you can pass a function to fully customize the generation process: ```ts title="Example" import { generateFiles, OperationOutput } from '@fumadocs/asyncapi'; void generateFiles({ per: 'custom', toPages(builder) { const items = builder.extract(); for (const op of items.operations) { const { operation, channel, displayName } = builder.fromExtractedOperation(op)!; const entry: OperationOutput = { type: 'operation', schemaId: builder.id, item: op, path: '...', info: { title: displayName, description: operation.description ?? channel.description, }, }; builder.create(entry); } }, }); ``` When set to `tag`, adding `x-displayName` to the tag definition can control the title of each page. ```yaml title="asyncapi.yaml" tags: - name: lighting description: Operations related to streetlights. x-displayName: Lighting ``` ### `groupBy` [#groupby] In `operation` mode, you can group output files with folders. | value | output | | -------------- | ------------------------------------- | | tag | `{tag}/{operationId}.mdx` | | channel | `{channel_address}/{operationId}.mdx` | | none (default) | `{operationId}.mdx` | ```ts import { generateFiles } from '@fumadocs/asyncapi'; void generateFiles({ per: 'operation', groupBy: 'tag', }); ``` ### `index` [#index] Generate index files with cards linking to generated pages. ```ts import { generateFiles } from '@fumadocs/asyncapi'; void generateFiles({ index: { // for generating `href` url: { baseUrl: '/docs', contentDir: './content/docs', }, items: [ { // output path path: 'index.mdx', // optional: restrict the input files (identical to the `input` field in server) only: ['streetlights.yaml'], // optional: set frontmatter description: 'All available pages', }, ], }, }); ``` ### `meta` [#meta] Generate [`meta.json`](/docs/page-conventions#meta) files automatically. You can always define the `meta.json` files manually for maximal flexibility. ```ts import { generateFiles } from '@fumadocs/asyncapi'; void generateFiles({ meta: true, // or customize how folders are represented: meta: { folderStyle: 'separator', }, }); ``` ### `imports` [#imports] Add custom imports to generated MDX files. This is useful for including components, utilities, or other dependencies in your generated documentation. ```ts import { generateFiles } from '@fumadocs/asyncapi'; void generateFiles({ imports: [ { names: ['API_BASE_URL'], from: '@/constants', }, ], }); ``` This will add the following imports to all generated MDX files: ```ts import { API_BASE_URL } from '@/constants'; ``` ### `name` [#name] A function that controls the output file name of MDX pages. ```ts import { generateFiles } from '@fumadocs/asyncapi'; void generateFiles({ name(output) { if (output.type === 'operation') { const { operation } = this.fromExtractedOperation(output.item)!; console.log(operation); return `my-dir/${output.item.id}`; } return 'my-dir/filename'; }, }); ``` You can also use the `algorithm` option: ```ts import { generateFiles } from '@fumadocs/asyncapi'; void generateFiles({ // v2 (default): use operation id // v1: use `{operationId}/{action}` name: { algorithm: 'v1' }, }); ``` ### `frontmatter` [#frontmatter] Customize the frontmatter of MDX files. By default, it includes: | property | description | | ------------- | ---------------------------------- | | `title` | Page title | | `description` | Page description | | `full` | Always true, added for Fumadocs UI | | `_asyncapi` | Internal Properties | ```ts import { generateFiles } from '@fumadocs/asyncapi'; void generateFiles({ frontmatter: (title, description) => ({ myProperty: 'hello', }), }); ``` ### `addGeneratedComment` [#addgeneratedcomment] Add a comment to the top of generated files indicating they are auto-generated. ```ts import { generateFiles } from '@fumadocs/asyncapi'; void generateFiles({ // Add default comment addGeneratedComment: true, // Or provide a custom comment addGeneratedComment: 'Custom auto-generated comment', // Or disable comments addGeneratedComment: false, }); ``` # Fumadocs (Framework Mode): Headless URL: /docs/integrations/asyncapi/headless Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/asyncapi/headless.mdx The state and logic of AsyncAPI pages, without UI. ## Overview [#overview] Everything outside `@fumadocs/asyncapi/ui` is headless. | Path | Module | | ------------------------------ | --------------------------------------------------------- | | `@fumadocs/asyncapi` | the page: the document, its servers and your components | | `@fumadocs/asyncapi/operation` | an operation: its channel, parameters, messages and reply | ## Page [#page] `createAsyncAPIRenderer()` takes your components and returns ``, which accepts the props of generated pages. ```tsx title="components/api-page.tsx" 'use client'; import { type CodeBlockProps, createAsyncAPIRenderer } from '@fumadocs/asyncapi'; import { Operation } from '@/components/my-operation'; import { SchemaUI } from '@/components/my-schema'; export const AsyncAPIPage = createAsyncAPIRenderer({ components: { Operation, SchemaUI, CodeBlock({ lang, code }: CodeBlockProps) { return (
          {code}
        
); }, }, }); ``` | Component | Renders | | ----------- | ------------------------------------------------------------ | | `Operation` | each operation of the page, see [Operation](#operation) | | `SchemaUI` | the JSON schemas of parameters, headers and message payloads | | `Layout` | optional, wraps the rendered operations | ### Hooks [#hooks] Components under the page read its state. | Hook | Returns | | -------------------- | -------------------------------------------------- | | `useAsyncAPI()` | the dereferenced document (`doc`) and page options | | `useComponents()` | the components passed to the page | | `useServer()` | the servers, the selected one and its variables | | `useRenderContext()` | the render options of the page, like `content` | To render operations yourself, mount `` in place of `createAsyncAPIRenderer()`. ## Operation [#operation] `` derives the details of an operation once: traits are applied, and its channel, parameters, messages and reply are resolved. ```tsx title="components/my-operation.tsx" 'use client'; import { OperationProvider, type PageOperationProps, useOperation, useOperationSecurity, } from '@fumadocs/asyncapi/operation'; export function Operation(props: PageOperationProps) { return ( ); } function Content() { const { title, channel, parameters, messages } = useOperation(); const schemes = useOperationSecurity(); return ( <>

{title}

{channel.address}
    {parameters.map((param) => (
  • {param.name}
  • ))}
{messages.map((message) => (

{message.name}

{message.examples.map((example) => (
{JSON.stringify(example.payload, null, 2)}
))}
))}

{schemes.map((scheme) => scheme.type).join(', ')}

); } ``` | Hook | Returns | | ------------------------ | ------------------------------------------------------------------------ | | `useOperation()` | the operation with its details resolved | | `useOperationSecurity()` | security schemes of the operation, falling back to the selected server's | ### OperationInfo | Prop | Type | Description | | -------------- | -------- | ----------------------------- | | `id` | `string` | | | `action` | `union` | | | `operation` | `object` | resolved, with traits applied | | `channel` | `object` | | | `title` | `string` | | | `description?` | `string` | | | `parameters` | `array` | | | `messages` | `array` | | | `reply?` | `object` | | Examples of a message come from the document, or are generated from its schemas. ### OperationMessage | Prop | Type | Description | | ---------- | -------- | ----------------------------------- | | `id` | `string` | id for anchors and accordion values | | `name` | `string` | display name | | `message` | `object` | resolved, with traits applied | | `headers?` | `union` | | | `payload?` | `union` | | | `examples` | `array` | example messages | # Fumadocs (Framework Mode): AsyncAPI URL: /docs/integrations/asyncapi Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/asyncapi/index.mdx Generating docs for AsyncAPI schema. ## Setup [#setup] Install the required packages. ```npm npm i @fumadocs/asyncapi shiki ``` ### Generate Styles [#generate-styles] Add the following line: ```css title="Tailwind CSS" @import 'tailwindcss'; @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; /* [!code ++] */ @import '@fumadocs/asyncapi/css/preset.css'; ``` ### Configure Plugin [#configure-plugin] Create the AsyncAPI server instance & `` component. ```ts tab="lib/asyncapi.ts" import { createAsyncAPI } from '@fumadocs/asyncapi/server'; // note: this is a server-side API export const asyncapi = createAsyncAPI({ // the AsyncAPI schema, you can also give it an external URL. input: ['./asyncapi.yaml'], }); ``` ```tsx tab="components/api-page.tsx" 'use client'; import { createAsyncAPIPage } from '@fumadocs/asyncapi/ui'; export const AsyncAPIPage = createAsyncAPIPage(); ``` ```ts tab="lib/source.ts" import { asyncapiPlugin } from '@fumadocs/asyncapi/server'; import { loader } from 'fumadocs-core/source'; export const source = loader({ // [!code ++] optional: adds a send/receive badge to each page item in page tree plugins: [asyncapiPlugin()], }); ``` See [`createAsyncAPI()`](/docs/integrations/asyncapi/server) & [`createAsyncAPIPage()`](/docs/integrations/asyncapi/api-page) for available options. ### Generate Pages [#generate-pages] You can generate MDX files directly from your AsyncAPI schema. Create a script: ```js title="scripts/generate-docs.ts" import { generateFiles } from '@fumadocs/asyncapi'; import { asyncapi } from '@/lib/asyncapi'; void generateFiles({ input: asyncapi, output: './content/docs', // we recommend to enable it // make sure your operation description doesn't break MDX syntax. includeDescription: true, }); ``` Generate docs with the script: ```bash bun ./scripts/generate-docs.ts ``` Add the `AsyncAPIPage` component to your MDX components. Generated files use the `` tag. ```tsx tab="Server Components" title="app/docs/[[...slug]]/page.tsx" import { source } from '@/lib/source'; import { asyncapi } from '@/lib/asyncapi'; import { AsyncAPIPage } from '@/components/api-page'; import { getMDXComponents } from '@/components/mdx'; // e.g. in your page renderer export default function Page({ slug }) { const page = source.getPage(slug); const MdxContent = page.data.body; return ( ( ), })} /> ); } ``` ```tsx tab="Without Server Components" title="docs/[...slug]" import { source } from '@/lib/source'; import { asyncapi } from '@/lib/asyncapi'; import { AsyncAPIPage } from '@/components/api-page'; import { getMDXComponents } from '@/components/mdx'; // e.g. where you return server payload to client export function loader({ slug }) { const page = source.getPage(slug); return { // [!code ++] asyncapiData: await asyncapi.preloadAsyncAPIPage(page), // ... }; } // e.g. in your page renderer export default function Page() { // consume loader data const { asyncapiData } = useLoaderData(); return ( , })} /> ); } ``` You can also use it without generating real files by integrating into [Loader API](/docs/headless/source-api/source). ```ts title="lib/source.ts" import { loader } from 'fumadocs-core/source'; import { docs } from 'collections/server'; import { asyncapi } from '@/lib/asyncapi'; export const source = loader( // [!code ++:6] { docs: docs.toFumadocsSource(), asyncapi: await asyncapi.staticSource({ baseDir: 'asyncapi', }), }, { baseUrl: '/docs', plugins: [asyncapi.loaderPlugin()], // ... }, ); ``` `staticSource()` is a server-side API that generates pages directly to your `loader()`, hence it allows dynamic content generation, such as re-generating page tree as schema changes. **It will change the type of your pages**, make sure to update all references to your `source`. For example, where you return text for LLM: ```ts import { llms } from 'fumadocs-core/source'; import { source } from '@/lib/source'; export const docsLlms = llms(source, { renderPage: (page) => { // [!code ++:4] if (page.type === 'asyncapi') { // e.g. return the stringified AsyncAPI schema return JSON.stringify(page.data.getSchema().bundled, null, 2); } // your original flow below... }, }); ``` And update your page renderer: ```tsx title="docs/[[...slug]]/page.tsx" import { AsyncAPIPage } from '@/components/api-page'; export default function Page({ slug }) { const page = source.getPage(slug); // for AsyncAPI pages if (page.type === 'asyncapi') { return (

{page.data.title}

); } // your original flow below... } ```
Pass a client payload from server, then render the page using the `` component you created above. For example, in Tanstack Start: ```tsx title="routes/docs/$.tsx" import { createFileRoute, notFound } from '@tanstack/react-router'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { createServerFn } from '@tanstack/react-start'; import { docs, source } from '@/lib/source'; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page'; import { useFumadocsLoader } from 'fumadocs-core/source/client'; import { type ReactNode, Suspense } from 'react'; import { AsyncAPIPage } from '@/components/api-page'; export const Route = createFileRoute('/docs/$')({ component: Page, loader: async ({ params }) => { const slugs = params._splat?.split('/') ?? []; const data = await serverLoader({ data: slugs }); // Fumadocs MDX: only preload content for normal pages [!code highlight:3] if (data.type === 'docs') { await docs.getPage(data.path)?.preload(); } return data; }, }); const serverLoader = createServerFn({ method: 'GET', }) .validator((slugs: string[]) => slugs) .handler(async ({ data: slugs }) => { const page = source.getPage(slugs); if (!page) throw notFound(); const pageTree = await source.serializePageTree(source.getPageTree()); // different result for AsyncAPI pages [!code ++:9] if (page.type === 'asyncapi') { return { type: 'asyncapi', title: page.data.title, description: page.data.description, pageTree, props: page.data.getAsyncAPIPageProps(), }; } return { type: 'docs', path: page.path, pageTree, // ... }; }); // renders a page of your `docs` collection function Content({ path }: { path: string }) { // ... } function Page() { const page = useFumadocsLoader(Route.useLoaderData()); let content: ReactNode; // render AsyncAPI page content [!code ++:11] if (page.type === 'asyncapi') { content = ( {page.title} {page.description} {/* pass the payload data */} ); } else { content = ; } return ( {content} ); } ``` You can see the full [AsyncAPI example](https://github.com/fuma-nama/fumadocs/blob/dev/examples/asyncapi).
Run a type check to verify before continuing, e.g. ```npm npm run types:check ```
## Features [#features] The official AsyncAPI integration supports: - Basic operation information (send/receive) - Channel, message, and reply documentation - Message payload & header schemas - Security schemes - Message examples (from schema examples or generated samples) - Bindings and traits ### Demo [#demo] See the [AsyncAPI example](/docs/asyncapi). # Fumadocs (Framework Mode): createAsyncAPI() URL: /docs/integrations/asyncapi/server Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/asyncapi/server.mdx The AsyncAPI server instance. ## AsyncAPI Server [#asyncapi-server] The main config for Fumadocs AsyncAPI. > It should not be referenced in browser environments. ### `input` [#input] The AsyncAPI schemas to read from. - File Paths - External URLs - Functions (see below) ```ts tab="Basic" import { createAsyncAPI } from '@fumadocs/asyncapi/server'; export const asyncapi = createAsyncAPI({ input: ['./streetlights.yaml'], }); ``` ```ts tab="Functions" import { createAsyncAPI } from '@fumadocs/asyncapi/server'; export const asyncapi = createAsyncAPI({ input: { // [id]: downloaded AsyncAPI Schema galaxy: async () => { const res = await fetch('https://example.com/asyncapi.yaml'); return res.text(); }, }, }); ``` ### `disableCache` [#disablecache] Disable caching of loaded schemas. Useful during development when schemas change frequently. ```ts import { createAsyncAPI } from '@fumadocs/asyncapi/server'; export const asyncapi = createAsyncAPI({ input: ['./asyncapi.yaml'], disableCache: true, }); ``` # Fumadocs (Framework Mode): Build Your Own URL: /docs/integrations/content/custom Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/custom.mdx Build your own content source for Fumadocs. ## Introduction [#introduction] For a custom content source implementation, there's two ways: ### Using Loader API [#using-loader-api] Loader API has a file-system-like interface for integrating different content sources. See [Loader API `source`](/docs/headless/source-api/source) for details. ### Using Low-Level API [#using-low-level-api] For more robust control, you can use lower level APIs directly. #### Page Tree [step] [#page-tree] You can either hardcode the page tree, or write some code to generate one. See [Definitions of Page Tree](/docs/headless/page-tree). Pass your page tree to `DocsLayout` (usually in a `layout.tsx`): ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` The page tree is like a smarter "sidebar items", they will be referenced everywhere in the UI for navigation elements, such as the page footer. #### Page Content [step] [#page-content] For docs page, it's the same logic: - Define path params (`slugs`). - Fetch page content from path params. - Render the content (inside the `DocsPage` component). You may need table of contents, which can be generated on your own, or using the [`getTableOfContents`](/docs/headless/utils/get-toc) utility (Markdown/MDX only). ```tsx import { DocsPage, DocsBody } from 'fumadocs-ui/layouts/docs/page'; import { getPage } from './my-content-source'; import { notFound } from 'next/navigation'; export default function Page({ params }: { params: { slug?: string[] } }) { const page = getPage(params.slug); if (!page) notFound(); return ( {page.render()} ); } ``` #### Pre-rendering [step] [#pre-rendering] The mechanism for pre-rendering differs depending on your React.js framework. It's important to pre-render pages (especially the frequently visited ones) to improve initial load time. Define the [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) function to populate dynamic & catch-all routes. When pre-rendering pages from a remote source, make sure to optimize repeated reads during the build phase. For example, if you're fetching content via GitHub API, it is better to clone the repository content locally for pre-rendering. ```text tab="Before" /docs/* -> GET github.com /docs/introduction -> GET github.com /docs/my-page -> GET github.com Error: API Ratelimit ``` ```text tab="After" start -> git clone /docs/* -> ls /docs/introduction -> read file /docs/my-page -> read file ``` #### Document Search [step] [#document-search] This can be difficult considering your content may not be necessarily Markdown/MDX. For Markdown and MDX, the built-in [Search API](/docs/headless/search/orama) + search index usage is adequate for most use cases. Otherwise, you will have to bring your own implementation. We recommend 3rd party solutions like Orama or Algolia Search. They are more flexible than the built-in Search API, and is easier to integrate with remote sources. Fumadocs offers a variant of [Search Integrations](/docs/headless/search). # Fumadocs (Framework Mode): Content Source URL: /docs/integrations/content Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/index.mdx Integrate different content sources with Fumadocs. ## Introduction [#introduction] **Fumadocs is very flexible.** You can integrate with any content source, even without an official adapter. ### Examples [#examples] You can see examples to use Fumadocs with a CMS, which allows a nice experience on publishing content, and real-time update without re-building the app. - [BaseHub](https://github.com/fuma-nama/fumadocs-basehub) - [Sanity](https://github.com/fuma-nama/fumadocs-sanity) - [Community: Payload CMS by `MFarabi619`](https://github.com/MFarabi619/fumadocs-payloadcms) - [Community: Payload CMS by `bapspatil`](https://github.com/bapspatil/fumadocs-payload-template) Not enough? [Build your own content source](/docs/integrations/content/custom). # Fumadocs (Framework Mode): Local HTML URL: /docs/integrations/content/local-html Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/local-html.mdx Content source for local HTML files. ## Introduction [#introduction] `@fumadocs/local-html` is a content source for local HTML files, it is bundleless (works fully at runtime) by design. It is made for pages you receive as HTML instead of author yourself, like exported decks, reports, and the output of other tools. Each file is adapted to your docs theme rather than rendered as it came. As compared to [Local Markdown](/docs/integrations/content/local-md), HTML is treated as data to adapt, not as a document format to write in. ### Limitations [#limitations] - Scripts are stripped, the interactive behaviour of the original page is not preserved. - No MDX components in content, but you can map HTML tags to components at render phase. - Processing is a transform, not sanitization. Nothing that executes survives it, but the remaining markup is rendered as-is, and the resources it references (images above all) are fetched by everyone who opens the page. Keep the content directory to files you trust, like your Markdown content. ## Setup [#setup] Install the package: ```package-install @fumadocs/local-html ``` Create a `localHtml` instance and connect it to Fumadocs: ```ts title="lib/source.ts" import { dynamicLoader } from 'fumadocs-core/source'; import { localHtml } from '@fumadocs/local-html'; const pages = localHtml({ dir: 'content/pages', // options }); const pagesLoader = dynamicLoader(pages.dynamicSource(), { baseUrl: '/docs', }); export async function getSource() { return pagesLoader.get(); } ``` The returned source from `pagesLoader.get()` is a normal [content loader](/docs/headless/source-api) instance. ## Usage [#usage] `page.data.load()` returns a renderer for the processed content: ```tsx title="app/docs/[[...slug]]/page.tsx" import { getSource } from '@/lib/source'; import { DocsPage, DocsBody, DocsTitle, DocsDescription } from 'fumadocs-ui/page'; import { notFound } from 'next/navigation'; export default async function Page(props: PageProps<'/docs/[[...slug]]'>) { const params = await props.params; const source = await getSource(); const page = source.getPage(params.slug); if (!page) notFound(); const renderer = await page.data.load(); const { body, toc } = await renderer.render(); return ( {page.data.title} {page.data.description} {body} ); } ``` Pass components to `render()` to map HTML tags onto your own, like headings with anchors: ```tsx import { getMDXComponents } from '@/mdx-components'; const { body, toc } = await renderer.render(getMDXComponents()); ``` ### Page Metadata [#page-metadata] The title and description of each page are read from its ``: - `` becomes the page title. - `<meta name="description">` becomes the description. You can override them (and set an icon) without touching the visible content, using `fumadocs:` metas: ```html <meta name="fumadocs:title" content="My Page" /> <meta name="fumadocs:description" content="A short description." /> <meta name="fumadocs:icon" content="SquareBookmark" /> ``` Every `<meta name>`/`content` pair of the document is also available on `page.data.metadata`. Like other local sources, `meta.json` files customize the page tree, validated with `metaSchema`. ### Content Options [#content-options] A page keeps only its content element: the only `<main>`, then the only `<article>`, falling back to `<body>` with page chrome like `<header>` and `<nav>` dropped. Its `class` and `style` attributes are removed so your theme's prose styling takes over, and its headings get ids to power the table of contents and search. Each part of that is configurable: ```ts import { localHtml } from '@fumadocs/local-html'; const pages = localHtml({ dir: 'content/pages', // keep the original class/style attributes instead of adapting to your theme adaptStyles: false, // drop additional tags from the content exclude: ['aside'], // custom logic to pick the element holding the page content selectContent: (root) => { // return a hast element, or undefined for the default behaviour }, }); ``` ### Syntax Highlighting [#syntax-highlighting] `<pre><code>` blocks are highlighted with [Shiki](https://shiki.style), using the same options as [Local Markdown](/docs/integrations/content/local-md): ```ts const pages = localHtml({ dir: 'content/pages', rehypeCodeOptions: { themes: { light: 'github-light', dark: 'github-dark', }, }, }); ``` The language is read from the `language-*` class of the block, on the `<code>` or its enclosing `<pre>`: ```html <pre><code class="language-ts">const answer: number = 42;</code></pre> ``` A block without one falls back to plain text. Pass `rehypeCodeOptions: false` to disable syntax highlighting. ### Hot Reload [#hot-reload] `local-html` works at runtime, so nothing watches your files by default. During development, pick one of the two watchers. #### Vite [#vite] Vite already watches your project, so its watcher can invalidate the source in the same process. No extra process or port. ```ts tab="vite.config.ts" import { localContentPlugin } from '@fumadocs/local-html/dev/vite'; export default defineConfig({ plugins: [localContentPlugin()], }); ``` ```ts tab="lib/source.ts" import { watchWithVite } from '@fumadocs/local-html/dev/vite'; if (import.meta.env.DEV) { watchWithVite(pages); } ``` #### Dev Server [#dev-server] For everything else, run the bundled dev server alongside your app. The watcher lives in one process and broadcasts over a websocket, so it works when your framework runs several workers. ```json tab="package.json" { "scripts": { "dev": "local-html dev -- next dev" } } ``` ```ts tab="lib/source.ts" import { watchWithDevServer } from '@fumadocs/local-html/dev/ws'; // change it if you use a different framework (e.g. import.meta.env.DEV) if (process.env.NODE_ENV === 'development') { void watchWithDevServer(pages); } ``` The command publishes its URL through an environment variable, so no configuration is needed. Pass one explicitly if you run the server yourself, and use `-p` to change the port: ```ts void watchWithDevServer(pages, { url: 'ws://127.0.0.1:8000/_fumadocs_local_md' }); ``` This invalidates the source on the server. On frameworks that cache rendered output in the browser, like Next.js, render `<DevClient />` in your root layout so the page refreshes too: ```tsx title="app/layout.tsx" import { DevClient } from '@fumadocs/local-html/dev/react-client'; export function RootLayout({ children }) { return ( <RootProvider> {/* [!code ++] */} <DevClient /> {children} </RootProvider> ); } ``` Both watchers keep the loader in sync when local HTML files change. To wire up your own instead, call `pages.invalidateFile(path)` when a file changes. ### Disable Revalidation [#disable-revalidation] You can use `staticSource()` when you only need a one-time snapshot without revalidation. It works with a normal `loader()` instead of only `dynamicLoader()`: ```ts title="lib/source.ts" import { loader } from 'fumadocs-core/source'; import { localHtml } from '@fumadocs/local-html'; const pages = localHtml({ dir: 'content/pages', }); export const source = loader(await pages.staticSource(), { baseUrl: '/docs', }); ``` # Fumadocs (Framework Mode): Local Markdown URL: /docs/integrations/content/local-md Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/local-md.mdx Content source for local Markdown content. ## Introduction [#introduction] `@fumadocs/local-md` is a content source for local Markdown/MDX files, it is bundleless (works fully at runtime) by design. As compared to [MDX Remote](/docs/integrations/content/mdx-remote), it is more comprehensive & robust while focused solely on local files. As compared to [Fumadocs MDX](/docs/mdx), it doesn't need a type-gen or bundler to work, but build-time image optimization will be disabled. ### Limitations [#limitations] - No build-time image optimization. - No imports/exports in MDX files, but you can pass variables & components at render phase. ## Setup [#setup] Install the package: ```package-install @fumadocs/local-md shiki ``` > `shiki` is installed because it has to be externalized by the bundler. Create a `localMd` instance and connect it to Fumadocs: ```ts title="lib/source.ts" import { dynamicLoader } from 'fumadocs-core/source'; import { localMd } from '@fumadocs/local-md'; const docs = localMd({ dir: 'content/docs', // options }); const docsLoader = dynamicLoader(docs.dynamicSource(), { baseUrl: '/docs', }); export async function getSource() { return docsLoader.get(); } ``` ### Schema [#schema] You may pass `frontmatterSchema` and `metaSchema` to customize the validation schemas: ```ts title="lib/source.ts" import { localMd } from '@fumadocs/local-md'; import { pageSchema, metaSchema } from 'fumadocs-core/source/schema'; const docs = localMd({ dir: 'content/docs', frontmatterSchema: pageSchema.extend({ // ... }), metaSchema: metaSchema.extend({ // ... }), }); ``` ## Usage [#usage] The recommended integration is: 1. Create `localMd({ dir })` for your content directory. 2. Pass `docs.dynamicSource()` to `dynamicLoader()`. 3. Read the source in your route/layout and render it with Fumadocs UI. For example, in a docs layout: ```tsx title="app/docs/layout.tsx" import { getSource } from '@/lib/source'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; export default async function Layout({ children }: LayoutProps<'/docs'>) { const docs = await getSource(); return <DocsLayout tree={docs.getPageTree()}>{children}</DocsLayout>; } ``` The returned source from `docsLoader.get()` is a normal [content loader](/docs/headless/source-api) instance. ### Hot Reload [#hot-reload] `local-md` works at runtime, so nothing watches your files by default. During development, pick one of the two watchers. #### Vite [#vite] Vite already watches your project, so its watcher can invalidate the source in the same process. No extra process or port. ```ts title="vite.config.ts" import { localContentPlugin } from '@fumadocs/local-md/dev/vite'; export default defineConfig({ plugins: [localContentPlugin()], }); ``` ```ts title="lib/source.ts" import { watchWithVite } from '@fumadocs/local-md/dev/vite'; if (import.meta.env.DEV) { watchWithVite(docs); } ``` The plugin adds your content directory to Vite's watcher, since it usually sits outside the module graph. Content is read at runtime rather than imported, so there is no module to invalidate and the browser is reloaded instead. Pass `localContentPlugin({ reload: false })` to handle that yourself. #### Dev Server [#dev-server] For everything else, run the bundled dev server alongside your app. The watcher lives in one process and broadcasts over a websocket, so it works when your framework runs several workers. ```json title="package.json" { "scripts": { "dev": "local-md dev -- next dev" } } ``` ```ts title="lib/source.ts" import { watchWithDevServer } from '@fumadocs/local-md/dev/ws'; // change it if you use a different framework (e.g. import.meta.env.DEV) if (process.env.NODE_ENV === 'development') { void watchWithDevServer(docs); } ``` The command publishes its URL through an environment variable, so no configuration is needed. Pass one explicitly if you run the server yourself, and use `-p` to change the port: ```ts void watchWithDevServer(docs, { url: 'ws://127.0.0.1:8000/_fumadocs_local_md' }); ``` Both keep the loader in sync when local Markdown or MDX files change. To wire up your own watcher instead, call `docs.invalidateFile(path)` when a file changes. ### JavaScript Engine [#javascript-engine] When compiling Markdown files (`*.md`), `@fumadocs/local-md` uses a virtual JavaScript engine to avoid `eval()` at runtime. This allows your app to work on environments like Cloudflare Worker, while the performance will be slower than the native JavaScript JIT compiler. ### Disable Revalidation [#disable-revalidation] You can use `staticSource()` when you only need a one-time snapshot without revalidation. It works with a normal `loader()` instead of only `dynamicLoader()`: ```ts title="lib/source.ts" import { loader } from 'fumadocs-core/source'; import { localMd } from '@fumadocs/local-md'; const docs = localMd({ dir: 'content/docs', }); export const source = loader(await docs.staticSource(), { baseUrl: '/docs', }); ``` ## Migration from Fumadocs MDX [#migration-from-fumadocs-mdx] If you're already using Fumadocs MDX for local docs content, migrating is usually straightforward. ### Before [#before] With Fumadocs MDX, a common setup looks like: ```ts tab="source.config.ts" import { defineDocs, defineConfig } from 'fumadocs-mdx/config'; export const docs = defineDocs({ dir: 'content/docs', }); export default defineConfig(); ``` ```ts tab="lib/source.ts" import { docs } from 'collections/server'; import { loader } from 'fumadocs-core/source'; export const source = loader({ baseUrl: '/docs', source: docs.toFumadocsSource(), }); ``` ### After [#after] With `@fumadocs/local-md`, you can replace that setup with: ```ts title="lib/source.ts" import { dynamicLoader } from 'fumadocs-core/source'; import { watchWithDevServer } from '@fumadocs/local-md/dev/ws'; import { localMd } from '@fumadocs/local-md'; const docs = localMd({ dir: 'content/docs', }); if (process.env.NODE_ENV === 'development') { void watchWithDevServer(docs); } const docsLoader = dynamicLoader(docs.dynamicSource(), { baseUrl: '/docs', }); export async function getSource() { return docsLoader.get(); } ``` Then: - remove `source.config.ts`. - remove framework-specific MDX integration like `createMDX()` in `next.config.mjs`. - replace `collections/server` imports with a loader created from `localMd()`. Finally, update references to your `source` object with `getSource()`: ```tsx title="app/docs/layout.tsx" import { getSource } from '@/lib/source'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; export default async function Layout({ children }: LayoutProps<'/docs'>) { // [!code --] return <DocsLayout tree={source.getPageTree()}>{children}</DocsLayout>; // [!code ++:2] const docs = await getSource(); return <DocsLayout tree={docs.getPageTree()}>{children}</DocsLayout>; } ``` And the type of pages is also changed: ```tsx tab="Before" const page = source.getPage(['...']); // title & description page.data.title; // custom frontmatter properties page.data.full; // getText() API await page.data.getText('processed'); // compiled properties: page.data.structuredData; page.data.toc; return ( <div> <page.data.body components={{}} /> </div> ); ``` ```tsx tab="After" const page = docs.getPage(['...']); // title & description (unchanged) page.data.title; // custom frontmatter properties page.data.frontmatter.full; // there's no getText() API, use data.content instead console.log(page.data.content); // compiled properties: const structuredData = await page.data.structuredData(); const { toc, body } = await (await page.data.load()).render(mdxComponents); return <div>{body}</div>; ``` ### Without RSC [#without-rsc] If your setup doesn't support RSC, unlike Fumadocs MDX's `collections/browser` API, `local-md` leverages server payload to send AST, then render Markdown at client-side. For example, in Tanstack Start: ```tsx title="src/routes/docs/$.tsx" import { createFileRoute, notFound } from '@tanstack/react-router'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { createServerFn } from '@tanstack/react-start'; import { getSource } from '@/lib/source'; import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page'; import { useFumadocsLoader } from 'fumadocs-core/source/client'; import { useMemo } from 'react'; import { useMDXComponents } from '@/components/mdx'; import { rendererFromSerialized } from '@fumadocs/local-md/client'; import type { MarkdownRendererSerializedOptions } from '@fumadocs/local-md'; // [!code highlight:5] register the payload as serializable (see below) declare module '@tanstack/router-core' { interface SerializableExtensions { fumadocsMarkdownRenderer: MarkdownRendererSerializedOptions; } } export const Route = createFileRoute('/docs/$')({ component: Page, loader: async ({ params }) => { const slugs = params._splat?.split('/') ?? []; return serverLoader({ data: slugs }); }, }); const serverLoader = createServerFn({ method: 'GET', }) .validator((slugs: string[]) => slugs) .handler(async ({ data: slugs }) => { const source = await getSource(); const page = source.getPage(slugs); if (!page) throw notFound(); // compile const { serialize } = await page.data.load(); return { frontmatter: page.data.frontmatter, // [!code highlight] pass it to payload render: serialize(), // ... }; }); function Page() { const { pageTree, frontmatter, render } = useFumadocsLoader(Route.useLoaderData()); // [!code highlight:2] render under sync mode const renderer = useMemo(() => rendererFromSerialized(render), [render]); const { body, toc } = renderer.renderSync(useMDXComponents()); return ( <DocsLayout tree={pageTree}> <DocsPage toc={toc}> {/* ... */} <DocsBody>{body}</DocsBody> </DocsPage> </DocsLayout> ); } ``` TanStack Start checks that the result of server functions is serializable. The serialized renderer contains a HAST tree, which is serializable but too complex for the type-level check to verify. Register it in `SerializableExtensions` from `@tanstack/router-core` (install it if it isn't resolvable) to skip the check for it. # Fumadocs (Framework Mode): MDX Remote URL: /docs/integrations/content/mdx-remote Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/mdx-remote.mdx Compiled Markdown/MDX content on demand. ## Introduction [#introduction] Fumadocs offers the **MDX Remote** package, it is a helper to integrate Markdown/MDX content sources with Fumadocs. You can think it as a `next-mdx-remote` with built-in plugins for Fumadocs. <Callout type='warn' title="Be Careful"> The Passed MDX content must be trusted as it allows code execution by default. </Callout> ## Setup [#setup] ```package-install @fumadocs/mdx-remote ``` The main feature it offers is the MDX Compiler, it can compile MDX content to JSX nodes. Since it doesn't use a bundler, there's some limitations: - Images have to be optimized at runtime. - No imports and exports in MDX files. ### With RSC [#with-rsc] It's compatible with Server Components. For example: ```tsx import { createCompiler } from '@fumadocs/mdx-remote'; import { getPage } from './my-content-source'; import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page'; import { getMDXComponents } from '@/components/mdx'; const compiler = createCompiler({ // options }); export default async function Page({ params }: PageProps<'/docs/[[...slug]]'>) { const { slug } = await params; const page = getPage(slug); const { toc, body: MdxContent } = await compiler.compile({ source: page.content, }); return ( <DocsPage toc={toc}> <DocsBody> <MdxContent components={getMDXComponents()} /> </DocsBody> </DocsPage> ); } ``` ### Without RSC [#without-rsc] Without Server Components, you can enable `skipRender` and pass the compiled code to client instead: ```tsx import { createCompiler } from '@fumadocs/mdx-remote'; import { executeMdxSync } from '@fumadocs/mdx-remote/client'; import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page'; import { getPage } from './my-content-source'; import { useMemo } from 'react'; import { getMDXComponents } from '@/components/mdx'; const compiler = createCompiler({ // options }); // e.g. where you generate loader data async function serverPayload(slugs?: string[]) { const page = getPage(slugs); const { compiled, frontmatter } = await compiler.compile({ source: page.content, skipRender: true, }); return { compiled, frontmatter }; } function Page() { // read compiled code from server payload const { compiled } = useLoaderData(); const { default: MdxContent, toc } = useMemo(() => executeMdxSync(compiled), [compiled]); return ( <DocsPage toc={toc}> <DocsBody> <MdxContent components={getMDXComponents()} /> </DocsBody> </DocsPage> ); } ``` > The `frontmatter` property returned by `compile()` is not validated, you may use a library like `zod` to validate its data type. ### Images [#images] On serverless platforms like Vercel, the original `public` folder (including static assets like images) will be removed after production build. The MDX compiler might no longer be able to access local images in `public`. When referencing images, make sure to use a URL. # Fumadocs (Framework Mode): Notion URL: /docs/integrations/content/notion Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/notion.mdx Pull pages from Notion and render them with Fumadocs. ## Introduction [#introduction] `@fumadocs/notion` turns a Notion database into a Fumadocs content source. It provides a dynamic [content source](/docs/headless/source-api) that pulls pages over the official Notion API, and a server renderer that renders Notion blocks with Fumadocs UI. It does not ship a client-side renderer or convert content to MDX, blocks are rendered on the server, so there is no bundler or type-gen step. ## Setup [#setup] Install the integration and the official Notion client: ```package-install @fumadocs/notion @notionhq/client ``` Import the renderer's Tailwind CSS source after the Fumadocs UI styles: ```css title="app/global.css" @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; /* [!code ++] */ @import '@fumadocs/notion/css/preset.css'; ``` ### Notion Database [step] [#notion-database] 1. [Create a Notion integration](https://developers.notion.com/docs/getting-started) with permission to read content. 2. Connect the database that contains your pages to the integration. 3. Store the integration token and the database's **data source ID** in server-only environment variables. ```ini title=".env.local" NOTION_TOKEN=ntn_... NOTION_DATA_SOURCE_ID=... ``` <Callout type="info"> Use a data source ID, not the database ID shown in the database URL. In the current Notion API, a database is a container and each query targets one of its data sources. See Notion's [data source upgrade guide](https://developers.notion.com/guides/get-started/upgrade-guide-2025-09-03). </Callout> ### Notion Integration [step] [#notion-integration] Create the integration once and share it between the content source and the file route. ```ts title="lib/notion.ts" import { Client } from '@notionhq/client'; import { createNotion } from '@fumadocs/notion'; export const notion = createNotion({ client: new Client({ auth: process.env.NOTION_TOKEN }), dataSourceId: process.env.NOTION_DATA_SOURCE_ID!, }); ``` ### Content Loader [step] [#content-loader] Pass `notion.dynamicSource()` to `dynamicLoader()` and expose the loader through `getSource()`: ```ts title="lib/source.ts" import { dynamicLoader } from 'fumadocs-core/source'; import { notion } from '@/lib/notion'; const source = dynamicLoader( notion.dynamicSource({ // map Notion properties to page fields properties: { title: 'Name', slug: 'Slug', description: 'Description', }, // filter, sort, or otherwise customize the query query: { filter: { property: 'Published', checkbox: { equals: true }, }, }, }), { baseUrl: '/docs', }, ); export async function getSource() { return source.get(); } ``` The source paginates through every matching data-source row and lazily pulls each page's nested blocks. Calls to load the same page body are deduplicated until the loader is invalidated. ### Render Pages [step] [#render-pages] Use the loader in your layout to build the page tree: ```tsx title="app/docs/layout.tsx" import { getSource } from '@/lib/source'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; export default async function Layout({ children }: LayoutProps<'/docs'>) { const source = await getSource(); return <DocsLayout tree={source.getPageTree()}>{children}</DocsLayout>; } ``` Load a page's blocks and render them with `NotionRenderer`. `blocksToTableOfContents()` produces the table of contents: ```tsx title="app/docs/[[...slug]]/page.tsx" import { notFound } from 'next/navigation'; import { getSource } from '@/lib/source'; import { NotionRenderer, blocksToTableOfContents, createNotionFileUrlResolver, } from '@fumadocs/notion'; import { DocsPage, DocsBody, DocsDescription, DocsTitle } from 'fumadocs-ui/layouts/docs/page'; export default async function Page({ params }: PageProps<'/docs/[[...slug]]'>) { const { slug = [] } = await params; const source = await getSource(); const page = source.getPage(slug); if (!page) notFound(); // [!code highlight] load the page body const { blocks } = await page.data.load(); return ( <DocsPage toc={blocksToTableOfContents(blocks)}> <DocsTitle>{page.data.title}</DocsTitle> <DocsDescription>{page.data.description}</DocsDescription> <DocsBody> <NotionRenderer blocks={blocks} getFileUrl={createNotionFileUrlResolver({ pageId: page.data.id })} /> </DocsBody> </DocsPage> ); } ``` ### Notion Files [step] [#notion-files] Notion-hosted file URLs (images, videos, PDFs) expire after about an hour, so they can't be embedded directly on a page that is cached or statically served. `createNotionFileUrlResolver()` routes those internal assets through a file handler that fetches a fresh signed URL on demand. Mount the handler with `createNotionFileHandler()`, it verifies that the requested block belongs to a page in your data source before redirecting: ```ts title="app/api/notion/file/route.ts" import { createNotionFileHandler } from '@fumadocs/notion'; import { notion } from '@/lib/notion'; export const GET = createNotionFileHandler(notion); ``` The default route is `/api/notion/file`. To mount it elsewhere, pass a matching `path` to both functions: ```ts createNotionFileUrlResolver({ pageId: page.data.id, path: '/notion/file' }); ``` <Callout type="info"> For a static export, use durable external URLs for your assets and omit `getFileUrl`, the renderer then links Notion's URLs directly. The file handler needs a dynamic runtime. </Callout> ### Done [step] [#done] Add pages to your database, mark them published, and they appear under the docs route. ## Revalidation [#revalidation] The loader caches pages until you invalidate it. To reflect Notion edits, call `revalidate()` from a trusted webhook or revalidation route: ```ts title="lib/source.ts" export function revalidateNotion() { return source.revalidate(); } ``` For a preview-style setup where every request reflects the latest content, invalidate before reading: ```ts title="lib/source.ts" export async function getSource() { source.invalidate(); // [!code ++] return source.get(); } ``` ## Page Properties [#page-properties] `properties` maps Notion properties to page fields. When omitted, the source: - uses the first Notion `title` property as the page title, - uses a property named `Slug` for the URL, falling back to a slug generated from the title, - uses a property named `Description` when present. A slug can contain `/` to create nested routes. Duplicate or unsafe virtual paths fail with a descriptive error instead of silently replacing a page. ## Query and Paths [#query-and-paths] Pass Notion filters and sorts through `query`. Use `baseDir` to place the virtual pages under a directory, or `generatePath` for full control over the virtual file path: ```ts notion.dynamicSource({ baseDir: 'docs', query: { sorts: [{ timestamp: 'last_edited_time', direction: 'descending' }], }, generatePath(page, info) { return info.slugs.length > 0 ? `${info.slugs.join('/')}.mdx` : `${page.id}.mdx`; }, }); ``` ## Customize Rendering [#customize-rendering] `NotionRenderer` ships renderers for common block types. Pass `components` to add or override them, each override is typed against its block: ```tsx import { NotionRenderer } from '@fumadocs/notion'; <NotionRenderer blocks={blocks} components={{ callout({ block, children, renderRichText }) { return ( <aside className="rounded-lg bg-fd-muted p-4"> {renderRichText(block.callout.rich_text)} {children} </aside> ); }, }} />; ``` Code blocks are highlighted on the server with Shiki by default. Pass `highlightCode={false}` to render plain code, or a function to plug in your own highlighter. ## Source-only Import [#source-only-import] Code that only pulls Notion pages, such as a search indexer or `llms.txt` route, can import from `@fumadocs/notion/source` to skip the React renderer entry: ```ts import { createNotion } from '@fumadocs/notion/source'; ``` This entry keeps the Notion SDK external and contains no React imports. `blocksToPlainText()`, `blocksToStructuredData()`, and `blocksToTableOfContents()` are available here too. # Fumadocs (Framework Mode): Obsidian URL: /docs/integrations/content/obsidian Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/obsidian.mdx Render an Obsidian vault directly with Fumadocs. `fumadocs-obsidian` exposes a runtime content source. It reads and compiles the vault directly, so it does not generate intermediate MDX files or need a Fumadocs MDX remark plugin. Supported Obsidian syntax includes: - Wikilinks and embeds. - Callouts. - Block IDs. - Comments. Ambiguous wikilink names resolve like Obsidian: notes take precedence over attachments, then the shortest path wins. Code fences in unknown languages (e.g. `dataview`) render as plain text. ## Setup [#setup] Install the package: ```package-install fumadocs-obsidian shiki ``` ### Generate Styles [#generate-styles] The components from `fumadocs-obsidian/ui` are styled with Tailwind CSS. Add the following line so their classes are generated: ```css title="Tailwind CSS" @import 'tailwindcss'; @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; /* [!code ++] */ @import 'fumadocs-obsidian/css/preset.css'; ``` ### Configure Source [#configure-source] Create an Obsidian source and pass it to `dynamicLoader()`: ```ts title="lib/source.ts" import { dynamicLoader } from 'fumadocs-core/source'; import { obsidian } from 'fumadocs-obsidian'; const vault = obsidian({ dir: 'public/vault', // map vault attachments to their public URLs url: (path) => `/vault/${path}`, }); if (process.env.NODE_ENV === 'development') { void vault.devServer(); } const vaultLoader = dynamicLoader(vault.dynamicSource(), { baseUrl: '/docs', }); export function getSource() { return vaultLoader.get(); } ``` The example keeps its vault in `public/vault`, allowing Next.js to serve attachments at the URLs returned by `url`. You can keep the vault elsewhere when your app serves or uploads its attachments separately. Then load and render a page: ```tsx title="app/docs/[[...slug]]/page.tsx" import { getSource } from '@/lib/source'; import { notFound } from 'next/navigation'; import defaultMdxComponents, { createRelativeLink } from 'fumadocs-ui/mdx'; import * as ObsidianComponents from 'fumadocs-obsidian/ui'; import { DocsBody, DocsPage } from 'fumadocs-ui/layouts/docs/page'; export default async function Page({ params }: PageProps<'/docs/[[...slug]]'>) { const source = await getSource(); const page = source.getPage((await params).slug); if (!page) notFound(); const { body, toc } = await ( await page.data.load() ).render({ ...defaultMdxComponents, ...ObsidianComponents, a: createRelativeLink(source, page), }); return ( <DocsPage toc={toc}> <DocsBody>{body}</DocsBody> </DocsPage> ); } ``` `page.data.load()` compiles each page at most once per vault snapshot. Vault content is compiled as plain Markdown: rendering only maps it to JSX and resolves component names, JavaScript in content is never evaluated. Media files are referenced by URL and never read into memory. ## Hot Reload [#hot-reload] Run the included content watcher alongside your framework: ```json title="package.json" { "scripts": { "dev": "fumadocs-obsidian dev -- next dev" } } ``` The `vault.devServer()` call shown above receives changes from that watcher. Since wikilinks and aliases can create cross-file dependencies, a change invalidates the whole source snapshot. For Vite, use the in-process adapter instead: ```ts title="vite.config.ts" import { localContentPlugin } from 'fumadocs-obsidian/dev/vite'; export default defineConfig({ plugins: [localContentPlugin()], }); ``` ```ts title="lib/source.ts" import { watchWithVite } from 'fumadocs-obsidian/dev/vite'; if (import.meta.env.DEV) watchWithVite(vault); ``` ## Static Source [#static-source] Use `staticSource()` when the vault only needs to be read once: ```ts title="lib/source.ts" import { loader } from 'fumadocs-core/source'; import { obsidian } from 'fumadocs-obsidian'; const vault = obsidian({ dir: 'public/vault', url: (path) => `/vault/${path}`, }); export const source = loader(await vault.staticSource(), { baseUrl: '/docs', }); ``` ## Schema [#schema] Use Standard Schema-compatible validators to customize page frontmatter and `meta.json` files: ```ts title="lib/source.ts" import { frontmatterSchema, obsidian } from 'fumadocs-obsidian'; import { metaSchema } from 'fumadocs-core/source/schema'; import { z } from 'zod'; const vault = obsidian({ dir: 'public/vault', frontmatterSchema: frontmatterSchema.extend({ tags: z.array(z.string()).optional(), }), metaSchema, }); ``` The default `frontmatterSchema` is forgiving about loosely structured vaults: scalar titles like `title: 2024` are coerced to strings, and `aliases` accepts both a single name and a list. A note failing validation is reported and skipped without affecting the rest of the vault. ### Additions [#additions] Pass remark/rehype plugins to enable syntax features separately: - [Mermaid](/docs/markdown/mermaid). - [Math](/docs/markdown/math). ```ts title="lib/source.ts" import { obsidian } from 'fumadocs-obsidian'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; const vault = obsidian({ dir: 'public/vault', remarkPlugins: [remarkMath], rehypePlugins: [rehypeKatex], }); ``` # Fumadocs (Framework Mode): Python URL: /docs/integrations/content/python Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/python.mdx Generate API references from Python packages. <Callout type="warn" title="Experimental"> Support for Python docgen is still experimental, please use it in caution. </Callout> ## Introduction [#introduction] `fumadocs-python` documents a Python package from its source code and docstrings. A Python command collects the package into a JSON file, which you can serve directly as a runtime content source, or convert into MDX files. Every module and class becomes a page, with functions, attributes and parameters rendered by the included components. ## Setup [#setup] Install the package: ```package-install fumadocs-python shiki ``` ### Generate JSON [#generate-json] Install the Python command, it collects docs from your Python package: ```bash pip install ./node_modules/fumadocs-python ``` Generate the JSON file of a package: ```bash fumapy-generate httpx ``` It writes `httpx.json` into the current directory, pass `--dir` to change it. Docstring sections like `Parameters:` and `Returns:` are parsed as Google style, pass `--docstring-style numpy` or `--docstring-style sphinx` for other styles. Docstrings outside these styles are rendered as Markdown. ### Styles [#styles] The components are styled with Tailwind CSS, add the preset: ```css title="Tailwind CSS" @import 'tailwindcss'; @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; /* [!code ++] */ @import 'fumadocs-python/preset.css'; ``` ### Configure Source [#configure-source] Create a source from the JSON file and pass it to `loader()`: ```ts title="lib/source.ts" import { loader } from 'fumadocs-core/source'; import { createPython } from 'fumadocs-python'; const python = createPython({ file: './httpx.json', // Pages are grouped in a directory named after the root module, for example `httpx._client.Client` is served at `/docs/httpx/_client/Client`. // Set to "none" to place them at the root of the source instead. groupBy: 'module', }); export const source = loader(await python.staticSource(), { baseUrl: '/docs', // adds a `module` or `class` badge to generated pages plugins: [python.loaderPlugin()], }); ``` Then load and render a page: ```tsx title="app/docs/[[...slug]]/page.tsx" import { source } from '@/lib/source'; import { notFound } from 'next/navigation'; import { DocsBody, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page'; export default async function Page({ params }: PageProps<'/docs/[[...slug]]'>) { const page = source.getPage((await params).slug); if (!page) notFound(); const data = await page.data.load(); const { body, toc } = await data.render(); return ( <DocsPage toc={toc}> <DocsTitle>{page.data.title}</DocsTitle> <DocsBody>{body}</DocsBody> </DocsPage> ); } ``` ### Dynamic Source [#dynamic-source] `staticSource()` reads the JSON file once. To pick up a regenerated file without restarting, use `dynamicSource()` with `dynamicLoader()` instead: ```ts title="lib/source.ts" import { dynamicLoader } from 'fumadocs-core/source'; import { createPython } from 'fumadocs-python'; const python = createPython({ file: './httpx.json', }); const pythonLoader = dynamicLoader(python.dynamicSource(), { baseUrl: '/docs', }); export function getSource() { return pythonLoader.get(); } ``` The file is read again after `pythonLoader.invalidate()`, and `getSource()` returns the same [content loader](/docs/headless/source-api) instance until then. ### Compiler [#compiler] Source code and docstring examples are highlighted with [Shiki](https://shiki.style), configure it with `rehypeCodeOptions`. You can also pass remark/rehype plugins: ```ts title="lib/source.ts" import { createPython } from 'fumadocs-python'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; const python = createPython({ file: './httpx.json', rehypeCodeOptions: { themes: { light: 'github-light', dark: 'github-dark', }, }, remarkPlugins: [remarkMath], rehypePlugins: [rehypeKatex], }); ``` ## Generate MDX Files [#generate-mdx-files] You can convert the JSON into MDX files instead, and keep them next to your other content. Links between modules and classes are written into the files, so `baseUrl` must match where the output directory is served: ```js tab="scripts/generate-docs.mjs" import { rimraf } from 'rimraf'; import * as Python from 'fumadocs-python'; import * as fs from 'node:fs/promises'; const content = JSON.parse(await fs.readFile('./httpx.json', 'utf-8')); // clean previous output await rimraf('content/docs/httpx'); await Python.write(Python.convert(content, { baseUrl: '/docs' }), 'content/docs'); ``` ```tsx tab="mdx-components.tsx" import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; import * as Python from 'fumadocs-python/components'; export function getMDXComponents(components?: MDXComponents): MDXComponents { return { ...defaultMdxComponents, ...Python, ...components, }; } ``` # Fumadocs (Framework Mode): Sanity URL: /docs/integrations/content/sanity Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/sanity.mdx Official integration for Sanity. ## Overview [#overview] Fumadocs provide an official integration to simplify setup for Sanity CMS, it brings native live preview experience without compromise on performance. > You can see our [Sanity Example](https://github.com/fuma-nama/fumadocs-sanity) on GitHub. ## Setup [#setup] Assume you already have [Sanity](https://sanity.io/docs/getting-started) configured according to their docs. ### Data Types [step] [#data-types] Run the following command to initialize content types. ```npm npx @fumadocs/cli add sanity/base ``` > Or you can run `npx @fumadocs/cli add` and browse additional Sanity presets to install. Register the installed types: ```ts tab="schemaTypes/index.ts" import { docsType, blockContent, callout, card, cards } from '@/lib/sanity/base'; // [!code ++] export const schemaTypes = [blockContent, callout, card, cards, docsType]; ``` ```tsx tab="components/sanity.ts" import { PortableText, type PortableTextProps, type PortableTextReactComponents, } from '@portabletext/react'; // the installed components import { baseBlocks, baseComponents, baseMarks } from './base'; const components: Partial<PortableTextReactComponents> = { block: { // [!code ++] ...baseBlocks, }, marks: { // [!code ++] ...baseMarks, }, types: { // [!code ++] ...baseComponents, }, }; export function CustomPortableText(props: PortableTextProps) { return <PortableText {...props} components={{ ...components, ...props.components }} />; } ``` ### Content Loader [step] [#content-loader] Install the integration: ```npm npm i @fumadocs/sanity ``` ```ts title="lib/source.ts" tab="Next.js (live preview)" import { dynamicLoader } from 'fumadocs-core/source'; import { createSanitySource } from '@fumadocs/sanity'; // the live preview client import { sanityFetch } from '@/sanity/lib/live'; // the generated types import type { Docs } from '@/sanity/types'; const source = dynamicLoader( createSanitySource<Docs>({ sanityFetch, docType: 'docs', }), { baseUrl: '/docs', }, ); export async function getSource() { return source.get(); } ``` ```ts title="lib/source.ts" tab="Others" import { dynamicLoader } from 'fumadocs-core/source'; import { createSanitySource } from '@fumadocs/sanity'; // the Sanity client import { client } from '@/sanity/lib/client'; // the generated types import type { Docs } from '@/sanity/types'; const source = dynamicLoader( createSanitySource<Docs>({ client, docType: 'docs', }), { baseUrl: '/docs', }, ); export async function getSource() { return source.get(); } ``` With `sanityFetch`, queries are cached by Next.js and revalidated by `<SanityLive />` when content changes. The loader re-reads them on every render, so live updates and draft mode work without calling `invalidate()`. If you enabled [Cache Components](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents), call `getSource()` inside a `'use cache'` scope (for example, a cached page) as `sanityFetch` requires one. Now update references to your `source` with the `getSource()` function like: ```tsx import { getSource } from '@/lib/source'; import { CustomPortableText } from '@/components/sanity'; import { DocsPage, DocsBody, DocsDescription, DocsTitle } from 'fumadocs-ui/layouts/docs/page'; // [!code highlight] const source = await getSource(); const page = source.getPage(['...']); // basic info console.log(page.data.title, page.data.description); // [!code highlight] load full info const { renderToc, body } = await page.data.load(); // to render docs page content: return ( // `renderToc()` to render TOC items on server <DocsPage toc={renderToc({ render: (body) => <CustomPortableText value={body} /> })}> <DocsTitle>{page.data.title}</DocsTitle> <DocsDescription>{page.data.description}</DocsDescription> <DocsBody> <CustomPortableText value={body!} /> </DocsBody> </DocsPage> ); ``` If you are not using React Server Components, `renderToc` can be imported separately: ```tsx import { getSource } from '@/lib/source'; import { renderToc } from '@fumadocs/sanity/client'; import { CustomPortableText } from '@/components/sanity'; import { DocsPage, DocsBody, DocsDescription, DocsTitle } from 'fumadocs-ui/layouts/docs/page'; // where you return server payload export async function serverPayload(slugs: string[]) { const source = await getSource(); const page = source.getPage(slugs); if (!page) throw new Error('not found'); const { title, description, _toc, body } = await page.data.load(); return { title, description, toc: _toc, body }; } export function Page() { // receive server payload const { toc, body, title, description } = useLoaderData(); return ( <DocsPage toc={renderToc({ toc, render: (body) => <CustomPortableText value={body} /> })}> <DocsTitle>{title}</DocsTitle> <DocsDescription>{description}</DocsDescription> <DocsBody> <CustomPortableText value={body!} /> </DocsBody> </DocsPage> ); } ``` ### Done [step] [#done] Create documents under the `docs` type in your studio editor, new content will be displayed in the docs route. # Fumadocs (Framework Mode): Local Markdown (Sätteri) URL: /docs/integrations/content/satteri-local-md Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/content/satteri-local-md.mdx Content source for local Markdown content, compiled with Sätteri. ## Introduction [#introduction] `@fumadocs/satteri/local-md` is a content source for local Markdown/MDX files, compiled with [Sätteri](https://github.com/bruits/satteri) instead of the unified/remark pipeline. It has the same API shape as [`@fumadocs/local-md`](/docs/integrations/content/local-md) and is also bundleless, so it works fully at runtime with no type-gen or bundler step. Reach for it when you want Sätteri's compiler and its plugin model, and to reuse the Fumadocs Sätteri plugins and preset. ### Differences [#differences] - Compilation always produces JavaScript, so there is no virtual JavaScript engine option. Rendering uses the native engine. - Plugins are Sätteri plugins, configured through `satteriOptions`, not remark/rehype plugins. ## Setup [#setup] ```package-install @fumadocs/satteri shiki ``` > `shiki` is installed because it has to be externalized by the bundler. Create a `localMd` instance and connect it to Fumadocs: ```ts title="lib/source.ts" import { dynamicLoader } from 'fumadocs-core/source'; import { localMd } from '@fumadocs/satteri/local-md'; const docs = localMd({ dir: 'content/docs', }); const docsLoader = dynamicLoader(docs.dynamicSource(), { baseUrl: '/docs', }); export async function getSource() { return docsLoader.get(); } ``` ### Schema [#schema] Pass `frontmatterSchema` and `metaSchema` to customize the validation schemas: ```ts title="lib/source.ts" import { localMd } from '@fumadocs/satteri/local-md'; import { pageSchema, metaSchema } from 'fumadocs-core/source/schema'; const docs = localMd({ dir: 'content/docs', frontmatterSchema: pageSchema.extend({ // ... }), metaSchema: metaSchema.extend({ // ... }), }); ``` ### Compiler Options [#compiler-options] `satteriOptions` is passed through the Fumadocs Sätteri preset, so plugins and their options are configured the same way as in Fumadocs MDX: ```ts title="lib/source.ts" const docs = localMd({ dir: 'content/docs', satteriOptions: { rehypeCodeOptions: { themes: { light: 'github-light', dark: 'github-dark' }, }, mdastPlugins: [myPlugin()], }, }); ``` Pass a function to build the options lazily. ## Usage [#usage] Read the source in your route or layout and render it with Fumadocs UI, exactly as with `@fumadocs/local-md`: ```tsx title="app/docs/layout.tsx" import { getSource } from '@/lib/source'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; export default async function Layout({ children }: LayoutProps<'/docs'>) { const docs = await getSource(); return <DocsLayout tree={docs.getPageTree()}>{children}</DocsLayout>; } ``` Each page exposes `load()`, which compiles it once and returns a renderer: ```tsx const page = source.getPage(params.slug); const { body, toc } = await (await page.data.load()).render(); ``` The renderer also exposes `structuredData`, and `exports` for anything the document or its plugins exported. ### Client Rendering [#client-rendering] `serialize()` returns a JSON-safe payload, so a server can hand a compiled page to the client: ```tsx title="app/page.tsx" const serialized = (await page.data.load()).serialize(); ``` ```tsx title="components/content.tsx" 'use client'; import { rendererFromSerialized } from '@fumadocs/satteri/local-md/client'; export function Content({ serialized }) { return rendererFromSerialized(serialized).renderSync().body; } ``` ### Hot Reload [#hot-reload] The source works at runtime, so nothing watches your files by default. On Vite, use its own watcher: ```ts title="vite.config.ts" import { localContentPlugin } from '@fumadocs/satteri/local-md/dev/vite'; export default defineConfig({ plugins: [localContentPlugin()], }); ``` ```ts title="lib/source.ts" import { watchWithVite } from '@fumadocs/satteri/local-md/dev/vite'; if (import.meta.env.DEV) { watchWithVite(docs); } ``` Otherwise run the dev server from `@fumadocs/local-md` alongside your app, and connect to it: ```json title="package.json" { "scripts": { "dev": "local-md dev -- next dev" } } ``` ```ts title="lib/source.ts" import { watchWithDevServer } from '@fumadocs/satteri/local-md/dev/ws'; // change it if you use a different framework (e.g. import.meta.env.DEV) if (process.env.NODE_ENV === 'development') { void watchWithDevServer(docs); } ``` To wire up your own watcher instead, call `docs.invalidateFile(path)` when a file changes. ### Disable Revalidation [#disable-revalidation] Use `staticSource()` when you only need a one-time snapshot, so a normal `loader()` works: ```ts title="lib/source.ts" import { loader } from 'fumadocs-core/source'; export const source = loader(await docs.staticSource(), { baseUrl: '/docs', }); ``` # Fumadocs (Framework Mode): Feedback URL: /docs/integrations/feedback Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/feedback.mdx Receive feedback from your users ## Overview [#overview] Feedback is crucial for knowing what your reader thinks, and help you to further improve documentation content. You can integrate a simple feedback system with Fumadocs. ## Installation [#installation] Install it using **Fumadocs CLI**, it also adds the component to your docs page when it finds one: ```npm npx @fumadocs/cli feature feedback ``` Or install the component only: ```npm npx @fumadocs/cli@latest add feedback ``` ### Page Feedback [#page-feedback] To create a page-level feedback UI, add the `<Feedback />` component to your docs page: ```tsx import { DocsPage } from 'fumadocs-ui/layout/docs/page'; import { Feedback } from '@/components/feedback/client'; export default async function Page() { return ( <DocsPage> {/* at the bottom of page */} <Feedback onSendAction={async (feedback) => { 'use server'; console.log(feedback); }} /> </DocsPage> ); } ``` - `onSendAction`: fired when user submit feedback. You can specify a server action, or any function (in client component) to handle the user feedback. For example, to report user feedback as a `on_rate_docs` event on PostHog. ### Feedback Block [#feedback-block] You can also configure block-level feedback (e.g. a feedback popover as user select text). Add the `remark-block-id` Remark plugin: ```tsx title="source.config.ts (Fumadocs MDX)" import { remarkBlockId, type RemarkBlockIdOptions, } from 'fumadocs-core/mdx-plugins/remark-block-id'; import { defineConfig } from 'fumadocs-mdx/config'; const blockIdOptions: RemarkBlockIdOptions = { addDataAttribute: 'feedback', }; export default defineConfig({ mdxOptions: { remarkPlugins: [ // [!code ++] [remarkBlockId, blockIdOptions], ], }, }); ``` Then, wrap your page content under the `FeedbackText` component: ```tsx import { DocsPage } from 'fumadocs-ui/layout/docs/page'; import { FeedbackText } from '@/components/feedback/client'; export default async function Page() { return ( <DocsPage> <FeedbackText onSendAction={async (feedback) => { 'use server'; console.log(feedback); }} > {/* the content of page */} </FeedbackText> </DocsPage> ); } ``` - `onSendAction`: fired when user submit feedback. <Callout type='info' title='Good to know'> `remark-block-id` generates a block ID from its content and order in the page, hence it is also possible to track the blocks from 3rd party services. </Callout> ### Integrating with GitHub Discussion [#integrating-with-github-discussion] To report your feedback to GitHub Discussion, you can copy this file as a starting point: ```ts title='lib/github.ts' import { App, Octokit } from 'octokit'; import { blockFeedback, BlockFeedback, pageFeedback, type ActionResponse, type PageFeedback, } from '@/components/feedback/schema'; export const repo = 'fumadocs'; export const owner = 'fuma-nama'; export const DocsCategory = 'Docs Feedback'; let instance: Octokit | undefined; async function getOctokit(): Promise<Octokit> { if (instance) return instance; const appId = process.env.GITHUB_APP_ID; const privateKey = process.env.GITHUB_APP_PRIVATE_KEY; if (!appId || !privateKey) { throw new Error('No GitHub keys provided for Github app, docs feedback feature will not work.'); } const app = new App({ appId, privateKey, }); const { data } = await app.octokit.request('GET /repos/{owner}/{repo}/installation', { owner, repo, headers: { 'X-GitHub-Api-Version': '2022-11-28', }, }); instance = await app.getInstallationOctokit(data.id); return instance; } interface RepositoryInfo { id: string; discussionCategories: { nodes: { id: string; name: string; }[]; }; } let cachedDestination: RepositoryInfo | undefined; async function getFeedbackDestination() { if (cachedDestination) return cachedDestination; const octokit = await getOctokit(); const { repository, }: { repository: RepositoryInfo; } = await octokit.graphql(` query { repository(owner: "${owner}", name: "${repo}") { id discussionCategories(first: 25) { nodes { id name } } } } `); return (cachedDestination = repository); } export async function onPageFeedbackAction(feedback: PageFeedback): Promise<ActionResponse> { 'use server'; feedback = pageFeedback.parse(feedback); const url = new URL(feedback.url); return createDiscussionThread( url.pathname, `[${feedback.opinion}] ${feedback.message}\n\n> Forwarded from user feedback.`, ); } export async function onBlockFeedbackAction(feedback: BlockFeedback): Promise<ActionResponse> { 'use server'; feedback = blockFeedback.parse(feedback); const url = new URL(feedback.url); url.hash = feedback.blockId; return createDiscussionThread( url.pathname, `> ${feedback.blockBody}\n\n${feedback.message}\n\n> [Forwarded from user feedback](${url.href}).`, ); } async function createDiscussionThread(pageId: string, body: string) { const octokit = await getOctokit(); const destination = await getFeedbackDestination(); const category = destination.discussionCategories.nodes.find( (category) => category.name === DocsCategory, ); if (!category) throw new Error(`Please create a "${DocsCategory}" category in GitHub Discussion`); const title = `Feedback for ${pageId}`; const queryResult: { search: { nodes: { id: string; title: string; url: string }[]; }; } = await octokit.graphql(` query { search(type: DISCUSSION, query: ${JSON.stringify(`"${title}" in:title repo:${owner}/${repo} author:@me`)}, first: 10) { nodes { ... on Discussion { id, title, url } } } }`); const discussion = queryResult.search.nodes.find((item) => item.title === title); if (discussion) { const result: { addDiscussionComment: { comment: { id: string; url: string }; }; } = await octokit.graphql(` mutation { addDiscussionComment(input: { body: ${JSON.stringify(body)}, discussionId: "${discussion.id}" }) { comment { id, url } } }`); return { githubUrl: result.addDiscussionComment.comment.url, }; } else { const result: { createDiscussion: { discussion: { id: string; url: string }; }; } = await octokit.graphql(` mutation { createDiscussion(input: { repositoryId: "${destination.id}", categoryId: "${category.id}", body: ${JSON.stringify(body)}, title: ${JSON.stringify(title)} }) { discussion { id, url } } }`); return { githubUrl: result.createDiscussion.discussion.url, }; } } ``` 1. Create your own GitHub App and obtain its app ID and private key. 2. Fill required environment variables. 3. Replace constants like `owner`, `repo`, and `DocsCategory`. 4. Use the `onPageFeedbackAction` & `onBlockFeedbackAction` in your feedback components. # Fumadocs (Framework Mode): createGraphQLPage() URL: /docs/integrations/graphql/api-page Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/graphql/api-page.mdx The component for rendering GraphQL docs content ## Overview [#overview] Fumadocs GraphQL uses a `<GraphQLPage />` component to render page contents, it should be a client component. ```tsx title="components/api-page.tsx" 'use client'; import { createGraphQLPage } from '@fumadocs/graphql/ui'; export const GraphQLPage = createGraphQLPage({ // config }); ``` ### Playground [#playground] Enable the interactive playground on operation pages by configuring an endpoint, operations are sent over HTTP POST. ```tsx title="components/api-page.tsx" 'use client'; import { createGraphQLPage } from '@fumadocs/graphql/ui'; export const GraphQLPage = createGraphQLPage({ playground: { url: 'https://api.example.com/graphql', // allow users to edit the endpoint URL (default: true) allowUrlEdit: false, // default headers of playground requests headers: { Authorization: 'Bearer <token>', }, }, }); ``` You can replace the default fetcher, e.g. to proxy requests: ```tsx title="components/api-page.tsx" 'use client'; import { createGraphQLPage } from '@fumadocs/graphql/ui'; export const GraphQLPage = createGraphQLPage({ playground: { async fetcher(request) { const res = await fetch('/api/proxy', { method: 'POST', headers: request.headers, body: JSON.stringify({ query: request.query, variables: request.variables }), }); return { type: 'response', status: res.status, time: 0, body: await res.text(), contentType: res.headers.get('Content-Type') ?? '', }; }, }, }); ``` Or replace the playground UI entirely with `render`: ```tsx title="components/api-page.tsx" 'use client'; import { createGraphQLPage } from '@fumadocs/graphql/ui'; export const GraphQLPage = createGraphQLPage({ playground: { render({ kind, name, operation }) { return <div>my playground</div>; }, }, }); ``` <Callout title="Subscriptions"> The default fetcher sends operations over HTTP POST, which cannot serve subscriptions — the Run button is disabled on subscription pages unless a custom `fetcher` is provided. </Callout> ### Cross-linking [#cross-linking] Type & operation references are cross-linked automatically when `baseUrl` is passed to your source (see [`createGraphQL()`](/docs/integrations/graphql/server#baseurl)). You can also resolve the links yourself: ```tsx title="components/api-page.tsx" 'use client'; import { createGraphQLPage } from '@fumadocs/graphql/ui'; export const GraphQLPage = createGraphQLPage({ typeLinks(name) { return `/docs/types/${name}`; }, operationLinks(kind, name) { return `/docs/${kind}/${name}`; }, }); ``` Return `undefined` for types/operations without their own page. ### Custom Layout [#custom-layout] You can customize how operation and type pages are laid out. ```tsx title="components/api-page.tsx" 'use client'; import { createGraphQLPage } from '@fumadocs/graphql/ui'; export const GraphQLPage = createGraphQLPage({ content: { renderOperationLayout(slots) { return ( <div className="flex flex-col gap-6"> {slots.header} {slots.description} {slots.deprecated} {slots.directives} {slots.playground} {slots.arguments} {slots.returns} {slots.example} </div> ); }, renderTypeLayout(slots) { return ( <div className="flex flex-col gap-6"> {slots.header} {slots.description} {slots.directives} {slots.relations} {slots.usages} {slots.fields} {slots.values} {slots.scalar} </div> ); }, }, }); ``` ### Schema UI [#schema-ui] Customize how types are rendered. ```tsx title="components/api-page.tsx" 'use client'; import { createGraphQLPage } from '@fumadocs/graphql/ui'; export const GraphQLPage = createGraphQLPage({ schemaUI: { render(options, ctx) { // fully custom schema renderer return <ctx.SchemaUI {...options} />; }, }, }); ``` ### Internationalization [#internationalization] Assuming you have configured [Internationalization](/docs/internationalization) at UI level: ```tsx title="layout.shared.tsx" import { defineI18n } from 'fumadocs-core/i18n'; import { uiTranslations } from 'fumadocs-ui/i18n'; import { graphqlTranslations } from '@fumadocs/graphql/i18n'; const i18n = defineI18n({ languages: ['en', 'cn'], defaultLanguage: 'en', }); export const translations = i18n .translations() .extend(uiTranslations()) // [!code ++] .extend(graphqlTranslations()) .add({ cn: { displayName: 'Chinese', // [!code ++] 'Arguments(operation page)': '参数', }, }); ``` See [Translations](/docs/ui/translations) for more details. ### Syntax Highlighting [#syntax-highlighting] GraphQL pages use Shiki for code blocks. You can customize the highlighter and themes. ```tsx title="components/api-page.tsx" 'use client'; import { createGraphQLPage } from '@fumadocs/graphql/ui'; import { defaultShikiFactory } from 'fumadocs-core/highlight/shiki/full'; export const GraphQLPage = createGraphQLPage({ shiki: defaultShikiFactory, shikiOptions: { themes: { light: 'github-light', dark: 'github-dark', }, }, }); ``` ### Custom Components [#custom-components] Override the default heading, code block and Markdown components. ```tsx title="components/api-page.tsx" 'use client'; import { createGraphQLPage } from '@fumadocs/graphql/ui'; export const GraphQLPage = createGraphQLPage({ components: { Heading({ id, depth, ...props }) { const Tag = `h${depth}` as const; return <Tag id={id} {...props} />; }, CodeBlock({ lang, code }) { return <pre data-lang={lang}>{code}</pre>; }, }, }); ``` ## Customise UI [#customise-ui] For customisations beyond the available options, you can install the UI into your codebase with [Fumadocs CLI](/docs/cli). ### Full UI [#full-ui] The entire UI of GraphQL pages, built on [the headless layer](/docs/integrations/graphql/headless). ```npm npx @fumadocs/cli add graphql/page ``` It installs `<GraphQLPage />` itself, your `components/api-page.tsx` is no longer needed. ```tsx title="mdx-components.tsx" import { GraphQLPage } from '@/components/api-page'; // [!code --] import { GraphQLPage } from '@/components/graphql/page'; // [!code ++] ``` ### Parts [#parts] Install only the pieces you want to edit, and pass them back. | Component | Contents | | -------------------- | -------------------------------------------- | | `graphql/operation` | operations, with their arguments and example | | `graphql/type-docs` | named types, with their relations and fields | | `graphql/schema-ui` | the type, argument and field details | | `graphql/playground` | the interactive playground | ```tsx title="components/api-page.tsx" 'use client'; import { createGraphQLPage } from '@fumadocs/graphql/ui'; import { Operation } from '@/components/graphql/operation'; import { TypeDocs } from '@/components/graphql/type-docs'; export const GraphQLPage = createGraphQLPage({ components: { Operation, TypeDocs, }, }); ``` # Fumadocs (Framework Mode): Headless URL: /docs/integrations/graphql/headless Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/graphql/headless.mdx The state and logic of GraphQL pages, without UI. ## Overview [#overview] Everything outside `@fumadocs/graphql/ui` is headless. | Path | Module | | ------------------------------ | -------------------------------------------------------------------- | | `@fumadocs/graphql` | the page: the schema built from SDL, cross-links and your components | | `@fumadocs/graphql/operation` | an operation: its field, title, directives and generated example | | `@fumadocs/graphql/type-docs` | a named type: its kind, directives, relations and usages | | `@fumadocs/graphql/playground` | the fetcher, form model and stored state of the playground | ## Page [#page] `createGraphQLRenderer()` takes your components and returns `<GraphQLPage />`, which accepts the props of generated pages. ```tsx title="components/api-page.tsx" 'use client'; import { type CodeBlockProps, createGraphQLRenderer } from '@fumadocs/graphql'; import { Operation } from '@/components/my-operation'; import { TypeDocs } from '@/components/my-type'; export const GraphQLPage = createGraphQLRenderer({ components: { Operation, TypeDocs, SchemaUI: ({ client, root }) => <pre>{`${client.name}: ${root.type}`}</pre>, CodeBlock({ lang, code }: CodeBlockProps) { return ( <pre> <code className={`language-${lang}`}>{code}</code> </pre> ); }, }, }); ``` | Component | Renders | | ----------- | ------------------------------------------------------- | | `Operation` | each operation of the page, see [Operation](#operation) | | `TypeDocs` | each named type of the page, see [Type](#type) | | `SchemaUI` | a type, argument or field in detail | | `Layout` | optional, wraps the rendered items | Pass `typeLinks` and `operationLinks` to override the [cross-links](/docs/integrations/graphql/api-page#cross-linking) of pre-generated pages. ### Hooks [#hooks] Components under the page read its state. | Hook | Returns | | ------------------------------ | ------------------------------------------------------- | | `useGraphQL()` | the built `schema`, its `sdl` and the page links | | `useComponents()` | the components passed to the page | | `useTypeLink(name)` | the page URL of a named type, `undefined` when unlinked | | `useOperationLink(kind, name)` | the page URL of an operation | | `useRenderContext()` | the render options of the page, like `playground` | To render operations yourself, mount `<GraphQLProvider sdl={sdl} shiki={shiki} components={...} />` in place of `createGraphQLRenderer()`. ## Operation [#operation] `<OperationProvider />` derives the details of an operation. ```tsx title="components/my-operation.tsx" 'use client'; import { useComponents } from '@fumadocs/graphql'; import { OperationProvider, type PageOperationProps, useOperation, } from '@fumadocs/graphql/operation'; export function Operation({ kind, name }: PageOperationProps) { return ( <OperationProvider kind={kind} name={name}> <Content /> </OperationProvider> ); } function Content() { const { title, field, example } = useOperation(); const { CodeBlock, SchemaUI } = useComponents(); return ( <> <h2>{title}</h2> {field.args.map((arg) => ( <SchemaUI key={arg.name} client={{ name: arg.name }} root={{ type: arg.type }} /> ))} {example && <CodeBlock lang="graphql" code={example.query} />} </> ); } ``` ### OperationInfo | Prop | Type | Description | | ------------ | --------------- | ---------------------------------------------------------------------- | | `kind` | `OperationKind` | | | `name` | `string` | | | `title` | `string` | display title of the operation, e.g. `createOrder` -> `Create Order`. | | `field` | `object` | | | `directives` | `array` | custom directive applications | | `example?` | `object` | a generated example, `undefined` when the operation cannot be sampled. | ## Type [#type] `<TypeProvider />` derives the details of a named type. ```tsx title="components/my-type.tsx" 'use client'; import { useComponents } from '@fumadocs/graphql'; import { type PageTypeProps, TypeProvider, useNamedType } from '@fumadocs/graphql/type-docs'; export function TypeDocs({ name }: PageTypeProps) { return ( <TypeProvider name={name}> <Content /> </TypeProvider> ); } function Content() { const { name, kind, type, relations } = useNamedType(); const { SchemaUI } = useComponents(); return ( <> <h2> {name} ({kind}) </h2> {relations.usages.returnedBy.map((op) => ( <span key={`${op.kind}:${op.name}`}>{op.name}</span> ))} <SchemaUI client={{ name, as: 'body' }} root={{ type }} /> </> ); } ``` ### NamedTypeInfo | Prop | Type | Description | | ------------ | ------------------ | ----------------------------- | | `name` | `string` | | | `kind` | `NamedTypeKind` | | | `type` | `GraphQLNamedType` | | | `directives` | `array` | custom directive applications | | `relations` | `object` | | `relations.usages` is where the type appears across the schema. ### TypeUsages | Prop | Type | Description | | ------------ | ------- | -------------------------------------------------------------------------------------------- | | `returnedBy` | `array` | operations whose return type resolves to this named type (list/non-null wrappers unwrapped). | | `memberOf` | `array` | object/interface/input object fields typed as this named type. | | `inputFor` | `array` | operations with a direct argument of this named type. | | `argumentOf` | `array` | field arguments (on non-root types) typed as this named type. | # Fumadocs (Framework Mode): GraphQL URL: /docs/integrations/graphql Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/graphql/index.mdx Generating docs for GraphQL schema. ## Setup [#setup] Install the required packages. ```npm npm i @fumadocs/graphql graphql ``` ### Generate Styles [#generate-styles] Add the following line: ```css title="Tailwind CSS" @import 'tailwindcss'; @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; /* [!code ++] */ @import '@fumadocs/graphql/css/preset.css'; ``` ### Configure Plugin [#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()`](/docs/integrations/graphql/server) & [`createGraphQLPage()`](/docs/integrations/graphql/api-page) for available options. ### Generate Pages [#generate-pages] Integrate into [Loader API](/docs/headless/source-api/source) 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 ++:7] { docs: docs.toFumadocsSource(), graphql: await graphql.staticSource({ baseDir: 'graphql', baseUrl: '/docs', }), }, { 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 & named type by default, grouped into folders by their kind (e.g. `queries/`, `objects/`). Pass the `baseUrl` of your `loader()` to pre-generate links, so type & operation references are cross-linked automatically. **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... } ``` <Callout type='warn' title="Ensure the migration is complete!"> Run a type check to verify before continuing, e.g. ```npm npm run types:check ``` </Callout> ## Features [#features] The official GraphQL integration supports: - A page per operation (query/mutation/subscription) and named type - Arguments, fields, deprecations, default values, and custom directive callouts - Usage backlinks on type pages (returned by, field of, input for) - Generated example queries/responses & request snippets (cURL, JavaScript) - Interactive playground: highlighted query editor with live validation, a variables form generated from argument types, and custom headers ### Demo [#demo] See the [GraphQL example](/docs/graphql). # Fumadocs (Framework Mode): createGraphQL() URL: /docs/integrations/graphql/server Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/graphql/server.mdx The GraphQL server instance. ## GraphQL Server [#graphql-server] The main config for Fumadocs GraphQL. > It should not be referenced in browser environments. ### `input` [#input] The GraphQL schemas to read from, either: - an array of file paths/URLs to SDL files. - a record of schema id -> input. Record inputs accept SDL file paths/URLs, SDL text, introspection results, `GraphQLSchema` instances, and functions returning them. ```ts tab="Basic" import { createGraphQL } from '@fumadocs/graphql/server'; export const graphql = createGraphQL({ input: ['./schema.graphql'], }); ``` ```ts tab="Record" import { createGraphQL } from '@fumadocs/graphql/server'; export const graphql = createGraphQL({ input: { // [id]: schema input store: './store.graphql', remote: 'https://example.com/schema.graphql', // functions, e.g. downloading an introspection result galaxy: async () => { const res = await fetch('https://example.com/introspection.json'); return res.json(); }, }, }); ``` You can pass an array of SDL files to merge them into a single schema, type extensions (e.g. `extend type Query`) are supported. ```ts import { createGraphQL } from '@fumadocs/graphql/server'; export const graphql = createGraphQL({ input: { api: ['./base.graphql', './orders.graphql'], }, }); ``` ### `disableCache` [#disablecache] Disable caching of loaded schemas. Useful during development when schemas change frequently. ```ts import { createGraphQL } from '@fumadocs/graphql/server'; export const graphql = createGraphQL({ input: ['./schema.graphql'], disableCache: true, }); ``` ## Generating Pages [#generating-pages] `staticSource()` and `dynamicSource()` generate pages for your `loader()`, they share the same options. ```ts title="lib/source.ts" export const source = loader( { docs: docs.toFumadocsSource(), graphql: await graphql.staticSource({ baseDir: 'graphql', baseUrl: '/docs', }), }, // ... ); ``` ### `per` [#per] Customize the content of pages, default to `item`. | mode | content | file path | | ------ | --------------------------- | ----------------- | | item | each operation & named type | `{name}.mdx` | | file | everything in same schema | `{file_name}.mdx` | | custom | see below | N/A | When set to `custom`, you can pass a function to fully customize the generation process: ```ts import { graphql } from '@/lib/graphql'; await graphql.staticSource({ per: 'custom', toPages(builder) { builder.create({ type: 'operation', path: 'my-orders.mdx', info: { title: 'My Orders' }, item: { kind: 'query', name: 'orders' }, }); }, }); ``` ### `groupBy` [#groupby] For `per: 'item'`, customize how generated pages are grouped into folders, default to `kind`. | mode | folders | | ---- | ------------------------------------------ | | kind | by their kind, e.g. `queries/`, `objects/` | | none | no folders | You can also pass a function that returns the group name of an entry: ```ts import { graphql } from '@/lib/graphql'; await graphql.staticSource({ groupBy: (entry) => (entry.type === 'operation' ? 'operations' : 'types'), }); ``` ### `includeOperations` & `includeTypes` [#includeoperations--includetypes] For `per: 'item'`, filter the operations (query/mutation/subscription fields) and named types to generate pages for, default to `true`. ```ts import { graphql } from '@/lib/graphql'; await graphql.staticSource({ // skip subscriptions includeOperations: (kind, field) => kind !== 'subscription', // skip internal types includeTypes: (type) => !type.name.startsWith('Internal'), }); ``` ### `name` & `slugify` [#name--slugify] Customize the file name of generated pages. GraphQL names are already URL-safe, hence the original name (including its case) is kept by default. ```ts import { graphql } from '@/lib/graphql'; await graphql.staticSource({ name: (entry) => entry.item.name.toLowerCase(), }); ``` `slugify` converts group names (and item names by default) into URL-friendly segments. ### `baseUrl` [#baseurl] The `baseUrl` of your `loader()`. When specified, links of generated pages are pre-generated and passed to the UI, so type & operation references are cross-linked automatically. ```ts import { graphql } from '@/lib/graphql'; await graphql.staticSource({ baseUrl: '/docs', }); ``` ### `meta` [#meta] Generate `meta.json` files to customize the page tree, disabled by default. | `folderStyle` | page tree | | ------------- | ------------------------------------------ | | folder | a folder per group (default) | | separator | a separator per group, with pages under it | ```ts import { graphql } from '@/lib/graphql'; await graphql.staticSource({ meta: { folderStyle: 'separator' }, }); ``` ## Loader Plugin [#loader-plugin] `loaderPlugin()` decorates GraphQL pages in your page tree: - a `query`/`mutation`/`subscription` badge per operation page. - a strikethrough on deprecated operations. ```ts title="lib/source.ts" export const source = loader( // ... { baseUrl: '/docs', plugins: [graphql.loaderPlugin()], }, ); ``` # Fumadocs (Framework Mode): AI & LLMs URL: /docs/integrations/llms Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/llms.mdx Integrate AI functionality to Fumadocs. ## Docs for LLM [#docs-for-llm] Serve your docs as Markdown for LLMs and AI agents: ```package-install npx @fumadocs/cli feature llms ``` It enables processed Markdown on your docs collection, adds `docsLlms` to `lib/source.ts`, and creates the routes below. The generated files are listed here if you prefer a manual setup. ### `docsLlms` [#docsllms] `llms()` renders your docs for LLMs. `renderPage` turns one page into Markdown, from the processed document rather than the raw file content: ```ts title="lib/source.ts" import { llms } from 'fumadocs-core/source'; export const docsLlms = llms(source, { renderPage: async (page) => `# ${page.data.title} (${page.url}) ${await page.data.getText('processed')}`, }); ``` | Method | Output | | -------------- | ---------------------------------------------- | | `index(lang?)` | the `llms.txt` index, built from the page tree | | `page(page)` | one page, rendered with `renderPage` | | `full(lang?)` | every page rendered with `renderPage`, joined | <Callout title="renderPage is required"> `page()` and `full()` are only available when you pass `renderPage`, since Fumadocs cannot know how your content source exposes its Markdown. </Callout> Runtime content sources like [`@fumadocs/local-md`](/docs/integrations/content/local-md) resolve on demand. Pass their `getSource` and the CLI generates the same routes: ```ts export const docsLlms = llms(getSource, { renderPage: (page) => `# ${page.data.title} (${page.url}) ${page.data.content}`, }); ``` With `getSource`, `index()` and `indexNode()` return a promise instead of a string. It requires `includeProcessedMarkdown` in **Fumadocs MDX**: ```ts title="source.config.ts" import { defineDocs } from 'fumadocs-mdx/config'; export const docs = defineDocs({ dir: 'content/docs', docs: { // [!code ++:5] postprocess: { includeProcessedMarkdown: true, }, }, }); ``` MDX components appear as JSX syntax by default, you can render them into meaningful Markdown with the [`output` option](/docs/headless/mdx/remark-llms#output). ### `llms.txt` [#llmstxt] An index of all pages, generated from the page tree. ```ts tab="Next.js" title="app/llms.txt/route.ts" import { docsLlms } from '@/lib/source'; export const revalidate = false; export async function GET() { return new Response(await docsLlms.index()); } ``` ```ts tab="React Router" title="app/routes.ts" import { index, route, type RouteConfig } from '@react-router/dev/routes'; export default [ index('routes/home.tsx'), route('llms.txt', 'routes/llms.txt.ts'), // [!code ++:3] route('llms-full.txt', 'routes/llms-full.txt.ts'), route('llms.mdx/docs/*', 'routes/llms.mdx.docs.ts'), ] satisfies RouteConfig; ``` ```ts tab="React Router" title="app/routes/llms.txt.ts" import { docsLlms } from '@/lib/source'; export async function loader() { return new Response(await docsLlms.index()); } ``` ```ts tab="Tanstack Start" title="src/routes/llms[.]txt.ts" import { createFileRoute } from '@tanstack/react-router'; import { docsLlms } from '@/lib/source'; export const Route = createFileRoute('/llms.txt')({ server: { handlers: { GET: async () => new Response(await docsLlms.index()), }, }, }); ``` ```ts tab="Waku" title="pages/_api/llms.txt.ts" import { docsLlms } from '@/lib/source'; export async function GET() { return new Response(await docsLlms.index()); } export async function getConfig() { return { render: 'static' as const, } as const; } ``` ### `llms-full.txt` [#llms-fulltxt] The content of all pages in a single file. ```ts tab="Next.js" title="app/llms-full.txt/route.ts" import { docsLlms } from '@/lib/source'; export const revalidate = false; export async function GET() { return new Response(await docsLlms.full()); } ``` ```ts tab="React Router" title="app/routes/llms-full.txt.ts" import { docsLlms } from '@/lib/source'; export async function loader() { return new Response(await docsLlms.full()); } ``` ```ts tab="Tanstack Start" title="src/routes/llms-full[.]txt.ts" import { createFileRoute } from '@tanstack/react-router'; import { docsLlms } from '@/lib/source'; export const Route = createFileRoute('/llms-full.txt')({ server: { handlers: { GET: async () => new Response(await docsLlms.full()), }, }, }); ``` ```ts tab="Waku" title="pages/_api/llms-full.txt.ts" import { docsLlms } from '@/lib/source'; export async function GET() { return new Response(await docsLlms.full()); } export async function getConfig() { return { render: 'static' as const, } as const; } ``` ### `*.md` [#md-extension] The Markdown of a single page, for AI agents. ```ts tab="Next.js" title="lib/shared.ts" import { createGetUrl } from 'fumadocs-core/source'; export const docsContentRoute = '/llms.mdx/docs'; const getContentUrl = createGetUrl(docsContentRoute); export function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) { const segments = [...page.slugs, 'content.md']; return { segments, url: getContentUrl(segments, page.locale) }; } ``` ```ts tab="Next.js" title="app/llms.mdx/docs/[[...slug]]/route.ts" import { docsLlms, source } from '@/lib/source'; import { notFound } from 'next/navigation'; export const revalidate = false; export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) { const { slug } = await params; // remove the appended "content.md", `/docs/index.md` is rewritten to the root page const slugs = slug?.slice(0, -1) ?? []; if (slugs.at(-1) === 'index') slugs.pop(); const page = source.getPage(slugs); if (!page) notFound(); return new Response(await docsLlms.page(page), { headers: { 'Content-Type': 'text/markdown', }, }); } export function generateStaticParams() { return source.generateParams().map((item) => ({ ...item, slug: [...item.slug, 'content.md'], })); } ``` ```ts tab="Next.js" title="next.config.ts" import type { NextConfig } from 'next'; const config: NextConfig = { reactStrictMode: true, async rewrites() { // [!code ++:8] return [ { source: '/docs/:slug*.md', destination: '/llms.mdx/docs/:slug*/content.md', }, ]; }, }; export default config; ``` ```ts tab="React Router" title="app/lib/shared.ts" import { createGetUrl } from 'fumadocs-core/source'; export const docsContentRoute = '/llms.mdx/docs'; const getContentUrl = createGetUrl(docsContentRoute); export function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) { const segments = [...page.slugs, 'content.md']; return { segments, url: getContentUrl(segments, page.locale) }; } ``` ```ts tab="React Router" title="app/routes/llms.mdx.docs.ts" import type { Route } from './+types/llms.mdx.docs'; import { docsLlms, source } from '@/lib/source'; export async function loader({ params }: Route.LoaderArgs) { const slugs = params['*'].split('/').filter((v) => v.length > 0); // remove the appended "content.md" slugs.pop(); const page = source.getPage(slugs); if (!page) return new Response('not found', { status: 404 }); return new Response(await docsLlms.page(page), { headers: { 'Content-Type': 'text/markdown', }, }); } ``` ```ts tab="React Router" title="app/root.tsx" import { rewritePath } from 'fumadocs-core/negotiation'; import type { Route } from './+types/root'; // serve `/docs/page.md` [!code ++:11] const { rewrite: rewriteLLM } = rewritePath( '/docs{/*path}.md', '/llms.mdx/docs{/*path}/content.md', ); const serverMiddleware: Route.MiddlewareFunction = async ({ request }, next) => { const url = new URL(request.url); const path = rewriteLLM(url.pathname); if (path) return Response.redirect(new URL(path, url)); return next(); }; export const middleware = [serverMiddleware]; ``` ```ts tab="React Router" title="react-router.config.ts" import type { Config } from '@react-router/dev/config'; export default { // enable middleware [!code ++:3] future: { v8_middleware: true, }, } satisfies Config; ``` ```ts tab="Tanstack Start" title="src/lib/shared.ts" import { createGetUrl } from 'fumadocs-core/source'; export const docsRoute = '/docs'; const getDocsUrl = createGetUrl(docsRoute); export function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) { const segments = [...page.slugs]; if (segments.length === 0) { segments.push('index.md'); } else { segments[segments.length - 1] += '.md'; } return { segments, url: getDocsUrl(segments, page.locale) }; } /** @returns page slugs */ export function decodeMarkdownUrl(segments: string[]) { if (segments.length === 0) return []; const out = [...segments]; out[out.length - 1] = out[out.length - 1].replace(/\.md$/, ''); if (out.length === 1 && out[0] === 'index') out.pop(); return out; } ``` ```ts tab="Tanstack Start" title="src/routes/docs/{$}[.]md.ts" import { createFileRoute, notFound } from '@tanstack/react-router'; import { decodeMarkdownUrl } from '@/lib/shared'; import { docsLlms, source } from '@/lib/source'; export const Route = createFileRoute('/docs/{$}.md')({ server: { handlers: { GET: async ({ params }) => { const slugs = decodeMarkdownUrl(params._splat?.split('/') ?? []); const page = source.getPage(slugs); if (!page) throw notFound(); return new Response(await docsLlms.page(page), { headers: { 'Content-Type': 'text/markdown', }, }); }, }, }, }); ``` ```ts tab="Waku" title="lib/shared.ts" import { createGetUrl } from 'fumadocs-core/source'; export const docsContentRoute = '/llms.mdx/docs'; const getContentUrl = createGetUrl(docsContentRoute); export function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) { const segments = [...page.slugs, 'content.md']; return { segments, url: getContentUrl(segments, page.locale) }; } ``` ```ts tab="Waku" title="pages/_api/llms.mdx/docs/[...slug]/content.md.ts" import { docsLlms, source } from '@/lib/source'; import type { ApiContext } from 'waku/router'; import { unstable_notFound } from 'waku/router/server'; export async function GET(_: Request, { params }: ApiContext<'/llms.mdx/docs/[...slug]/content.md'>) { const page = source.getPage(params.slug); if (!page) unstable_notFound(); return new Response(await docsLlms.page(page), { headers: { 'Content-Type': 'text/markdown', }, }); } export async function getConfig() { return { render: 'static' as const, staticPaths: source.generateParams().map((item) => item.slug), } as const; } ``` #### `Accept` [#accept] To serve the Markdown content instead for AI agents, you can leverage the `Accept` header. ```ts title="proxy.ts (Next.js)" import { NextRequest, NextResponse } from 'next/server'; import { isMarkdownPreferred, rewritePath } from 'fumadocs-core/negotiation'; const { rewrite: rewriteLLM } = rewritePath('/docs{/*path}', '/llms.mdx/docs{/*path}'); export default function proxy(request: NextRequest) { if (isMarkdownPreferred(request)) { const result = rewriteLLM(request.nextUrl.pathname); if (result) { return NextResponse.rewrite(new URL(result, request.nextUrl), { headers: { Vary: 'Accept' }, // [!code highlight] }); } } return NextResponse.next(); } ``` Because the same URL now has two representations, the response needs `Vary: Accept` to prevent cache collisions. <Callout title="Next.js"> Next.js discards `Vary` on App Router **page** responses, so the HTML side of the branch above can't carry the header from inside the app. Set it at your CDN if you serve documentation through a shared cache. </Callout> ### Page Actions [#page-actions] Common page actions for AI, require [`*.md`](#md-extension) to be implemented first. ![AI Page Actions](/docs/ai-page-actions.png) Use them in your docs page like: ```tsx title="app/docs/[[...slug]]/page.tsx" import { MarkdownCopyButton, ViewOptionsPopover } from 'fumadocs-ui/layouts/docs/page'; import { getPageMarkdownUrl } from '@/lib/shared'; const markdownUrl = getPageMarkdownUrl(page).url; <div className="flex flex-row gap-2 items-center border-b pt-2 pb-6"> <MarkdownCopyButton markdownUrl={markdownUrl} /> <ViewOptionsPopover markdownUrl={markdownUrl} githubUrl={`https://github.com/${owner}/${repo}/blob/main/content/docs/${page.path}`} /> </div>; ``` ## MCP Server [#mcp-server] Expose your docs to AI agents through a [MCP](https://modelcontextprotocol.io) server, with tools to list, search and read pages. ```package-install npx @fumadocs/cli feature mcp ``` It creates a `/api/mcp` route with the streamable HTTP transport, on top of the [LLM routes](#docs-for-llm) above. The tools come from [`fumadocs-core/mcp`](/docs/headless/utils/mcp), see linked docs for details. ```ts tab="Next.js" title="app/api/mcp/route.ts" import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp'; import { createFromSource } from 'fumadocs-core/search/server'; import { docsLlms, source } from '@/lib/source'; const handler = createMcpHandler(() => { const mcp = new McpServer({ name: 'docs', version: '1.0.0', }); registerSourceTools(mcp, source, docsLlms); registerSearchTool(mcp, createFromSource(source)); return mcp; }); export async function GET(request: Request) { return handler.fetch(request); } export async function POST(request: Request) { return handler.fetch(request); } export async function DELETE(request: Request) { return handler.fetch(request); } ``` ```ts tab="React Router" title="app/routes/api.mcp.ts" import type { Route } from './+types/api.mcp'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp'; import { createFromSource } from 'fumadocs-core/search/server'; import { docsLlms, source } from '@/lib/source'; const handler = createMcpHandler(() => { const mcp = new McpServer({ name: 'docs', version: '1.0.0', }); registerSourceTools(mcp, source, docsLlms); registerSearchTool(mcp, createFromSource(source)); return mcp; }); export async function loader({ request }: Route.LoaderArgs) { return handler.fetch(request); } export async function action({ request }: Route.ActionArgs) { return handler.fetch(request); } ``` ```ts tab="Tanstack Start" title="src/routes/api/mcp.ts" import { createFileRoute } from '@tanstack/react-router'; import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp'; import { createFromSource } from 'fumadocs-core/search/server'; import { docsLlms, source } from '@/lib/source'; const handler = createMcpHandler(() => { const mcp = new McpServer({ name: 'docs', version: '1.0.0', }); registerSourceTools(mcp, source, docsLlms); registerSearchTool(mcp, createFromSource(source)); return mcp; }); export const Route = createFileRoute('/api/mcp')({ server: { handlers: { GET: async ({ request }) => handler.fetch(request), POST: async ({ request }) => handler.fetch(request), DELETE: async ({ request }) => handler.fetch(request), }, }, }); ``` ```ts tab="Waku" title="pages/_api/api/mcp.ts" import { createMcpHandler, McpServer } from '@modelcontextprotocol/server'; import { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp'; import { createFromSource } from 'fumadocs-core/search/server'; import { docsLlms, source } from '@/lib/source'; const handler = createMcpHandler(() => { const mcp = new McpServer({ name: 'docs', version: '1.0.0', }); registerSourceTools(mcp, source, docsLlms); registerSearchTool(mcp, createFromSource(source)); return mcp; }); export async function GET(request: Request) { return handler.fetch(request); } export async function POST(request: Request) { return handler.fetch(request); } export async function DELETE(request: Request) { return handler.fetch(request); } ``` Connect an agent to it with: ```json { "mcpServers": { "docs": { "url": "https://your-site.com/api/mcp" } } } ``` ## WebMCP [#webmcp] <Callout type="warn" title="Experimental"> [WebMCP](https://webmachinelearning.github.io/webmcp/) is an early web standard, available in Chrome 149+ behind the `#enable-webmcp-testing` flag and an origin trial. The API may still change. </Callout> Where the MCP server above serves agents connecting to your site, WebMCP exposes tools to the AI agent of the browser, on the page the reader is viewing. ```package-install npx @fumadocs/cli feature webmcp ``` It creates a `WebMCP` component on top of the [LLM routes](#docs-for-llm), you can render it inside `<RootProvider />`. - `search_docs` queries the search API route, you can use [other clients](/docs/search) than `fetchClient`. - `read_page` fetches the Markdown of a page, `/docs/page.md` on TanStack Start. ## Ask AI [#ask-ai] ![AI Search](/docs/ai-search.png) You can install the AI chat dialog using Fumadocs CLI. <Tabs items={['AI SDK', 'LLMGateway', 'Inkeep AI']}> <Tab> ```npm npx @fumadocs/cli add ai/openrouter ``` It's automatically configured for [OpenRouter](https://openrouter.ai) using Vercel AI SDK, with a `/search` tool for AI. You can use other models by updating the `/api/chat` route. > Fumadocs doesn't provide the AI model, it's up to you. > > Your AI model can use the `llms-full.txt` file generated above, or more diversified sources of information when combined with 3rd party solutions. </Tab> <Tab> ```npm npx @fumadocs/cli add ai/llmgateway ``` It's automatically configured for [LLMGateway](https://llmgateway.io) using Vercel AI SDK, with a `/search` tool for AI. LLMGateway is an open-source API gateway that proxies requests to 300+ language models, so the same route handler can switch between providers by changing the model ID. Add your LLMGateway API key to environment variables: ```dotenv LLM_GATEWAY_API_KEY="..." ``` By default the route uses `anthropic/claude-3.5-sonnet`. To switch to any other model supported by LLMGateway, set the `LLM_GATEWAY_MODEL` environment variable—no code change required: ```dotenv # any model id from https://llmgateway.io/models LLM_GATEWAY_MODEL="openai/gpt-4o" ``` </Tab> <Tab> ```npm npx @fumadocs/cli add ai/inkeep ``` It's automatically configured for [Inkeep AI](https://inkeep.com) using Vercel AI SDK. Add your Inkeep API key to environment variables: ```dotenv INKEEP_API_KEY="..." ``` </Tab> </Tabs> Docs layouts render the chat from their `aiChat` option, as it takes client state, create a client component for your layout, and use it in place of the original one: ```tsx tab="Docs & Notebook" title="components/ai/layout.tsx" 'use client'; // or `fumadocs-ui/layouts/notebook` import { DocsLayout as Layout, type DocsLayoutProps } from 'fumadocs-ui/layouts/docs'; // the installed components import { AIChat, AIChatPanel, AIChatTrigger, useAIChat } from './search'; export function DocsLayout(props: DocsLayoutProps) { return ( <AIChat> <ChatLayout {...props} /> <AIChatTrigger /> </AIChat> ); } function ChatLayout(props: DocsLayoutProps) { const { open, setOpen } = useAIChat(); return <Layout {...props} aiChat={{ open, onOpenChange: setOpen, panel: <AIChatPanel /> }} />; } ``` ```tsx tab="Docs & Notebook" title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/docs'; // [!code --] import { DocsLayout } from '@/components/ai/layout'; // [!code ++] ``` ```tsx tab="Glass & Spacious" title="components/ai/layout.tsx" 'use client'; // or `fumadocs-ui/layouts/spacious` import { DocsLayout as Layout, type DocsLayoutProps } from 'fumadocs-ui/layouts/glass'; // the installed components import { AIChat, AIChatPanel, useAIChat } from './search'; // the layout has its own Ask AI button export function DocsLayout(props: DocsLayoutProps) { return ( <AIChat> <ChatLayout {...props} /> </AIChat> ); } function ChatLayout(props: DocsLayoutProps) { const { open, setOpen } = useAIChat(); return <Layout {...props} aiChat={{ open, onOpenChange: setOpen, panel: <AIChatPanel /> }} />; } ``` ```tsx tab="Glass & Spacious" title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/glass'; // [!code --] import { DocsLayout } from '@/components/ai/layout'; // [!code ++] ``` As the layout options are passed through a client component, they have to be serializable. # Fumadocs (Framework Mode): OG Image Generation URL: /docs/integrations/og Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/og/index.mdx Generate dynamic preview image for docs pages You can configure [Open Graph](https://ogp.me) image metadata, which defines the preview image of docs pages in website/social media, such as: <div className='mx-auto w-full max-w-[400px]'> ![preview](./preview.png) </div> # Fumadocs (Framework Mode): next/og URL: /docs/integrations/og/next Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/og/next.mdx Usage with Next.js Metadata API. > Make sure to read their [Metadata section](https://nextjs.org/docs/app/building-your-application/optimizing/metadata) for the fundamentals of Metadata API. ## Metadata Image [#metadata-image] You can generate metadata images dynamically using `next/og`. Add the following under your loader, and define image metadata for pages: ```ts tab="lib/source.ts" // [!code ++:8] export function getPageImageUrl(page: (typeof source)['$inferPage']) { const segments = [...page.slugs, 'image.png']; return { segments, url: '/' + [page.locale, 'og', 'docs', ...segments].filter(Boolean).join('/'), }; } ``` ```tsx tab="app/docs/[[...slug]]/page.tsx" import { notFound } from 'next/navigation'; import { source, getPageImageUrl } from '@/lib/source'; import type { Metadata } from 'next'; export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> { const params = await props.params; const page = source.getPage(params.slug); if (!page) notFound(); return { title: page.data.title, description: page.data.description, openGraph: { // [!code ++] images: getPageImageUrl(page).url, }, }; } ``` > We append `image.png` to the end of slugs so that we can access it via `/og/docs/my-page/image.png`. Finally, create a route handler to generate images at build time: ```tsx title="app/og/docs/[...slug]/route.tsx" import { source } from '@/lib/source'; import { notFound } from 'next/navigation'; import { generateOGImage } from 'fumadocs-ui/og'; import { appName, getPageImageUrl } from '@/lib/shared'; export const revalidate = false; export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) { const { slug } = await params; const page = source.getPage(slug.slice(0, -1)); if (!page) notFound(); return generateOGImage({ title: page.data.title, description: page.data.description, site: appName, }); } export function generateStaticParams() { return source.getPages().map((page) => ({ lang: page.locale, slug: getPageImageUrl(page).segments, })); } ``` You can specify options for Satori (used by `next/og`), see https://github.com/vercel/satori for reference. ### Other Presets [#other-presets] There's other available styles on Fumadocs CLI, such as `mono`: ```npm npx @fumadocs/cli@latest add og/mono ``` Replace your old `generate` with the installed one. # Fumadocs (Framework Mode): Takumi URL: /docs/integrations/og/takumi Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/og/takumi.mdx Integrate Takumi for framework agnostic and fast metadata image generation. ## Installation [#installation] ```npm npm install takumi-js ``` `takumi-js` loads a native binding (`@takumi-rs/core`) on Node.js. Keep it out of the server bundle for Next.js by marking it external: ```ts title="next.config.ts" tab="Next.js" import type { NextConfig } from 'next'; const config: NextConfig = { // [!code ++] serverExternalPackages: ['@takumi-rs/core'], }; ``` There are no actions needed for Vite-based frameworks. ## Metadata Image [#metadata-image] Generate metadata images dynamically with `takumi-js`. Add the following under your loader, and define image metadata for pages: ```ts tab="Next.js" title="lib/source.ts" // [!code ++:8] export function getPageImageUrl(page: (typeof source)['$inferPage']) { const segments = [...page.slugs, 'image.webp']; return { segments, url: '/' + [page.locale, 'og', 'docs', ...segments].filter(Boolean).join('/'), }; } ``` ```tsx tab="Next.js" title="app/docs/[[...slug]]/page.tsx" import { notFound } from 'next/navigation'; import { source, getPageImageUrl } from '@/lib/source'; import type { Metadata } from 'next'; export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> { const params = await props.params; const page = source.getPage(params.slug); if (!page) notFound(); return { title: page.data.title, description: page.data.description, openGraph: { // [!code ++] images: getPageImageUrl(page).url, }, }; } ``` ```tsx tab="React Router" title="app/lib/shared.ts" export const docsImageRoute = '/og/docs'; // [!code ++:5] export function getPageImagePath(slugs: string[], locale?: string) { return ( '/' + [locale, ...docsImageRoute.split('/'), ...slugs, 'image.webp'].filter(Boolean).join('/') ); } ``` ```tsx tab="React Router" title="app/routes/docs.tsx" // [!code ++] import { getPageImagePath } from '@/lib/shared'; // Somewhere in your loader return { // [!code ++] imagePath: getPageImagePath(page.slugs, page.locale), }; // Somewhere in your component or client loader return ( <> {/* [!code ++] */} <meta property="og:image" content={imagePath} /> </> ); ``` ```ts tab="TanStack Start" title="src/lib/shared.ts" import { createGetUrl } from 'fumadocs-core/source'; // [!code ++:7] const getImageUrl = createGetUrl('/og/docs'); export function getPageImageUrl(page: { slugs: string[]; locale?: string }) { const segments = [...page.slugs, 'image.webp']; return { segments, url: getImageUrl(segments, page.locale) }; } ``` ```tsx tab="TanStack Start" title="src/routes/docs/$.tsx" // [!code ++] import { getPageImageUrl } from '@/lib/shared'; export const Route = createFileRoute('/docs/$')({ // [!code ++:3] head: ({ loaderData }) => ({ meta: loaderData ? [{ property: 'og:image', content: loaderData.imageUrl }] : [], }), }); // Somewhere in your server function return { // [!code ++] imageUrl: getPageImageUrl(page).url, }; ``` ```tsx tab="Vite Based Framework" title="app/lib/source.ts" // [!code ++:5] export function getPageImageUrl(page: (typeof source)['$inferPage']) { const segments = [...page.slugs, 'image.webp']; return '/' + [page.locale, 'og', 'docs', ...segments].filter(Boolean).join('/'); } ``` ```tsx tab="Vite Based Framework" title="app/routes/docs.tsx" // [!code ++] import { getPageImageUrl } from '@/lib/source'; // Somewhere in your component or client loader return ( <> {/* [!code ++] */} <meta property="og:image" content={getPageImageUrl(page)} /> </> ); ``` > We append `image.webp` to the end of slugs so that we can access it via `/og/docs/my-page/image.webp`, which results in smaller image sizes. You could use `image.png` if you prefer. Finally, create a route handler to generate images: ```tsx tab="Next.js" title="app/og/docs/[...slug]/route.tsx" import { getPageImageUrl, source } from '@/lib/source'; import { notFound } from 'next/navigation'; import { generateOGImage } from 'fumadocs-ui/og/takumi'; export const revalidate = false; export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) { const { slug } = await params; const page = source.getPage(slug.slice(0, -1)); if (!page) notFound(); return generateOGImage({ title: page.data.title, description: page.data.description, site: 'My App', format: 'webp', }); } export function generateStaticParams() { return source.getPages().map((page) => ({ lang: page.locale, slug: getPageImageUrl(page).segments, })); } ``` ```tsx tab="React Router" title="app/routes/docs.og.tsx" import { source } from '@/lib/source'; import { generateOGImage } from 'fumadocs-ui/og/takumi'; import type { Route } from './+types/docs.og'; export function loader({ params }: Route.LoaderArgs) { const slugs = params['*'] .split('/') .filter((v) => v.length > 0) .slice(0, -1); const page = source.getPage(slugs); if (!page) throw new Response(undefined, { status: 404 }); return generateOGImage({ title: page.data.title, description: page.data.description, site: 'My App', format: 'webp', }); } ``` ```tsx tab="Waku" title="src/pages/_api/og/docs/[...slugs]/image.webp.tsx" import { source } from '@/lib/source'; import { generateOGImage } from 'fumadocs-ui/og/takumi'; import { ApiContext } from 'waku/router'; export async function GET(_: Request, { params }: ApiContext<'/og/docs/[...slugs]/image.webp'>) { const page = source.getPage(params.slugs); if (!page) return new Response(undefined, { status: 404 }); return generateOGImage({ title: page.data.title, description: page.data.description, site: 'My App', format: 'webp', }); } export async function getConfig() { const pages = source .generateParams() .map((item) => (item.lang ? [item.lang, ...item.slug] : item.slug)); return { render: 'static' as const, staticPaths: pages, } as const; } ``` ```ts tab="TanStack Start" title="src/routes/og/docs/$.ts" import { createFileRoute } from '@tanstack/react-router'; import { generateOGImage } from 'fumadocs-ui/og/takumi'; import { source } from '@/lib/source'; export const Route = createFileRoute('/og/docs/$')({ server: { handlers: { GET: ({ params }) => { const slugs = (params._splat?.split('/') ?? []).filter((v) => v.length > 0).slice(0, -1); const page = source.getPage(slugs); if (!page) return new Response(undefined, { status: 404 }); return generateOGImage({ title: page.data.title, description: page.data.description, site: 'My App', format: 'webp', }); }, }, }, }); ``` ```ts tab="TanStack Start" title="vite.config.ts" // SPA mode only: TanStack Start pre-renders responses as text, which breaks images import { defineConfig } from 'vite'; import { nitro } from 'nitro/vite'; // [!code ++:3] import { glob } from 'node:fs/promises'; import { getSlugs } from 'fumadocs-core/source'; import { getPageImageUrl } from './src/lib/shared'; // [!code ++:4] const imageRoutes: string[] = []; for await (const entry of glob('**/*.mdx', { cwd: 'content/docs' })) { imageRoutes.push(getPageImageUrl({ slugs: getSlugs(entry) }).url); } export default defineConfig({ plugins: [ // other plugins... nitro({ // [!code ++] prerender: { routes: imageRoutes }, }), ], }); ``` ```ts tab="React Router" title="app/routes.ts" import { type RouteConfig, route } from '@react-router/dev/routes'; export default [ // [!code ++] route('/og/docs/*', 'routes/docs.og.tsx'), ] satisfies RouteConfig; ``` ```ts tab="React Router" title="react-router.config.ts" import type { Config } from '@react-router/dev/config'; import { glob } from 'node:fs/promises'; import { createGetUrl, getSlugs } from 'fumadocs-core/source'; import { getPageImagePath } from './app/lib/shared'; const getUrl = createGetUrl('/docs'); export default { ssr: true, future: { v8_middleware: true, }, // [!code ++:12] async prerender({ getStaticPaths }) { const paths = [...getStaticPaths()]; for await (const entry of glob('**/*.mdx', { cwd: 'content/docs' })) { const slugs = getSlugs(entry); paths.push(getUrl(slugs)); paths.push(getPageImagePath(slugs)); } return paths; }, } satisfies Config; ``` > Takumi comes with pre-bundled full-axis (100-900) [`Geist`](https://vercel.com/font) and `Geist Mono` fonts, so you don't need to worry about it. See [Takumi's Documentation](https://takumi.kane.tw/docs) for more details or advanced usage. ### Other Templates [#other-templates] There's other available templates, see [Takumi Templates](https://takumi.kane.tw/docs/templates) for a full list. # Fumadocs (Framework Mode): createOpenAPIPage() URL: /docs/integrations/openapi/api-page Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/openapi/api-page.mdx The component for rendering OpenAPI docs content ## Overview [#overview] Fumadocs OpenAPI uses a `<OpenAPIPage />` component to render page contents, it should be a client component. ```tsx title="components/api-page.tsx" 'use client'; import { createOpenAPIPage } from 'fumadocs-openapi/ui'; export const OpenAPIPage = createOpenAPIPage({ // config }); ``` ### Generate Code Usages [#generate-code-usages] Generate code usage examples for each API endpoint. ```tsx title="components/api-page.tsx" twoslash 'use client'; import { createOpenAPIPage } from 'fumadocs-openapi/ui'; import { createCodeUsageGeneratorRegistry, type CodeUsageGenerator, } from 'fumadocs-openapi/requests/generators'; import { registerDefault } from 'fumadocs-openapi/requests/generators/all'; const codeUsages = createCodeUsageGeneratorRegistry(); // include defaults registerDefault(codeUsages); // add custom generators codeUsages.add('custom-id', { label: 'My Example', lang: 'js', generate(data, { mediaAdapters }) { // request data console.log(data); return 'const response = "hello world";'; }, }); export const OpenAPIPage = createOpenAPIPage({ // [!code ++] codeUsages, }); ``` In addition, you can also specify code samples via OpenAPI schema. ```yaml paths: /plants: get: x-codeSamples: - lang: js label: JavaScript SDK source: | const planter = require('planter'); planter.list({ unwatered: true }); ``` ### Internationalization [#internationalization] Assuming you have configured [Internationalization](/docs/internationalization) at UI level: ```tsx title="layout.shared.tsx" import { defineI18n } from 'fumadocs-core/i18n'; import { uiTranslations } from 'fumadocs-ui/i18n'; import { openapiTranslations } from 'fumadocs-openapi/i18n'; const i18n = defineI18n({ languages: ['en', 'cn'], defaultLanguage: 'en', }); export const translations = i18n .translations() .extend(uiTranslations()) // [!code ++] .extend(openapiTranslations()) .add({ cn: { displayName: 'Chinese', // [!code ++] 'Body(playground)': '請求正文', }, }); ``` See [Translations](/docs/ui/translations) for more details. ## Media Adapters [#media-adapters] You can create a media adapter to support other media types in API pages, a media adapter implements: - Converting value into `fetch()` body compatible with corresponding media type. - Generate code example based on different programming language/tool. Request bodies of other media types are read-only: they are left out of code usages, and cannot be sent from the playground. ```tsx title="components/api-page.tsx" twoslash 'use client'; import { createOpenAPIPage } from 'fumadocs-openapi/ui'; import type { MediaAdapter } from 'fumadocs-openapi'; const mediaAdapters: Record<string, MediaAdapter> = { // example: custom `application/json 'application/json': { encode(data) { return JSON.stringify(data.body); }, // returns code that inits a `body` variable, used for request body generateExample(data, ctx) { if (ctx.lang === 'js') { return `const body = "hello world"`; } if (ctx.lang === 'python') { return `body = "hello world"`; } if (ctx.lang === 'go' && 'addImport' in ctx) { ctx.addImport('strings'); return `body := strings.NewReader("hello world")`; } }, }, }; export const OpenAPIPage = createOpenAPIPage({ // [!code ++] mediaAdapters, }); ``` ## Authorization [#authorization] Auth providers handle the security schemes of the playground: the rows of their values, and how values are sent. ```tsx title="components/api-page.tsx" 'use client'; import { createOpenAPIPage } from 'fumadocs-openapi/ui'; import { Custom } from 'fumadocs-openapi/ui/playground/client'; import type { AuthPanelProps } from 'fumadocs-openapi/playground'; export const OpenAPIPage = createOpenAPIPage({ playground: { authProviders: [ { // a scheme ID or a function, every scheme when omitted on: 'my-auth', render: ({ Value, Panel }) => ( <> <Value name="Authorization" type="header" /> <Panel title="Sign in" component={SignIn} /> </> ), }, ], }, }); function SignIn({ field, close }: AuthPanelProps) { const { setValue } = Custom.useController(field.fieldName); return ( <button onClick={async () => { const res = await fetch('/api/token'); setValue(`Bearer ${await res.text()}`); close(); }} > Sign in </button> ); } ``` | Option | Is | | ----------- | -------------------------------------------------------------------- | | `on` | the security schemes it handles | | `render` | its rows, from `Value` (an input of the value) and `Panel` (a panel) | | `onRequest` | write the value into request data, for requests and code usages | Define providers outside of components, the auth fields are created again when they change. Rows and panels hold their own state, `Custom.useController(field.fieldName)` reads and writes the value. It starts from the default of scheme, like `Bearer `, and is remembered per scheme. <Accordions> <Accordion title="Sign-ins leaving the page"> Leave the page with the function from `useAuthRedirect()`. When the page loads again, the playground reopens, and the rows get the data back in `onReturn`. ```tsx import { Custom } from 'fumadocs-openapi/ui/playground/client'; import { type AuthPanelProps, type AuthRenderProps, useAuthRedirect, } from 'fumadocs-openapi/playground'; function Rows({ field, Value, Panel }: AuthRenderProps) { const { setValue } = Custom.useController(field.fieldName); useAuthRedirect(field, (url, state) => { if (url.searchParams.get('state') === state) setValue(`Bearer ${url.searchParams.get('token')}`); }); return ( <> <Value name="Authorization" type="header" /> <Panel title="Sign in" component={SignIn} /> </> ); } function SignIn({ field }: AuthPanelProps) { const redirect = useAuthRedirect(field); return ( <button onClick={() => { const state = crypto.randomUUID(); redirect(`https://auth.example.com/login?state=${state}`, state); }} > Sign in </button> ); } ``` </Accordion> </Accordions> ## Customise UI [#customise-ui] For customisations beyond the available options, you can install the UI into your codebase with [Fumadocs CLI](/docs/cli). ### Full UI [#full-ui] The entire UI of API pages, built on [the headless layer](/docs/integrations/openapi/headless). ```npm npx @fumadocs/cli add openapi/page ``` It installs `<OpenAPIPage />` itself, 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 ++] ``` ### API Playground [#api-playground] The UI of the playground, its state comes from [`usePlayground()`](/docs/integrations/openapi/headless#playground). ```npm npx @fumadocs/cli add openapi/playground ``` ```tsx title="components/api-page.tsx" 'use client'; import { createOpenAPIPage } from 'fumadocs-openapi/ui'; import PlaygroundClient from '@/components/openapi/playground'; export const OpenAPIPage = createOpenAPIPage({ playground: { render: () => <PlaygroundClient />, }, }); ``` ### Operation [#operation] The UI of operations and webhooks. ```npm npx @fumadocs/cli add openapi/operation ``` ```tsx title="components/api-page.tsx" 'use client'; import { createOpenAPIPage } from 'fumadocs-openapi/ui'; import { Operation } from '@/components/openapi/operation'; export const OpenAPIPage = createOpenAPIPage({ components: { Operation, }, }); ``` ### Schema UI [#schema-ui] The UI of JSON schemas, its state comes from [`<SchemaUIProvider />`](/docs/integrations/openapi/headless#schema-ui). ```npm npx @fumadocs/cli add api-docs/schema ``` ```tsx title="components/api-page.tsx" 'use client'; import { createOpenAPIPage } from 'fumadocs-openapi/ui'; import { Schema } from '@/components/api/schema'; export const OpenAPIPage = createOpenAPIPage({ components: { SchemaUI: Schema, }, }); ``` # Fumadocs (Framework Mode): generateFiles() URL: /docs/integrations/openapi/generate-files Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/openapi/generate-files.mdx Generate pages from OpenAPI schema. The `generateFiles()` function generates MDX files for API documentation. Note that the page content is still rendered by the `APIpage` component, docs generator only arranges the endpoints/webhooks to render for each page. ## Usage [#usage] It takes an OpenAPI instance. ```ts import { generateFiles } from 'fumadocs-openapi'; import { openapi } from '@/lib/openapi'; void generateFiles({ input: openapi, // The output directory. output: '/content/docs', }); ``` ### `per` [#per] Customize the content of pages, default to `operation`. <Callout>Operation refers to an API endpoint with specific method like `/api/weather:GET`.</Callout> | mode | content | file path | | --------- | ------------------------------ | ------------------------------------- | | tag | operations with same tag | `{tag_name}.mdx` | | file | operations in same schema file | `{file_name}.mdx` | | operation | each operation | `{operationId ?? endpoint_path}.mdx`) | | custom | see below | N/A | ```ts import { generateFiles } from 'fumadocs-openapi'; void generateFiles({ per: 'tag', }); ``` When set to `custom`, you can pass a function to fully customize the generation process: ```ts title="Example" import { generateFiles, OperationOutput } from 'fumadocs-openapi'; void generateFiles({ per: 'custom', toPages(builder) { const items = builder.extract(); for (const op of items.operations) { const { pathItem, operation, displayName } = builder.fromExtractedOperation(op)!; const entry: OperationOutput = { type: 'operation', schemaId: builder.id, item: op, path: '...', info: { title: displayName, description: operation.description ?? pathItem.description, }, }; builder.create(entry); } }, }); ``` When set to `tag`, adding `x-displayName` to the tag definition can control the title of each page. ```yaml title="openapi.yaml" tags: - name: test description: this is a tag. x-displayName: My Test Name ``` ### `groupBy` [#groupby] In `operation` mode, you can group output files with folders. | value | output | | -------------- | ------------------------------------------------------ | | tag | `{tag}/{operationId ?? endpoint_path}.mdx` | | route | `{endpoint_path}/{method}.mdx` (ignores `name` option) | | none (default) | `{operationId ?? endpoint_path}.mdx` | ```ts import { generateFiles } from 'fumadocs-openapi'; void generateFiles({ per: 'operation', groupBy: 'tag', }); ``` When grouping by `tag`, it follows the tag hierarchy of OpenAPI 3.2: a tag is nested into the folder of its `parent` tag, while tags with a `kind` other than `nav` are ignored. ```yaml title="openapi.yaml" tags: - name: store summary: Store kind: nav - name: products parent: store paths: /products: get: # output: store/products/listProducts.mdx operationId: listProducts tags: [products] ``` Operations without tags are grouped under an `unknown` folder. ### `index` [#index] Generate index files with cards linking to generated pages. ```ts import { generateFiles } from 'fumadocs-openapi'; void generateFiles({ index: { // for generating `href` url: { baseUrl: '/docs', contentDir: './content/docs', }, items: [ { // output path path: 'index.mdx', // optional: restrict the input files (identical to the `input` field in server) only: ['petstore.yaml'], // optional: set frontmatter description: 'All available pages', }, ], }, }); ``` ### `meta` [#meta] Generate [`meta.json`](/docs/page-conventions#meta) files automatically. You can always define the `meta.json` files manually for maximal flexibility. ```ts import { generateFiles } from 'fumadocs-openapi'; void generateFiles({ meta: true, // or customize how folders are represented: meta: { folderStyle: 'separator', }, }); ``` ### `imports` [#imports] Add custom imports to generated MDX files. This is useful for including components, utilities, or other dependencies in your generated documentation. ```ts import { generateFiles } from 'fumadocs-openapi'; void generateFiles({ imports: [ { names: ['API_BASE_URL'], from: '@/constants', }, ], }); ``` This will add the following imports to all generated MDX files: ```ts import { API_BASE_URL } from '@/constants'; ``` ### `name` [#name] A function that controls the output file name of MDX pages. ```ts import { generateFiles } from 'fumadocs-openapi'; void generateFiles({ name(output) { const doc = this.document; if (output.type === 'operation') { const operation = doc.paths![output.item.path]![output.item.method]!; // operation object console.log(operation); return 'my-dir/filename'; } const hook = this.dereferenceShallow(doc.webhooks![output.item.name])[output.item.method]!; // webhook object console.log(hook); return 'my-dir/filename'; }, }); ``` ### `frontmatter` [#frontmatter] Customize the frontmatter of MDX files. By default, it includes: | property | description | | ------------- | ---------------------------------- | | `title` | Page title | | `description` | Page description | | `full` | Always true, added for Fumadocs UI | | `_openapi` | Internal Properties | ```ts import { generateFiles } from 'fumadocs-openapi'; void generateFiles({ frontmatter: (title, description) => ({ myProperty: 'hello', }), }); ``` ### `addGeneratedComment` [#addgeneratedcomment] Add a comment to the top of generated files indicating they are auto-generated. ```ts import { generateFiles } from 'fumadocs-openapi'; void generateFiles({ // Add default comment addGeneratedComment: true, // Or provide a custom comment addGeneratedComment: 'Custom auto-generated comment', // Or disable comments addGeneratedComment: false, }); ``` # Fumadocs (Framework Mode): Headless URL: /docs/integrations/openapi/headless Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/openapi/headless.mdx The state and logic of API pages, without UI. ## Overview [#overview] Everything outside `fumadocs-openapi/ui` is headless. | 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` | an API playground: its form, auth and requests | | `fumadocs-openapi/requests` | encoding the data of an example into a real request | | `fumadocs-openapi/requests/generators` | turning that request into code usages | | `@fumadocs/json-schema/react` | the Schema UI: JSON schemas flattened for UI | | `@fumadocs/json-schema/react/client` | the state of a Schema UI | ## Page [#page] `createOpenAPIRenderer()` takes your components and returns `<OpenAPIPage />`, which accepts the props of generated pages. ```tsx title="components/api-page.tsx" 'use client'; import { type CodeBlockProps, createOpenAPIRenderer } from 'fumadocs-openapi'; import { Operation } from '@/components/my-operation'; import { SchemaUI } from '@/components/my-schema'; export const OpenAPIPage = createOpenAPIRenderer({ components: { Operation, SchemaUI, CodeBlock({ lang, code }: CodeBlockProps) { return ( <pre> <code className={`language-${lang}`}>{code}</code> </pre> ); }, }, }); ``` | Component | Renders | | ----------- | --------------------------------------------------------------------------------- | | `Operation` | each operation and webhook, see [Operation](#operation) | | `SchemaUI` | the JSON schemas of parameters, bodies and responses, see [Schema UI](#schema-ui) | | `Layout` | optional, wraps the rendered operations and webhooks | ### Hooks [#hooks] Components under the page read its state. | Hook | Returns | | ------------------------------------------- | ----------------------------------------------------- | | `useOpenAPI()` | the dereferenced document (`doc`) and request options | | `useComponents()` | the components passed to the page | | `useServer()` | the selected server and its variables | | `useTypeScriptDefinitions(schema, options)` | TypeScript definitions of a JSON schema | | `useRenderContext()` | the render options of the page, like `playground` | ## Operation [#operation] `<OperationProvider />` derives the details of an operation or webhook, and holds its selected example request. ```tsx title="components/my-operation.tsx" 'use client'; import { OperationProvider, type PageOperationProps, useCodeUsage, useOperation, useResponseExamples, } from 'fumadocs-openapi/operation'; export function Operation(props: PageOperationProps) { return ( <OperationProvider {...props}> <Content /> </OperationProvider> ); } function Content() { const { title, parameters, codeUsages } = useOperation(); const responses = useResponseExamples(); return ( <> <h2>{title}</h2> {parameters.map(({ in: location, items }) => ( <section key={location}>{items.map((param) => param.name).join(', ')}</section> ))} {Array.from(codeUsages.map().keys(), (id) => ( <CodeUsage key={id} id={id} /> ))} {responses.map((tab) => ( <pre key={tab.code}>{JSON.stringify(tab.examples?.[0]?.sample)}</pre> ))} </> ); } function CodeUsage({ id }: { id: string }) { const code = useCodeUsage(id); return code && <pre>{code}</pre>; } ``` | Hook | Returns | | ----------------------- | ---------------------------------------------------------------------------------- | | `useOperation()` | the operation with its details resolved | | `useExampleRequests()` | the example requests, the selected one, `select()` and `update()` to edit its data | | `useExampleRequest()` | data of the selected example, following its updates | | `useCodeUsage(id)` | the code generated by `codeUsages` for the selected example | | `useResponseExamples()` | responses with example values of their preferred media type | ### OperationInfo | Prop | Type | Description | | -------------- | ---------------------------- | ------------------------------------------------------------------------ | | `type` | `union` | | | `path` | `string` | | | `method` | `HttpMethods` | | | `operation` | `OperationObject` | | | `pathItem` | `PathItemObject` | | | `title` | `string` | | | `description?` | `string` | | | `requestBody?` | `object` | resolved request body with its media types, `undefined` when it has none | | `parameters` | `array` | resolved parameters grouped by location, empty groups omitted | | `security` | `array` | alternative security requirements, always empty for webhooks | | `responses` | `array` | | | `callbacks` | `array` | callbacks flattened into webhook operations | | `codeUsages` | `CodeUsageGeneratorRegistry` | code usage generators, including the inline code samples of operation | ## Schema UI [#schema-ui] `generateSchemaUI()` flattens a JSON schema: every `$ref` it reaches becomes an entry in `refs`, so your UI walks a map from `$root` instead of recursing. `<SchemaUIProvider />` holds the state on top of it: the schemas opened from properties, the selected members of unions, and links to a property. ```tsx title="components/my-schema.tsx" 'use client'; import { useMemo } from 'react'; import { generateSchemaUI } from '@fumadocs/json-schema/react'; import { SchemaUIProvider, useSchemaUI } from '@fumadocs/json-schema/react/client'; import type { OpenAPIComponents } from 'fumadocs-openapi'; export const SchemaUI: OpenAPIComponents['SchemaUI'] = ({ client, ...options }) => { const generated = useMemo(() => generateSchemaUI(options), [options.root]); return ( <SchemaUIProvider id={client.rootId} name={client.name} generated={generated}> <div id={client.rootId}> <Schema /> </div> </SchemaUIProvider> ); }; function Schema() { const { refs, path, open, back } = useSchemaUI(); const { schema } = path[path.length - 1]; return ( <> {path.length > 1 && <button onClick={() => back(path.length - 2)}>Back</button>} {schema.type === 'object' && ( <ul> {schema.props.map((prop) => ( <li key={prop.name}> {prop.name}:{' '} <button onClick={() => open(prop.name, prop.$type)}> {refs[prop.$type].typeName} </button> </li> ))} </ul> )} </> ); } ``` | `useSchemaUI()` | Is | | --------------------------------- | ---------------------------------------------------------------------------- | | `path` | the opened schemas, starting from the root | | `open(name, $ref)`, `back(index)` | navigate `path` | | `getLink(index, name)` | the URL linking to a property of a path item, visiting it opens `path` again | | `highlighted` | the property of the last path item that the URL links to | Each item of `path` has the `schema` to show, and its `unions` with the selected members. `select()` of a union shows another member. The page passes `renderMarkdown` and `renderCodeblock` in, so descriptions and examples render through your own components. `client` carries the anchor ID and name of the root, links to a property are resolved against it. <Callout title="Render the slots of client"> Bodies and responses pass `client.selector` to select their media type, and `client.actions` to copy their TypeScript definitions. </Callout> <Accordions> <Accordion title="The shape of each entry"> `infoTags` are the labelled values an API page shows next to a field, like `Format` or `Default`. `type` decides how to open it further. ### SchemaData | Prop | Type | Description | | -------------- | ----------- | ----------- | | `description?` | `ReactNode` | | | `infoTags?` | `array` | | | `typeName` | `string` | | | `aliasName` | `string` | | | `deprecated?` | `union` | | | `type` | `union` | | </Accordion> </Accordions> ## Playground [#playground] `usePlayground()` holds the state of an API playground under `<OperationProvider />`. Its form starts from the selected example request, and edits sync back to the example, so code usages follow them. ```tsx title="components/my-playground.tsx" 'use client'; import { StfProvider, useFieldValue } from '@fumari/stf'; import { useOperation } from 'fumadocs-openapi/operation'; import { usePlayground } from 'fumadocs-openapi/playground'; export function MyPlayground() { const { parameters } = useOperation(); const { stf, send, isSending, response } = usePlayground(); return ( <StfProvider value={stf}> {parameters.map(({ in: location, items }) => items.map((param) => ( <ParameterInput key={`${location}.${param.name}`} location={location} name={param.name} /> )), )} <button disabled={isSending} onClick={send}> Send </button> {response?.result.type === 'response' && <pre>{response.result.text()}</pre>} </StfProvider> ); } function ParameterInput({ location, name }: { location: string; name: string }) { const [value, setValue] = useFieldValue([location, name]); return ( <input aria-label={name} value={String(value ?? '')} onChange={(e) => setValue(e.target.value)} /> ); } ``` | Field | Is | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `stf` | the form (a `@fumari/stf` data engine), holding `path`, `query`, `header`, `cookie` and `body` | | `body` | the request body edited in the form, with its preferred media type | | `auth` | the auth fields of the selected security requirement, `select()` switches it, `authorize()` obtains an OAuth 2.0 token into a field | | `send()`, `isSending`, `response`, `clearResponse()` | send the form as a request, and the result of the last one | It accepts `fetchOptions` to customise requests, like `onRequestInit()`, `authProviders` to handle security schemes, see [Authorization](/docs/integrations/openapi/api-page#authorization), and `transformAuthInputs` to customise the auth fields. ### Requests [#requests] The parts `usePlayground()` builds on, to send a request without it. | Export | Does | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | `createBrowserFetcher()` | sends a request from the browser, through `proxyUrl` when set, its responses carry `duration`, `mediaType`, `fileName` and `text()` | | `encodeRequestData()` | turns the data of an example into a body, headers and query, from `fumadocs-openapi/requests` | | `resolveMediaAdapter()` | the adapter of a media type, `undefined` when unsupported | | `usePlaygroundAuth()` | the auth state of the page, its token store and the state of a flow | | `useAuthFields()` | the auth fields of an operation in a form, and the request data they encode into | | `requestOAuthToken()` | runs an OAuth 2.0 flow | # Fumadocs (Framework Mode): OpenAPI URL: /docs/integrations/openapi Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/openapi/index.mdx Generating docs for OpenAPI schema. ## Setup [#setup] Install the required packages. ```npm npm i fumadocs-openapi shiki ``` ### Generate Styles [#generate-styles] Add the following line: ```css title="Tailwind CSS" @import 'tailwindcss'; @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; /* [!code ++] */ @import 'fumadocs-openapi/css/preset.css'; ``` ### Configure Plugin [#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()`](/docs/integrations/openapi/server) & [`createOpenAPIPage()`](/docs/integrations/openapi/api-page) for available options. ### Generate Pages [#generate-pages] <Tabs items={["MDX Files", "Virtual Files"]}> <Tab> 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} />, })} /> ); } ``` </Tab> <Tab> You can also use it without generating real files by integrating into [Loader API](/docs/headless/source-api/source). ```ts title="lib/source.ts" import { loader } from 'fumadocs-core/source'; import { docs } from 'collections/server'; import { 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"]}> <Tab> ```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... } ``` </Tab> <Tab> 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](/docs/mdx/macro) collections in another module, like `lib/collections.ts`. ```tsx title="routes/docs/$.tsx" import { createFileRoute, notFound } from '@tanstack/react-router'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { createServerFn } from '@tanstack/react-start'; import { docs } from '@/lib/collections'; import { source } from '@/lib/source'; import { DocsBody, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page'; import { useFumadocsLoader } from 'fumadocs-core/source/client'; import { type ReactNode, Suspense } from 'react'; import { OpenAPIPage } from '@/components/api-page'; export const Route = createFileRoute('/docs/$')({ component: Page, loader: async ({ params }) => { const slugs = params._splat?.split('/') ?? []; const data = await serverLoader({ data: slugs }); // Fumadocs MDX: only preload content for normal pages [!code highlight:3] if (data.type === 'docs') { await docs.getPage(data.path)?.preload(); } return data; }, }); const serverLoader = createServerFn({ method: 'GET', }) .validator((slugs: string[]) => slugs) .handler(async ({ data: slugs }) => { const page = source.getPage(slugs); if (!page) throw notFound(); const pageTree = await source.serializePageTree(source.getPageTree()); // different result for OpenAPI pages [!code ++:8] if (page.type === 'openapi') { return { type: 'openapi', title: page.data.title, pageTree, props: page.data.getOpenAPIPageProps(), }; } return { type: 'docs', path: page.path, pageTree, // ... }; }); // renders a page of your `docs` collection function Content({ path }: { path: string }) { // ... } function Page() { const page = useFumadocsLoader(Route.useLoaderData()); let content: ReactNode; // render OpenAPI page content [!code ++:10] if (page.type === 'openapi') { content = ( <DocsPage full> <DocsTitle>{page.title}</DocsTitle> <DocsBody> {/* pass the payload data */} <OpenAPIPage {...page.props} /> </DocsBody> </DocsPage> ); } else { content = <Content path={page.path} />; } return ( <DocsLayout tree={page.pageTree}> <Suspense>{content}</Suspense> </DocsLayout> ); } ``` You can see the full [Tanstack Start example](https://github.com/fuma-nama/fumadocs/blob/dev/examples/tanstack-start-openapi). </Tab> </Tabs> <Callout type='warn' title="Ensure the migration is complete!"> Run a type check to verify before continuing, e.g. ```npm npm run types:check ``` </Callout> </Tab> </Tabs> ## Features [#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 [#demo] [View demo](/docs/openapi). # Fumadocs (Framework Mode): createOpenAPI() URL: /docs/integrations/openapi/server Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/openapi/server.mdx The OpenAPI server instance. ## OpenAPI Server [#openapi-server] The main config for Fumadocs OpenAPI. > It should not be referenced in browser environments. ### `input` [#input] The OpenAPI schemas to read from. - File Paths - External URLs - Functions (see below) ```ts tab="Basic" import { createOpenAPI } from 'fumadocs-openapi/server'; export const openapi = createOpenAPI({ input: ['./unkey.json'], }); ``` ```ts tab="Functions" import { createOpenAPI } from 'fumadocs-openapi/server'; export const openapi = createOpenAPI({ input: { // [id]: downloaded OpenAPI Schema my_schema: async () => { const res = await fetch('https://registry.scalar.com/@scalar/apis/galaxy/latest?format=json'); return res.json(); }, }, }); ``` ## Creating Proxy [#creating-proxy] A proxy server is useful for executing HTTP (`fetch`) requests, as it doesn't have CORS constraints like on the browser. We can use it for executing HTTP requests on the OpenAPI playground, when the target API endpoints do not have CORS configured correctly. <Callout type="warn" title="Warning"> Do not use this on unreliable sites and API endpoints, the proxy server will forward all received headers & body, including HTTP-only `Cookies` and `Authorization` header. </Callout> ### Setup [#setup] Create a route handler for proxy server. ```ts tab="Next.js" title="app/api/proxy/route.ts" import { openapi } from '@/lib/openapi'; export const { GET, HEAD, PUT, POST, PATCH, DELETE } = openapi.createProxy({ // important: set a list of allowed origins for proxied requests allowedOrigins: ['https://example.com'], }); ``` ```ts tab="Tanstack Start" title="app/routes/api/proxy.ts" import { openapi } from '@/lib/openapi'; import { createFileRoute } from '@tanstack/react-router'; const proxy = openapi.createProxy({ // important: set a list of allowed origins for proxied requests allowedOrigins: ['https://example.com'], }); export const Route = createFileRoute('/api/proxy')({ server: { handlers: { // Handles all methods by default, or specify individual methods (GET, POST, etc.) ANY: async ({ request }) => { return proxy.handle(request); }, }, }, }); ``` ```ts tab="Waku" title="src/pages/_api/api/proxy.ts" import { openapi } from '@/lib/openapi'; export const { GET, HEAD, PUT, POST, PATCH, DELETE } = openapi.createProxy({ // important: set a list of allowed origins for proxied requests allowedOrigins: ['https://example.com'], }); ``` And set the proxy URL in `createOpenAPI`. ```ts title="lib/openapi.ts" import { createOpenAPI } from 'fumadocs-openapi/server'; export const openapi = createOpenAPI({ proxyUrl: '/api/proxy', // [!code ++] }); ``` ## OAuth Redirect [#oauth-redirect] OAuth providers only accept registered redirect URIs, but the API playground redirects to the current page by default. Create a route handler as the redirect URI of every page, it sends users back to the page that started the flow. ```ts tab="Next.js" title="app/api/oauth/route.ts" import { openapi } from '@/lib/openapi'; export const GET = openapi.createOAuthHandler(); ``` ```ts tab="Tanstack Start" title="app/routes/api/oauth.ts" import { openapi } from '@/lib/openapi'; import { createFileRoute } from '@tanstack/react-router'; const handler = openapi.createOAuthHandler(); export const Route = createFileRoute('/api/oauth')({ server: { handlers: { GET: async ({ request }) => handler(request), }, }, }); ``` ```ts tab="Waku" title="src/pages/_api/api/oauth.ts" import { openapi } from '@/lib/openapi'; export const GET = openapi.createOAuthHandler(); ``` Pass its URL to `createOpenAPIPage()`, and register the full URL (e.g. `https://docs.example.com/api/oauth`) on your OAuth provider. ```tsx title="components/api-page.tsx" export const OpenAPIPage = createOpenAPIPage({ oauthRedirectUrl: '/api/oauth', // [!code ++] }); ``` # Fumadocs (Framework Mode): Headless URL: /docs/integrations/story/headless Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/story/headless.mdx Replace the UI of stories, or build your own on the headless layer. ## Overview [#overview] `@fumadocs/story` exports `<StoryProvider />`, `useStory()` and `useStoryArgs()`, which provide the state of a story without UI: - the selected variant, and the preset it resolves to - the arguments of the rendered component, debounced so typing doesn't re-render it on every keystroke The built-in panel is built on it, and you can swap the panel for your own. ## Your own panel [#your-own-panel] `defineStoryFactory()` takes a `WithControl` component, rendered in place of the built-in panel. ```tsx title="lib/story.tsx" import { defineStoryFactory } from '@fumadocs/story/vite/client'; import { StoryPanel } from '@/components/story-panel'; export const { defineStory } = defineStoryFactory({ WithControl: StoryPanel }); ``` It receives the presets of the story and the component to render. ```tsx title="components/story-panel.tsx" 'use client'; import { StoryProvider, useStory, useStoryArgs, type WithControlProps } from '@fumadocs/story'; export function StoryPanel(props: WithControlProps) { return ( <StoryProvider {...props}> <Content /> </StoryProvider> ); } function Content() { const { presets, variant, setVariant, preset, Component } = useStory(); const args = useStoryArgs(); return ( <div> <select value={variant} onChange={(e) => setVariant(e.target.value)}> {presets.map((item) => ( <option key={item.variant} value={item.variant}> {item.variant} </option> ))} </select> <Component {...args} /> <pre>{JSON.stringify(preset?.controls, null, 2)}</pre> </div> ); } ``` | Hook | Description | | ---------------- | --------------------------------------------------------------------------- | | `useStory()` | The presets, the selected `variant` with `setVariant`, and the `Component`. | | `useStoryArgs()` | The arguments of the rendered component, following the controls. | Controls are edited through [`@fumari/stf`](https://github.com/fuma-nama/fumadocs/tree/dev/packages/stf), the form engine `<StoryProvider />` sets up. `preset.controls` is the [type tree](/docs/integrations/story#controls) of the component props. <Callout title="Start from the built-in panel"> Writing a full control panel means handling every node kind of the type tree. Install the built-in one below and edit it instead. </Callout> ## Install the UI [#install-the-ui] The entire panel, including its argument controls: ```npm npx @fumadocs/cli add story/controls ``` ```tsx title="lib/story.tsx" import { defineStoryFactory } from '@fumadocs/story/vite/client'; import { WithControl } from '@/components/story'; export const { defineStory } = defineStoryFactory({ WithControl }); ``` # Fumadocs (Framework Mode): Story URL: /docs/integrations/story Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/story/index.mdx Display components with controls. <story.WithControl /> ## Introduction [#introduction] You can use Fumadocs Story to display & document components. It is a simple, docs-focused alternative of [Storybook](https://storybook.js.org), it is mainly designed for component libraries. If you are new to the concept of Story, this is a short explanation from **Storybook** docs: <blockquote> 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. </blockquote> <Callout title="When to use it over Storybook?"> Fumadocs Story is not a replacement for Storybook, we still recommend Storybook for proper UI testing. </Callout> ## Installation [#installation] Follow the installation guide according to your setup: <DocsCategory /> ## Configurations [#configurations] ### Variants [#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] 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: [], }, }, }, }); ``` ### Internationalization [#internationalization] Assuming you have configured [Internationalization](/docs/internationalization) at UI level: ```tsx title="layout.shared.tsx" import { defineI18n } from 'fumadocs-core/i18n'; import { uiTranslations } from 'fumadocs-ui/i18n'; import { 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 ++] 'New Item(story arguments form)': '新增項目', }, }); ``` See [Translations](/docs/ui/translations) for more details. # Fumadocs (Framework Mode): Next.js URL: /docs/integrations/story/next Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/story/next.mdx Configure Fumadocs Story on Next.js. ## Installation [#installation] ```npm npm i @fumadocs/story ``` Add the Tailwind CSS preset: ```css title="Tailwind CSS" /* [!code ++] */ @import '@fumadocs/story/css/preset.css'; ``` Add the Webpack/Turbopack plugin: ```ts title="next.config.mts" import type { NextConfig } from 'next'; // [!code ++] import { createNextStory } from '@fumadocs/story/next'; const config: NextConfig = { // ... }; // [!code ++] const withStory = createNextStory(); // wrap the config with `withStory()` // if you have other plugins like `withMDX()`, keep it like `withStory(withMDX(config))` [!code highlight] export default withStory(config); ``` ### Create a Story [#create-a-story] Create a story factory to store your global settings: ```ts title="lib/story.ts" import { defineStoryFactory } from '@fumadocs/story/next/client'; export const { defineStory } = defineStoryFactory(); ``` Now create your first story: ```tsx tab="my-component.story.tsx" import { defineStory } from '@/lib/story'; import { MyComponent } from './my-component'; export const story = defineStory({ // the passed component must be a client component Component: MyComponent, args: { // default props (recommended) initial: {}, }, }); ``` ```tsx tab="my-component.tsx" 'use client'; export interface MyComponentProps { title?: string; } export function MyComponent({ title }: MyComponentProps) { return <p>{title}</p>; } ``` <Callout type='warn' title={<>The <code>.story</code> suffix is required</>}> The build-time plugin will only transform files matching `*.story.{js,jsx,ts,tsx}`, you can configure this from `createNextStory()`. </Callout> Now you can render the story like: ```mdx title="index.mdx" import { story } from '@/components/my-component.story'; ## Overview Preview for component: <story.WithControl /> ``` # Fumadocs (Framework Mode): Vite URL: /docs/integrations/story/vite Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/story/vite.mdx Configure Fumadocs Story for Vite-based frameworks. ## Installation [#installation] ```npm npm i @fumadocs/story ``` Add the Tailwind CSS preset. ```css title="Tailwind CSS" /* [!code ++] */ @import '@fumadocs/story/css/preset.css'; ``` Add the Vite plugin: ```ts title="vite.config.ts" import { defineConfig } from 'vite'; import story from '@fumadocs/story/vite'; export default defineConfig({ plugins: [story()], }); ``` ### Create a Story [#create-a-story] Create a story factory to store your global settings: ```ts title="lib/story.ts" import { defineStoryFactory } from '@fumadocs/story/vite/client'; export const { defineStory } = defineStoryFactory(); ``` Now create your first story: ```tsx tab="my-component.story.tsx" import { defineStory } from '@/lib/story'; import { MyComponent } from './my-component'; export const story = defineStory({ Component: MyComponent, args: { // default props (recommended) initial: {}, }, }); ``` ```tsx tab="my-component.tsx" export interface MyComponentProps { title?: string; } export function MyComponent({ title }: MyComponentProps) { return <p>{title}</p>; } ``` <Callout type='warn' title={<>The <code>.story</code> suffix is required</>}> The Vite plugin will only transform files matching `*.story.{js,jsx,ts,tsx}`, you can also configure filters from the Vite plugin. </Callout> ### Render a Story [#render-a-story] Now you can render the story like: ```mdx title="index.mdx" import { story } from '@/components/my-component.story'; <story.WithControl /> ``` # Fumadocs (Framework Mode): Typescript URL: /docs/integrations/typescript Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/typescript.mdx Generate docs from Typescript definitions ## Usage [#usage] ```package-install fumadocs-typescript ``` ### UI Integration [#ui-integration] It comes with the `AutoTypeTable` component. Learn more about [Auto Type Table](/docs/ui/components/auto-type-table). ### MDX Integration [#mdx-integration] You can use it as a remark plugin: ```ts title="source.config.ts" tab="Fumadocs MDX" import { remarkAutoTypeTable, createGenerator, createFileSystemGeneratorCache, } from 'fumadocs-typescript'; import { defineConfig } from 'fumadocs-mdx/config'; const generator = createGenerator({ // recommended: choose a directory for cache cache: createFileSystemGeneratorCache('.next/fumadocs-typescript'), }); export default defineConfig({ mdxOptions: { remarkPlugins: [[remarkAutoTypeTable, { generator }]], }, }); ``` ```ts tab="MDX Compiler" import { remarkAutoTypeTable, createGenerator, createFileSystemGeneratorCache, } from 'fumadocs-typescript'; import { compile } from '@mdx-js/mdx'; const generator = createGenerator({ // recommended: choose a directory for cache cache: createFileSystemGeneratorCache('.next/fumadocs-typescript'), }); await compile('...', { remarkPlugins: [[remarkAutoTypeTable, { generator }]], }); ``` ```ts tab="Satteri" import { createGenerator, createFileSystemGeneratorCache } from 'fumadocs-typescript'; import { remarkAutoTypeTable } from '@fumadocs/satteri/remark-auto-type-table'; const generator = createGenerator({ cache: createFileSystemGeneratorCache('.next/fumadocs-typescript'), }); export default defineConfig({ docs: { compiler: 'satteri', async satteriOptions() { return { mdastPlugins: [remarkAutoTypeTable({ generator })], }; }, }, }); ``` It gives you a `auto-type-table` component. You can use it like [Auto Type Table](/docs/ui/components/auto-type-table), but with additional rules: - The value of attributes must be string. - `path` accepts a path relative to the MDX file itself. - You also need to add [`TypeTable`](/docs/ui/components/type-table) to MDX components. ```ts title="path/to/file.ts" export interface MyInterface { name: string; } ``` ```mdx title="page.mdx" <auto-type-table path="./path/to/file.ts" name="MyInterface" /> ``` ## Annotations [#annotations] ### Hide [#hide] Hide a field by adding `@internal` tsdoc tag. ```ts interface MyInterface { /** * @internal */ cache: number; } ``` ### Simplified Type [#simplified-type] You can specify the simplified name of a type with the `@remarks` tsdoc tag. ```ts interface MyInterface { /** * @remarks `timestamp` Returned by API. // [!code highlight] */ time: number; } ``` This will make the type of `time` property to be shown as `timestamp`. ### Full type [#full-type] You can specify the full name of a type with the `@fumadocsType` tsdoc tag. ```ts interface MyInterface { /** * @fumadocsType `MyBeautifulClient` // [!code highlight] */ client: MyClient; } ``` ### Type Href [#type-href] Link the property to other places when used with Type Table. Type Table will generate anchor ID automatically, you can use it to link to another type table. ```ts interface MyInterface { /** * @fumadocsHref #type-table-temp.ts-MyClass-test */ client: MyClient; } ``` # Fumadocs (Framework Mode): Validate Links URL: /docs/integrations/validate-links Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/integrations/validate-links.mdx Ensure your links are correct. ## Setup [#setup] You can use [`next-validate-link`](https://next-validate-link.vercel.app) to validate your links in content files. > This guide is mainly for **Fumadocs MDX**, see the docs of `next-validate-link` for other setups. Create a script: ```ts title="scripts/lint.ts" import { type FileObject, printErrors, scanURLs, validateFiles } from 'next-validate-link'; import { source } from '@/lib/source'; async function checkLinks() { const scanned = await scanURLs({ // pick a preset for your React framework preset: 'next', populate: { 'docs/[[...slug]]': source.getPages().map((page) => { return { value: { slug: page.slugs, }, hashes: getHeadings(page), }; }), }, }); printErrors( await validateFiles(await getFiles(), { scanned, // check `href` attributes in different MDX components markdown: { components: { Card: { attributes: ['href'] }, }, }, // check relative paths checkRelativePaths: 'as-url', }), true, ); } function getHeadings({ data }: (typeof source)['$inferPage']): string[] { return data.toc.map((item) => item.url.slice(1)); } function getFiles() { const promises = source.getPages().map(async (page): Promise<FileObject> => ({ path: page.absolutePath, content: await page.data.getText('raw'), url: page.url, data: page.data, })); return Promise.all(promises); } void checkLinks(); ``` ### Running Lint [#running-lint] To access the `source` object outside of app runtime, configure [Fumadocs MDX Loader](/docs/mdx/loader/bun) for Bun. Then, run the script to validate links: ```bash bun ./scripts/lint.ts ``` <Callout title="Node.js Loader"> You might need to configure TypeScript transpiling (or newer version of Node.js), and [Fumadocs MDX Loader](/docs/mdx/loader/node). Without special reasons, using Bun or [`tsx`](https://github.com/privatenumber/tsx) would be simpler. </Callout> # Fumadocs (Framework Mode): Internationalization URL: /docs/internationalization Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/internationalization/index.mdx Support multiple languages in your documentation <Callout title="Fumadocs CLI"> `npx @fumadocs/cli init --i18n` sets up a new docs with i18n, or configure it manually below. </Callout> ## Overview [#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](https://github.com/amannn/next-intl) on Next.js. <DocsCategory /> # Fumadocs (Framework Mode): Next.js URL: /docs/internationalization/next Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/internationalization/next.mdx Support i18n routing on your Next.js + Fumadocs app <Callout title="New to Next.js?"> You can [learn more about i18n in Next.js](https://nextjs.org/docs/app/building-your-application/routing/internationalization). </Callout> ## Setup [#setup] Define the i18n configurations in a file, we will import it with `@/lib/i18n` in this guide. ```ts title="lib/i18n.ts" import { defineI18n } from 'fumadocs-core/i18n'; export const i18n = defineI18n({ defaultLanguage: 'en', languages: ['en', 'cn'], }); ``` > See [available options](/docs/headless/internationalization/config) for i18n config. ### Middleware [step] [#middleware] Create a middleware that redirects users to appropriate locale. ```ts title="proxy.ts" import { createI18nMiddleware } from 'fumadocs-core/i18n/middleware'; import { i18n } from '@/lib/i18n'; export default createI18nMiddleware(i18n); export const config = { // Matcher ignoring `/_next/` and `/api/` // You may need to adjust it to ignore static assets in `/public` folder matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], }; ``` <Callout title="Using your own middleware?"> The default middleware is optional, you can also use your own middleware or the one provided by i18n libraries. Make sure its behaviour aligns with the [`hidePrefix`](/docs/headless/internationalization/config#hide-locale-prefix) option you set in your i18n config. </Callout> ### Translations [step] [#translations] Define your translations in `lib/layout.shared.tsx`, see [Translations](/docs/ui/translations) for more details (including official language packs). You can also add `locale` parameter to `baseOptions()` for localized layout options: ```tsx title="lib/layout.shared.tsx" import { i18n } from '@/lib/i18n'; import { uiTranslations } from 'fumadocs-ui/i18n'; import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; // [!code ++:11] export const translations = i18n .translations() .extend(uiTranslations()) .add({ en: { displayName: 'English', }, cn: { displayName: 'Chinese', }, }); // [!code highlight] export function baseOptions(locale: string): BaseLayoutProps { return { // different props based on `locale` }; } ``` Pass translations to `<RootProvider />`: ```tsx title="app/[lang]/layout.tsx" import { RootProvider } from 'fumadocs-ui/provider/next'; import { translations } from '@/lib/layout.shared'; import { i18nProvider } from 'fumadocs-ui/i18n'; export default async function RootLayout({ params, children, }: { params: Promise<{ lang: string }>; children: React.ReactNode; }) { const lang = (await params).lang; return ( <html lang={lang}> <body> <RootProvider // [!code ++] i18n={i18nProvider(translations, lang)} > {children} </RootProvider> </body> </html> ); } ``` ### Routing [step] [#routing] Create a `/app/[lang]` folder, and move your pages/layouts into it, except route handlers. <Files> <Folder name="app" defaultOpen> <File name="api/search/route.ts" /> <Folder name="[lang]" defaultOpen> <File name="layout.tsx" /> <File name="(home)/page.tsx" /> <File name="..." /> </Folder> </Folder> </Files> <Callout title="Common Mistake" type="error"> Did you accidentally find your styles lost? Make sure the import path to `global.css` is still correct! </Callout> ### Pass Locale [step] [#pass-locale] Pass the locale to Fumadocs in your pages and layouts. ```ts tab="lib/source.ts" import { i18n } from '@/lib/i18n'; import { loader } from 'fumadocs-core/source'; export const source = loader({ i18n, // [!code ++] // other options }); ``` ```tsx tab="Home Layout" title="app/[lang]/(home)/layout.tsx" import type { ReactNode } from 'react'; import { HomeLayout } from 'fumadocs-ui/layouts/home'; import { baseOptions } from '@/lib/layout.shared'; export default async function Layout({ params, children, }: { params: Promise<{ lang: string }>; children: ReactNode; }) { const { lang } = await params; return <HomeLayout {...baseOptions(lang)}>{children}</HomeLayout>; // [!code highlight] } ``` ```tsx tab="Docs Layout" title="app/[lang]/docs/layout.tsx" import type { ReactNode } from 'react'; import { source } from '@/lib/source'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { baseOptions } from '@/lib/layout.shared'; export default async function Layout({ params, children, }: { params: Promise<{ lang: string }>; children: ReactNode; }) { const { lang } = await params; return ( // [!code highlight] <DocsLayout {...baseOptions(lang)} tree={source.getPageTree(lang)}> {children} </DocsLayout> ); } ``` ```ts tab="Docs Page" title="app/[lang]/docs/[[...slug]]/page.tsx" import { source } from '@/lib/source'; export default async function Page({ params, }: { params: Promise<{ lang: string; slug?: string[] }>; }) { const { slug, lang } = await params; // get page source.getPage(slug); // [!code --] source.getPage(slug, lang); // [!code ++] // get pages source.getPages(); // [!code --] source.getPages(lang); // [!code ++] } ``` <Callout title={<>Using another name for <code>lang</code> dynamic segment?</>}> If you're using another name like `app/[locale]`, you also need to update `generateStaticParams()` in docs page: ```tsx export function generateStaticParams() { return source.generateParams(); // [!code --] return source.generateParams('slug', 'locale'); // [!code ++] new param name } ``` </Callout> ### Search [step] [#search] Configure i18n on your search solution. - **Built-in Search:** See [Internationalization](/docs/headless/search/orama#internationalization). - **Cloud Solutions (e.g. Algolia):** They usually have official support for multilingual. ## Writing Documents [#writing-documents] See [i18n routing](/docs/page-conventions#i18n-routing) to learn how to create pages for specific locales. ### Navigation [#navigation] Fumadocs only handles navigation for its own layouts (e.g. sidebar). For other places, you can use the `useParams` hook to get the locale from url, and prepend it to `href`. ```tsx import Link from 'next/link'; import { useParams } from 'next/navigation'; const { lang } = useParams(); return <Link href={`/${lang}/another-page`}>This is a link</Link>; ``` In addition, the [`fumadocs-core/dynamic-link`](/docs/headless/components/link#dynamic-hrefs) component supports dynamic hrefs, you can use it to prepend the locale prefix. It is useful for Markdown/MDX content. ```mdx title="content.mdx" import { DynamicLink } from 'fumadocs-core/dynamic-link'; <DynamicLink href="/[lang]/another-page">This is a link</DynamicLink> ``` # Fumadocs (Framework Mode): React Router URL: /docs/internationalization/react-router Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/internationalization/react-router.mdx Support i18n routing on your React Router + Fumadocs app. ## Setup [#setup] Define the i18n configurations in a file, we will import it with `@/lib/i18n` in this guide. ```ts title="app/lib/i18n.ts" import { defineI18n } from 'fumadocs-core/i18n'; export const i18n = defineI18n({ defaultLanguage: 'en', languages: ['cn', 'en'], }); ``` > See [available options](/docs/headless/internationalization/config) for i18n config. ### Routing [#routing] Add `:lang` prefix to all your pages. ```ts title="app/routes.ts" import { route, type RouteConfig } from '@react-router/dev/routes'; export default [ // [!code highlight:2] route(':lang', 'routes/home.tsx'), route(':lang/docs/*', 'routes/docs.tsx'), route('api/search', 'routes/search.ts'), ] satisfies RouteConfig; ``` #### Make locale optional [#make-locale-optional] You can also use `:lang?` prefix, and update the i18n config: ```ts tab="app/routes.ts" import { route, type RouteConfig } from '@react-router/dev/routes'; export default [ // [!code highlight:2] route(':lang?', 'routes/home.tsx'), route(':lang?/docs/*', 'routes/docs.tsx'), route('api/search', 'routes/search.ts'), ] satisfies RouteConfig; ``` ```ts tab="app/lib/i18n.ts" import { defineI18n } from 'fumadocs-core/i18n'; export const i18n = defineI18n({ defaultLanguage: 'en', languages: ['cn', 'en'], // [!code ++] hideLocale: 'default-locale', }); ``` ### Translations [#translations] Define your translations in `lib/layout.shared.tsx`, see [Translations](/docs/ui/translations) for more details (including official language packs). You can also add `locale` parameter to `baseOptions()` for localized layout options: ```tsx title="lib/layout.shared.tsx" import { i18n } from '@/lib/i18n'; import { uiTranslations } from 'fumadocs-ui/i18n'; import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; // [!code ++:11] export const translations = i18n .translations() .extend(uiTranslations()) .add({ en: { displayName: 'English', }, cn: { displayName: 'Chinese', }, }); // [!code highlight] export function baseOptions(locale: string): BaseLayoutProps { return { // different props based on `locale` }; } ``` ### Pages [#pages] Pass translations to `<RootProvider />`: ```tsx title="app/root.tsx" import { Links, Meta, Scripts, ScrollRestoration, useParams } from 'react-router'; import { RootProvider } from 'fumadocs-ui/provider/react-router'; import { translations } from '@/lib/layout.shared'; import { i18nProvider } from 'fumadocs-ui/i18n'; import './app.css'; export function Layout({ children }: { children: React.ReactNode }) { const { lang = i18nUI.defaultLanguage } = useParams(); return ( <html lang="en" suppressHydrationWarning> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta /> <Links /> </head> <body className="flex flex-col min-h-screen"> <RootProvider // [!code ++] i18n={i18nProvider(translations, lang)} > {children} </RootProvider> <ScrollRestoration /> <Scripts /> </body> </html> ); } ``` ### Pass Locale [#pass-locale] Pass the locale to Fumadocs in your pages and layouts. ```ts tab="app/lib/source.ts" import { i18n } from '@/lib/i18n'; import { loader } from 'fumadocs-core/source'; export const source = loader({ i18n, // [!code ++] // other options }); ``` ```tsx tab="Home Layout" title="app/routes/home.tsx" import type { Route } from './+types/home'; import { HomeLayout } from 'fumadocs-ui/layouts/home'; import { baseOptions } from '@/lib/layout.shared'; export default function Home({ params }: Route.ComponentProps) { return ( // [!code highlight] <HomeLayout {...baseOptions(params.lang)}></HomeLayout> ); } ``` ```tsx tab="Docs Page" title="app/routes/docs.tsx" import type { Route } from './+types/docs'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { source } from '@/lib/source'; import type * as PageTree from 'fumadocs-core/page-tree'; import { baseOptions } from '@/lib/layout.shared'; export async function loader({ params }: Route.LoaderArgs) { const slugs = params['*'].split('/').filter((v) => v.length > 0); // [!code --] const page = source.getPage(slugs); // [!code ++] const page = source.getPage(slugs, params.lang); if (!page) throw new Response('Not found', { status: 404 }); return { path: page.path, // [!code --] tree: source.getPageTree(), // [!code ++] tree: source.getPageTree(params.lang), }; } export default function Page({ loaderData, params }: Route.ComponentProps) { const { tree, path } = loaderData; return ( <DocsLayout // [!code highlight] {...baseOptions(params.lang)} tree={tree as PageTree.Root} ></DocsLayout> ); } ``` ### Search [#search] Configure i18n on your search solution. - **Built-in Search:** See [Internationalization](/docs/headless/search/orama#internationalization). - **Cloud Solutions (e.g. Algolia):** They usually have official support for multilingual. ## Writing Documents [#writing-documents] See [i18n routing](/docs/page-conventions#i18n-routing) to learn how to create pages for specific locales. ### Navigation [#navigation] Fumadocs only handles navigation for its own layouts (e.g. sidebar). For other places, you can use the `useParams` hook to get the locale from url. ```tsx import { Link, useParams } from 'react-router'; const { lang } = useParams(); <Link to={`/${lang}/about`}>About Us</Link>; ``` In addition, the [`fumadocs-core/dynamic-link`](/docs/headless/components/link#dynamic-hrefs) component supports dynamic hrefs, you can use it to attend the locale prefix. It is useful for Markdown/MDX content. ```mdx title="content.mdx" import { DynamicLink } from 'fumadocs-core/dynamic-link'; <DynamicLink href="/[lang]/another-page">This is a link</DynamicLink> ``` # Fumadocs (Framework Mode): Tanstack Start URL: /docs/internationalization/tanstack-start Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/internationalization/tanstack-start.mdx Support i18n routing on your Tanstack Start + Fumadocs app. ## Setup [#setup] Define the i18n configurations in a file, we will import it with `@/lib/i18n` in this guide. ```ts title="src/lib/i18n.ts" import { defineI18n } from 'fumadocs-core/i18n'; export const i18n = defineI18n({ defaultLanguage: 'en', languages: ['cn', 'en'], }); ``` > See [available options](/docs/headless/internationalization/config) for i18n config. ### Translations [#translations] Define your translations in `lib/layout.shared.tsx`, see [Translations](/docs/ui/translations) for more details (including official language packs). You can also add `locale` parameter to `baseOptions()` for localized layout options: ```tsx title="lib/layout.shared.tsx" import { i18n } from '@/lib/i18n'; import { uiTranslations } from 'fumadocs-ui/i18n'; import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; // [!code ++:11] export const translations = i18n .translations() .extend(uiTranslations()) .add({ en: { displayName: 'English', }, cn: { displayName: 'Chinese', }, }); // [!code highlight] export function baseOptions(locale: string): BaseLayoutProps { return { // different props based on `locale` }; } ``` Pass translations to `<RootProvider />`: ```tsx title="src/routes/__root.tsx" import { HeadContent, Scripts, useParams } from '@tanstack/react-router'; import * as React from 'react'; import { RootProvider } from 'fumadocs-ui/provider/tanstack'; import { i18nProvider } from 'fumadocs-ui/i18n'; import { translations } from '@/lib/layout.shared'; function RootDocument({ children }: { children: React.ReactNode }) { const { lang } = useParams({ strict: false }); return ( <html suppressHydrationWarning> <head> <HeadContent /> </head> <body className="flex flex-col min-h-screen"> <RootProvider // [!code ++] i18n={i18nProvider(translations, lang)} > {children} </RootProvider> <Scripts /> </body> </html> ); } ``` ### Routing [#routing] Create a `$lang` directory under `routes`, and move all pages into it. <Files> <Folder name="src/routes" defaultOpen> <Folder name="$lang" defaultOpen> <File name="index.tsx" /> <File name="docs/$.tsx" /> </Folder> <Folder name="api" defaultOpen> <File name="search.ts" /> </Folder> </Folder> </Files> ### Pass Locale [#pass-locale] Pass the locale to Fumadocs in your pages and layouts. ```ts tab="src/lib/source.ts" import { i18n } from '@/lib/i18n'; import { loader } from 'fumadocs-core/source'; export const source = loader({ i18n, // [!code ++] // other options }); ``` ```tsx tab="Home Layout" title="src/routes/$lang/index.tsx" import { createFileRoute } from '@tanstack/react-router'; import { HomeLayout } from 'fumadocs-ui/layouts/home'; import { baseOptions } from '@/lib/layout.shared'; export const Route = createFileRoute('/$lang/')({ component: Home, }); function Home() { const { lang } = Route.useParams(); // [!code highlight] return <HomeLayout {...baseOptions(lang)}></HomeLayout>; } ``` ```tsx tab="Docs Page" title="src/routes/$lang/docs/$.tsx" import { createFileRoute, notFound } from '@tanstack/react-router'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { createServerFn } from '@tanstack/react-start'; import { source } from '@/lib/source'; import { baseOptions } from '@/lib/layout.shared'; export const Route = createFileRoute('/$lang/docs/$')({ component: Page, loader: async ({ params }) => { const data = await loader({ // [!code ++:4] data: { slugs: params._splat?.split('/') ?? [], lang: params.lang, }, }); await clientLoader.preload(data.path); return data; }, }); // [!code highlight:13] const loader = createServerFn({ method: 'GET', }) .validator((params: { slugs: string[]; lang?: string }) => params) .handler(async ({ data: { slugs, lang } }) => { const page = source.getPage(slugs, lang); if (!page) throw notFound(); return { tree: source.getPageTree(lang) as object, path: page.path, }; }); function Page() { const { lang } = Route.useParams(); // ... return ( // [!code highlight] <DocsLayout {...baseOptions(lang)} tree={tree}> <Content /> </DocsLayout> ); } ``` ### Search [#search] Configure i18n on your search solution. - **Built-in Search:** See [Internationalization](/docs/headless/search/orama#internationalization). - **Cloud Solutions (e.g. Algolia):** They usually have official support for multilingual. ## Writing Documents [#writing-documents] See [i18n routing](/docs/page-conventions#i18n-routing) to learn how to create pages for specific locales. ### Navigation [#navigation] Fumadocs only handles navigation for its own layouts (e.g. sidebar). For other places, you can use the `useParams` hook to get the locale from url. ```tsx import { Link, useParams } from '@tanstack/react-router'; const { lang } = useParams({ from: '/$lang/' }); <Link to="/$lang/docs/$" params={{ lang, _splat: '', }} > Open Docs </Link>; ``` In addition, the [`fumadocs-core/dynamic-link`](/docs/headless/components/link#dynamic-hrefs) component supports dynamic hrefs, you can use it to attend the locale prefix. It is useful for Markdown/MDX content. ```mdx title="content.mdx" import { DynamicLink } from 'fumadocs-core/dynamic-link'; <DynamicLink href="/[lang]/another-page">This is a link</DynamicLink> ``` # Fumadocs (Framework Mode): Waku URL: /docs/internationalization/waku Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/internationalization/waku.mdx Support i18n routing on your Waku + Fumadocs app. ## Setup [#setup] Define the i18n configurations in a file, we will import it with `@/lib/i18n` in this guide. ```ts title="src/lib/i18n.ts" import { defineI18n } from 'fumadocs-core/i18n'; export const i18n = defineI18n({ defaultLanguage: 'en', languages: ['cn', 'en'], }); ``` > See [available options](/docs/headless/internationalization/config) for i18n config. ### Routing [#routing] Add a `[lang]` prefix to all your pages. <Files> <Folder name="src/pages" defaultOpen> <Folder name="[lang]" defaultOpen> <File name="index.tsx" /> <File name="docs/[...slugs].tsx" /> </Folder> <Folder name="_api" defaultOpen> <File name="api/search.ts" /> </Folder> </Folder> </Files> ### Translations [#translations] Define your translations in `lib/layout.shared.tsx`, see [Translations](/docs/ui/translations) for more details (including official language packs). You can also add `locale` parameter to `baseOptions()` for localized layout options: ```tsx title="lib/layout.shared.tsx" import { i18n } from '@/lib/i18n'; import { uiTranslations } from 'fumadocs-ui/i18n'; import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; // [!code ++:11] export const translations = i18n .translations() .extend(uiTranslations()) .add({ en: { displayName: 'English', }, cn: { displayName: 'Chinese', }, }); // [!code highlight] export function baseOptions(locale: string): BaseLayoutProps { return { // different props based on `locale` nav: { title: 'My App', url: `/${locale}`, }, }; } ``` ### Pages [#pages] Waku does not expose route segment params from `useRouter()`. Extract the locale from the path, then pass translations to `<RootProvider />`: ```tsx title="src/components/provider.tsx" 'use client'; import type { ReactNode } from 'react'; import { RootProvider } from 'fumadocs-ui/provider/waku'; import { i18nProvider } from 'fumadocs-ui/i18n'; import { useRouter } from 'waku/router/client'; import { i18n } from '@/lib/i18n'; import { translations } from '@/lib/layout.shared'; export function Provider({ children }: { children: ReactNode }) { const router = useRouter(); // [!code highlight] const lang = router.path.split('/')[1] ?? i18n.defaultLanguage; return ( <RootProvider // [!code ++] i18n={i18nProvider(translations, lang)} > {children} </RootProvider> ); } ``` ### Pass Locale [#pass-locale] Pass the locale to Fumadocs in your pages and layouts. ```ts tab="src/lib/source.ts" import { i18n } from '@/lib/i18n'; import { loader } from 'fumadocs-core/source'; export const source = loader({ i18n, // [!code ++] // other options }); ``` ```tsx tab="Home Page" title="src/pages/[lang]/index.tsx" import { i18n } from '@/lib/i18n'; import { HomeLayout } from 'fumadocs-ui/layouts/home'; import { baseOptions } from '@/lib/layout.shared'; import type { PageProps } from 'waku/router'; export default function Home({ lang }: PageProps<'/[lang]'>) { // [!code highlight] return <HomeLayout {...baseOptions(lang)}></HomeLayout>; } export const getConfig = async () => { return { render: 'static' as const, staticPaths: i18n.languages.map((lang) => [lang]), } as const; }; ``` ```tsx tab="Docs Page" title="src/pages/[lang]/docs/[...slugs].tsx" import { source } from '@/lib/source'; import type { PageProps } from 'waku/router'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { baseOptions } from '@/lib/layout.shared'; export default function DocPage({ slugs, lang }: PageProps<'/[lang]/docs/[...slugs]'>) { // [!code --] const page = source.getPage(slugs); // [!code ++] const page = source.getPage(slugs, lang); if (!page) return <div>Not Found</div>; return ( <DocsLayout // [!code highlight] {...baseOptions(lang)} // [!code --] tree={source.getPageTree()} // [!code ++] tree={source.getPageTree(lang)} > {/* ... */} </DocsLayout> ); } export async function getConfig() { const pages = source .generateParams() .map((item) => (item.lang ? [item.lang, ...item.slug] : item.slug)); return { render: 'static' as const, staticPaths: pages, } as const; } ``` ### Search [#search] Configure i18n on your search solution. - **Built-in Search:** See [Internationalization](/docs/headless/search/orama#internationalization). - **Cloud Solutions (e.g. Algolia):** They usually have official support for multilingual. ## Writing Documents [#writing-documents] See [i18n routing](/docs/page-conventions#i18n-routing) to learn how to create pages for specific locales. ### Navigation [#navigation] Fumadocs only handles navigation for its own layouts (e.g. sidebar). For other places, extract the locale from the path and prepend it to links. ```tsx import { Link, useRouter } from 'waku/router/client'; const router = useRouter(); const lang = router.path.split('/')[1]; <Link to={`/${lang}/about`}>About Us</Link>; ``` In addition, the [`fumadocs-core/dynamic-link`](/docs/headless/components/link#dynamic-hrefs) component supports dynamic hrefs, you can use it to attend the locale prefix. It is useful for Markdown/MDX content. ```mdx title="content.mdx" import { DynamicLink } from 'fumadocs-core/dynamic-link'; <DynamicLink href="/[lang]/another-page">This is a link</DynamicLink> ``` # Fumadocs (Framework Mode): Astro URL: /docs/manual-installation/astro Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/manual-installation/astro.mdx Setup Fumadocs on Astro with React islands. ## Getting Started [#getting-started] Before continuing, make sure to configure: - Astro with `@astrojs/react`. - Tailwind CSS 4. Fumadocs UI is a React library. On Astro, render it inside a React island and pass the current route information from your `.astro` page. ### Installation [#installation] ```npm npm i fumadocs-core fumadocs-ui @astrojs/react @astrojs/mdx @astrojs/markdown-remark react react-dom takumi-js sharp ``` ### Configuration [#configuration] Configure Astro, MDX, React, and Tailwind CSS: ```mjs title="astro.config.mjs" // @ts-check import { defineConfig } from 'astro/config'; import react from '@astrojs/react'; import tailwindcss from '@tailwindcss/vite'; import mdx from '@astrojs/mdx'; import { unified } from '@astrojs/markdown-remark'; import { rehypeCode, remarkCodeTab, remarkHeading, remarkNpm, remarkStructure, } from 'fumadocs-core/mdx-plugins'; const remarkPlugins = [ remarkHeading, remarkCodeTab, remarkNpm, [remarkStructure, { exportAs: 'structuredData' }], ]; const rehypePlugins = [rehypeCode]; export default defineConfig({ markdown: { processor: unified({ syntaxHighlight: false, remarkPlugins, rehypePlugins, }), }, integrations: [ react(), mdx({ extendMarkdownConfig: true, syntaxHighlight: false, }), ], vite: { plugins: [tailwindcss()], }, }); ``` Add the following to your Tailwind CSS file: ```css title="src/styles/global.css" @import 'tailwindcss'; /* [!code ++:2] */ @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; ``` ### Content Collections [#content-collections] Create content collections for docs and meta files: ```ts title="src/content.config.ts" import { glob } from 'astro/loaders'; import { defineCollection, z } from 'astro:content'; const docs = defineCollection({ loader: glob({ pattern: '**/*.{md,mdx}', base: './content/docs' }), schema: z.object({ title: z.string(), description: z.string().optional(), icon: z.string().optional(), }), }); const meta = defineCollection({ loader: glob({ pattern: '**/*.{json,yaml}', base: './content/docs' }), schema: z.object({ title: z.string().optional(), description: z.string().optional(), pages: z.array(z.string()).optional(), icon: z.string().optional(), }), }); export const collections = { docs, meta, }; ``` Create a Fumadocs source from Astro content collections: ```ts title="src/lib/source.ts" import type { StaticSource } from 'fumadocs-core/source'; import { loader } from 'fumadocs-core/source'; import { type CollectionEntry, getCollection } from 'astro:content'; import * as path from 'node:path'; import { structure, type StructuredData } from 'fumadocs-core/mdx-plugins'; export const source = loader({ source: await createMySource(), baseUrl: '/', }); export function getStructuredData(entry: CollectionEntry<'docs'>): StructuredData { return structure(entry.body); } async function createMySource() { const out: StaticSource<{ metaData: CollectionEntry<'meta'>['data']; pageData: CollectionEntry<'docs'>['data'] & { _raw: CollectionEntry<'docs'>; }; }> = { files: [], }; for (const page of await getCollection('docs')) { const virtualPath = path.relative('content/docs', page.filePath!); out.files.push({ type: 'page', path: virtualPath, data: { ...page.data, _raw: page, }, }); } for (const meta of await getCollection('meta')) { const virtualPath = path.relative('content/docs', meta.filePath!); out.files.push({ type: 'meta', path: virtualPath, data: meta.data, }); } return out; } ``` ### Create Pages [#create-pages] Create the shared Astro document layout: ```astro tab="src/components/layout.astro" --- import { ClientRouter } from 'astro:transitions'; --- <html lang="en"> <head> <meta charset="utf-8" /> <ClientRouter /> </head> <body class="dark flex flex-col min-h-screen" transition:persist transition:animate="none" transition:name="page-layout" > <slot /> </body> </html> ``` Create the React island that renders Fumadocs UI: ```tsx tab="src/components/docs.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { DocsPage, type DocsPageProps } from 'fumadocs-ui/layouts/docs/page'; import type { Root } from 'fumadocs-core/page-tree'; import type { ReactNode } from 'react'; import { navigate } from 'astro:transitions/client'; import { RootProvider } from 'fumadocs-ui/provider/astro'; import type { AstroProviderProps } from 'fumadocs-core/framework/astro'; import SearchDialog from './search'; export function Docs({ tree, children, pathname, params, page, }: { tree: Root; children: ReactNode; pathname: string; params: AstroProviderProps['params']; page?: DocsPageProps; }) { return ( <RootProvider pathname={pathname} params={params} navigate={navigate} theme={{ enabled: false }} search={{ SearchDialog }} > <DocsLayout tree={tree} themeSwitch={{ enabled: false, }} nav={{ title: 'Fumadocs on Astro', }} > <DocsPage {...page}>{children}</DocsPage> </DocsLayout> </RootProvider> ); } ``` Create a search dialog: ```tsx tab="src/components/search.tsx" 'use client'; import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, type SharedProps, } from 'fumadocs-ui/components/dialog/search'; import { useStaticSearch } from 'fumadocs-core/search/client'; import { useI18n } from 'fumadocs-ui/contexts/i18n'; export default function DefaultSearchDialog(props: SharedProps) { const { locale } = useI18n(); // (optional) for i18n const search = useStaticSearch({ locale, }); return ( <SearchDialog {...search} {...props}> <SearchDialogOverlay /> <SearchDialogContent> <SearchDialogHeader> <SearchDialogIcon /> <SearchDialogInput /> <SearchDialogClose /> </SearchDialogHeader> <SearchDialogList /> </SearchDialogContent> </SearchDialog> ); } ``` Create the docs route, search endpoint, and metadata image endpoint: ```astro tab="src/pages/[...slug].astro" --- import { render, type CollectionEntry } from "astro:content"; import { Docs } from "@/components/docs"; import defaultMdxComponents from "fumadocs-ui/mdx"; import "@/styles/global.css"; import { source } from "@/lib/source"; import { getPageImageUrl } from "@/lib/shared"; import Layout from "@/components/layout.astro"; import type { TOCItemType } from "fumadocs-core/toc"; interface Props { page: CollectionEntry<"docs">; } export async function getStaticPaths() { return source.getPages().map((page) => ({ params: { slug: page.slugs.length > 0 ? page.slugs.join("/") : undefined }, props: { page: page.data._raw }, })); } const { page } = Astro.props; const { Content, headings } = await render(page); const slugs = typeof Astro.params.slug === "string" ? Astro.params.slug.split("/") : []; const docsPage = source.getPage(slugs)!; const image = getPageImageUrl(docsPage).url; const toc: TOCItemType[] = headings.map((heading) => ({ depth: heading.depth, title: heading.text, url: "#" + heading.slug, })); --- <Layout> <title>{page.data.title}

{page.data.title}

{page.data.description}

``` ```ts tab="src/pages/api/search.ts" import type { APIRoute } from 'astro'; import { createFromSource } from 'fumadocs-core/search/server'; import { getStructuredData, source } from '@/lib/source'; const server = createFromSource(source, { buildIndex(page) { return { id: page.data._raw.id, title: page.data.title, description: page.data.description, structuredData: getStructuredData(page.data._raw), url: page.url, }; }, }); export const GET: APIRoute = () => { return server.staticGET(); }; ``` ```ts tab="src/pages/og/docs/[...slug]/image.webp.ts" import type { APIRoute } from 'astro'; import { generateOGImage } from 'fumadocs-ui/og/takumi'; import { source } from '@/lib/source'; export function getStaticPaths() { return source.getPages().map((page) => ({ params: { slug: page.slugs.length > 0 ? page.slugs.join('/') : undefined, }, })); } export const GET: APIRoute = ({ params }) => { const slugs = params.slug?.split('/').filter((item) => item.length > 0) ?? []; const page = source.getPage(slugs); if (!page) return new Response(undefined, { status: 404 }); return generateOGImage({ title: page.data.title, description: page.data.description, site: 'Astro', format: 'webp', }); }; ``` ### Done [#done] You can start writing documents at `content/docs`: ```mdx title="content/docs/index.mdx" --- title: Hello World description: Your first document --- I love Fumadocs. ``` # Fumadocs (Framework Mode): Manual Installation URL: /docs/manual-installation Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/manual-installation/index.mdx Add Fumadocs to existing projects. You can also let [Fumadocs CLI](/docs/cli#init) set it up for you: ```npm npx @fumadocs/cli init ``` # Fumadocs (Framework Mode): Next.js URL: /docs/manual-installation/next Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/manual-installation/next.mdx Setup Fumadocs on Next.js. ## Prerequisite [#prerequisite] Before continuing, make sure you have configured: - Next.js 16. - Tailwind CSS 4. We will use [Fumadocs MDX](/docs/mdx) as a content source, you can configure it first:
### Installation [#installation] ```npm npm i fumadocs-mdx fumadocs-core @types/mdx ``` Add the plugin to Next.js config: ```js title="next.config.mjs" import { createMDX } from 'fumadocs-mdx/next'; /** @type {import('next').NextConfig} */ const config = { reactStrictMode: true, }; // [!code ++:4] const withMDX = createMDX(); // [!code highlight] export default withMDX(config); ``` Fumadocs MDX is ESM-only, it's recommended to use `next.config.mjs` for accurate ESM resolution. For TypeScript config file, it requires Native Node.js TypeScript Resolver, you can see [Next.js docs](https://nextjs.org/docs/app/api-reference/config/typescript#using-nodejs-native-typescript-resolver-for-nextconfigts) for details. ### Integrate with Fumadocs [#integrate-with-fumadocs] Create a `lib/source.ts` file: ```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(), }); ``` ### Done [#done] You can now write content in `content/docs` folder.
Fumadocs also supports other content sources, including [Content Collections](/docs/headless/content-collections) and headless CMS. ## Getting Started [#getting-started] ```npm npm i fumadocs-ui fumadocs-core ``` ### Root Layout [#root-layout] Wrap the entire application inside [Root Provider](/docs/ui/layouts/root-provider), and add required styles to `body`. ```tsx title="app/layout.tsx" import { RootProvider } from 'fumadocs-ui/provider/next'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` ### Styles [#styles] Add the following Tailwind CSS styles to `global.css`. ```css title="global.css" @import 'tailwindcss'; /* [!code ++:2] */ @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; ``` > It doesn't come with a default font, you may choose one from `next/font`. ### Routes [#routes] Create a `lib/layout.shared.tsx` file to put the shared options for our layouts. ```tsx title="lib/layout.shared.tsx" import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; export function baseOptions(): BaseLayoutProps { return { nav: { title: 'My App', }, }; } ``` Create the following files & routes: ```tsx tab="components/mdx.tsx" import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultMdxComponents, ...components, } satisfies MDXComponents; } export const useMDXComponents = getMDXComponents; declare global { type MDXProvidedComponents = ReturnType; } ``` ```tsx tab="app/docs/layout.tsx" import { source } from '@/lib/source'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { baseOptions } from '@/lib/layout.shared'; export default function Layout({ children }: LayoutProps<'/docs'>) { return ( {children} ); } ``` ```tsx tab="app/docs/[[...slug]]/page.tsx" import { source } from '@/lib/source'; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page'; import { notFound } from 'next/navigation'; import { getMDXComponents } from '@/components/mdx'; import type { Metadata } from 'next'; import { createRelativeLink } from 'fumadocs-ui/mdx'; export default async function Page(props: PageProps<'/docs/[[...slug]]'>) { const params = await props.params; const page = source.getPage(params.slug); if (!page) notFound(); const MDX = page.data.body; return ( {page.data.title} {page.data.description} ); } export async function generateStaticParams() { return source.generateParams(); } export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise { const params = await props.params; const page = source.getPage(params.slug); if (!page) notFound(); return { title: page.data.title, description: page.data.description, }; } ``` ```ts tab="app/api/search/route.ts" import { source } from '@/lib/source'; import { createFromSource } from 'fumadocs-core/search/server'; export const { GET } = createFromSource(source, { // https://docs.orama.com/docs/orama-js/supported-languages language: 'english', }); ``` > The search is powered by ZBSearch, learn more about [Document Search](/docs/headless/search). ### Finish [#finish] You can start the dev server and create MDX files. ```mdx title="content/docs/index.mdx" --- title: Hello World --- ## Introduction I love Anime. ``` # Fumadocs (Framework Mode): React Router URL: /docs/manual-installation/react-router Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/manual-installation/react-router.mdx Setup Fumadocs on React Router. ## Getting Started [#getting-started] Before continuing, make sure to configure: - Tailwind CSS 4. - Fumadocs MDX: follow the [Vite setup guide](/docs/mdx/vite) and create essential files like `lib/source.ts`. ### Installation [#installation] ```npm npm i fumadocs-core fumadocs-ui ``` ### Styles [#styles] Add the following to your Tailwind CSS file: ```css title="app/app.css" @import 'tailwindcss'; /* [!code ++:2] */ @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; ``` ### Create Pages [#create-pages] Update your routes: ```ts title="routes.ts" import { type RouteConfig, index, route } from '@react-router/dev/routes'; export default [ index('routes/home.tsx'), // [!code ++:2] route('docs/*', 'routes/docs.tsx'), route('api/search', 'routes/search.ts'), ] satisfies RouteConfig; ``` Create the following files: ```tsx tab="app/components/mdx.tsx" import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultMdxComponents, ...components, } satisfies MDXComponents; } export const useMDXComponents = getMDXComponents; declare global { type MDXProvidedComponents = ReturnType; } ``` ```tsx tab="app/lib/layout.shared.tsx" import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; export function baseOptions(): BaseLayoutProps { return { nav: { title: 'React Router', }, }; } ``` ```tsx tab="app/routes/docs.tsx" import type { Route } from './+types/docs'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page'; import { docs, source } from '@/lib/source'; import { baseOptions } from '@/lib/layout.shared'; import { useFumadocsLoader } from 'fumadocs-core/source/client'; import { useMDXComponents } from '@/components/mdx'; import { use } from 'react'; export async function loader({ params }: Route.LoaderArgs) { const slugs = params['*'].split('/').filter((v) => v.length > 0); const page = source.getPage(slugs); if (!page) throw new Response('Not found', { status: 404 }); return { path: page.path, url: page.url, pageTree: await source.serializePageTree(source.getPageTree()), }; } function Content({ path }: { path: string }) { const page = docs.getPage(path); if (!page) throw new Error(`unknown page: ${path}`); // content is loaded lazily, call `page.preload()` in your loader to avoid suspending const { toc } = use(page.load()); const Mdx = page.body; return ( {page.title} {page.title} {page.description} ); } export default function Page({ loaderData }: Route.ComponentProps) { const { path, pageTree } = useFumadocsLoader(loaderData); return ( ); } ``` ```ts tab="app/routes/search.ts" import type { Route } from './+types/search'; import { createFromSource } from 'fumadocs-core/search/server'; import { source } from '@/lib/source'; const server = createFromSource(source); export async function loader({ request }: Route.LoaderArgs) { return server.GET(request); } ``` Wrap your entire app under Fumadocs providers: ```tsx title="root.tsx" import { Links, Meta, Scripts, ScrollRestoration } from 'react-router'; import { RootProvider } from 'fumadocs-ui/provider/react-router'; import './app.css'; export function Layout({ children }: { children: React.ReactNode }) { return ( {/* [!code ++] */} {children} ); } ``` ### Done [#done] You can start writing documents at `content/docs`: ```mdx title="content/docs/index.mdx" --- title: Hello World --- I love Fumadocs ``` # Fumadocs (Framework Mode): Tanstack Start URL: /docs/manual-installation/tanstack-start Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/manual-installation/tanstack-start.mdx Setup Fumadocs on Tanstack Start. ## Getting Started [#getting-started] Before continuing, make sure to configure: - Tailwind CSS 4. - Fumadocs MDX: follow the [Vite setup guide](/docs/mdx/vite) and create essential files like `lib/source.ts`. ### Installation [#installation] ```npm npm i fumadocs-core fumadocs-ui ``` ### Styles [#styles] Add the following to your Tailwind CSS file: ```css title="styles/app.css" @import 'tailwindcss'; /* [!code ++:2] */ @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; ``` ### Create Pages [#create-pages] Create the following files: ```tsx tab="components/mdx.tsx" import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultMdxComponents, ...components, } satisfies MDXComponents; } export const useMDXComponents = getMDXComponents; declare global { type MDXProvidedComponents = ReturnType; } ``` ```tsx tab="lib/layout.shared.tsx" import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; export function baseOptions(): BaseLayoutProps { return { nav: { title: 'Tanstack Start', }, }; } ``` ```tsx tab="routes/docs/$.tsx" import { createFileRoute, notFound } from '@tanstack/react-router'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { createServerFn } from '@tanstack/react-start'; import { docs, source } from '@/lib/source'; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page'; import { baseOptions } from '@/lib/layout.shared'; import { useFumadocsLoader } from 'fumadocs-core/source/client'; import { Suspense, use } from 'react'; import { useMDXComponents } from '@/components/mdx'; export const Route = createFileRoute('/docs/$')({ component: Page, loader: async ({ params }) => { const slugs = params._splat?.split('/') ?? []; const data = await serverLoader({ data: slugs }); await docs.getPage(data.path)?.preload(); return data; }, }); const serverLoader = createServerFn({ method: 'GET', }) .validator((slugs: string[]) => slugs) .handler(async ({ data: slugs }) => { const page = source.getPage(slugs); if (!page) throw notFound(); return { path: page.path, pageTree: await source.serializePageTree(source.getPageTree()), }; }); function Content({ path }: { path: string }) { const page = docs.getPage(path); if (!page) throw new Error(`unknown page: ${path}`); const { toc } = use(page.load()); const MDX = page.body; return ( {page.title} {page.description} ); } function Page() { const data = useFumadocsLoader(Route.useLoaderData()); return ( ); } ``` ```ts tab="routes/api/search.ts" import { createFileRoute } from '@tanstack/react-router'; import { source } from '@/lib/source'; import { createFromSource } from 'fumadocs-core/search/server'; const server = createFromSource(source); export const Route = createFileRoute('/api/search')({ server: { handlers: { GET: async ({ request }) => server.GET(request), }, }, }); ``` Wrap your entire app under Fumadocs providers: ```tsx title="__root.tsx" import { createRootRoute, HeadContent, Outlet, Scripts } from '@tanstack/react-router'; import * as React from 'react'; import { RootProvider } from 'fumadocs-ui/provider/tanstack'; export const Route = createRootRoute({ component: RootComponent, }); function RootComponent() { return ( ); } function RootDocument({ children }: { children: React.ReactNode }) { return ( {/* [!code ++] */} {children} ); } ``` ### Done [#done] You can start writing documents at `content/docs`: ```mdx title="content/docs/index.mdx" --- title: Hello World --- I love Fumadocs ``` # Fumadocs (Framework Mode): Waku URL: /docs/manual-installation/waku Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/manual-installation/waku.mdx Setup Fumadocs on Waku. ## Getting Started [#getting-started] Before continuing, make sure to configure: - Tailwind CSS 4. - Fumadocs MDX: follow the [Vite setup guide](/docs/mdx/vite) and create essential files like `lib/source.ts`. ### Installation [#installation] ```npm npm i fumadocs-core fumadocs-ui ``` ### Styles [#styles] Add the following to your Tailwind CSS file: ```css title="src/styles/globals.css" @import 'tailwindcss'; /* [!code ++:2] */ @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; ``` ### Create Pages [#create-pages] Create a file for sharing layout props: ```tsx title="lib/layout.shared.tsx" import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; import { appName, gitConfig } from './shared'; export function baseOptions(): BaseLayoutProps { return { nav: { // JSX supported title: appName, }, githubUrl: `https://github.com/${gitConfig.user}/${gitConfig.repo}`, }; } ``` Create the following files: ```tsx tab="components/mdx.tsx" import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultMdxComponents, ...components, } satisfies MDXComponents; } export const useMDXComponents = getMDXComponents; declare global { type MDXProvidedComponents = ReturnType; } ``` ```tsx tab="pages/docs/_layout.tsx" import type { ReactNode } from 'react'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { source } from '@/lib/source'; import { baseOptions } from '@/lib/layout.shared'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` ```tsx tab="pages/docs/[...slugs].tsx" import { source } from '@/lib/source'; import { PageProps } from 'waku/router'; import { createRelativeLink } from 'fumadocs-ui/mdx'; import { DocsBody, DocsDescription, DocsPage, DocsTitle, MarkdownCopyButton, ViewOptionsPopover, } from 'fumadocs-ui/layouts/docs/page'; import { unstable_notFound } from 'waku/router/server'; import { getMDXComponents } from '@/components/mdx'; import { getPageImageUrl, getPageMarkdownUrl, gitConfig } from '@/lib/shared'; export default function Page({ slugs }: PageProps<'/docs/[...slugs]'>) { const page = source.getPage(slugs); if (!page) unstable_notFound(); const MDX = page.data.body; const markdownUrl = getPageMarkdownUrl(page).url; return ( {page.data.title} {page.data.description}
); } export async function getConfig() { const pages = source .generateParams() .map((item) => (item.lang ? [item.lang, ...item.slug] : item.slug)); return { render: 'static' as const, staticPaths: pages, } as const; } ``` ```ts tab="pages/_api/api/search.ts" import { createFromSource } from 'fumadocs-core/search/server'; import { source } from '@/lib/source'; export const { GET } = createFromSource(source); ``` Finally, wrap your entire app under Fumadocs providers: ```tsx tab="components/provider.tsx" 'use client'; import type { ReactNode } from 'react'; import { RootProvider } from 'fumadocs-ui/provider/waku'; export function Provider({ children }: { children: ReactNode }) { return {children}; } ``` ```tsx tab="pages/_layout.tsx" import '@/styles/globals.css'; import type { ReactNode } from 'react'; import { Provider } from '@/components/provider'; // [!code ++:3] export default function RootLayout({ children }: { children: ReactNode }) { return {children}; } export const getConfig = async () => { return { render: 'static', }; }; ``` ### Done [#done] You can start writing documents at `content/docs`: ```mdx title="content/docs/index.mdx" --- title: Hello World --- I love Fumadocs ``` # Fumadocs (Framework Mode): Markdown URL: /docs/markdown Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/markdown/index.mdx How to write documents ## Introduction [#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](/docs/integrations/content/custom) for details. ## MDX [#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 Syntax](https://mdxjs.com/docs/what-is-mdx/#mdx-syntax). - GFM (GitHub Flavored Markdown) is also supported, see [GFM Specification](https://github.github.com/gfm). ```mdx noCopy import { Component } from './component'; ## Heading ### Heading #### Heading Hello World, **Bold**, _Italic_, ~~Hidden~~ 1. First 2. Second 3. Third - Item 1 - Item 2 > Quote here ![alt](/image.png) | Table | Description | | ----- | ----------- | | Hello | World | ``` ### Frontmatter [#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`](/docs/mdx/collections#schema-1) option to add frontmatter properties. ### Auto Links [#auto-links] Internal links use the `` 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 [#cards] Useful for adding links. ```mdx import { HomeIcon } from 'lucide-react'; Learn more about caching in Next.js Learn more about `fetch` in Next.js. } href="/" title="Home"> You can include icons too. ``` 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 [#further-reading-section] You can do something like: ```tsx title="page.tsx" import { getPageTreePeers } from 'fumadocs-core/page-tree'; import { source } from '@/lib/source'; {getPageTreePeers(source.getPageTree(), '/docs/my-page').map((peer) => ( {peer.description} ))} ; ``` This will show the other pages in the same folder as cards. ### Callouts [#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 Hello World Hello World Hello World Hello World ``` Hello World Hello World Hello World Hello World ### Headings [#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 [#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: ``` #### Custom Anchor [#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 [#codeblock] Syntax Highlighting is supported by default using [Rehype Code](/docs/headless/mdx/rehype-code). ````mdx ```js console.log('Hello World'); ``` ```js title="My Title" console.log('Hello World'); ``` ```` #### Line Numbers [#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 [#shiki-transformers] We support some of the [Shiki Transformers](https://shiki.style/packages/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
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] ``` #### Tab Groups [#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 [#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=" Tab 2" console.log('B'); ``` ```` ```ts tab=" Tab 1" console.log('A'); ``` ```ts tab=" Tab 2" console.log('B'); ``` ### Include [#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" ./another.mdx ``` See [other usages of include](/docs/mdx/include). ### NPM Commands [#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`](/docs/headless/mdx/remark-npm) for details. ### Steps [#steps] Useful for denoting steps for guides, see [`remark-steps`](/docs/headless/mdx/remark-steps) for enable & configure. ```md ### Installation [step] ### Write Code [step] ### Deploy [step] ``` #### Installation [step] [#installation] #### Write Code [step] [#write-code] #### Deploy [step] [#deploy] ### Other Components [#other-components] You can configure & use the [built-in components](/docs/ui/components) in your MDX document, such as Tabs, Accordions and Zoomable Image. ## Additional Features [#additional-features] You may be interested: # Fumadocs (Framework Mode): Math URL: /docs/markdown/math Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/markdown/math.mdx Writing math equations in Markdown/MDX. ## Getting Started [#getting-started] ```package-install remark-math rehype-katex katex ``` ### Add Plugins [#add-plugins] Add the required remark/rehype plugins, the code might be vary depending on your content source. ```ts title="source.config.ts" tab="Fumadocs MDX" import rehypeKatex from 'rehype-katex'; import remarkMath from 'remark-math'; import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { remarkPlugins: [remarkMath], // Place it at first, it should be executed before the syntax highlighter rehypePlugins: (v) => [rehypeKatex, ...v], }, }); ``` ### Add Stylesheet [#add-stylesheet] Add the following to root layout to make it looks great: ```tsx title="layout.tsx" import 'katex/dist/katex.css'; ``` ### Done [#done] Type some TeX expression in your documents, like the Pythagoras theorem: ````mdx Inline: $$c = \pm\sqrt{a^2 + b^2}$$ ```math c = \pm\sqrt{a^2 + b^2} ``` ```` Inline: $$c = \pm\sqrt{a^2 + b^2}$$ ```math c = \pm\sqrt{a^2 + b^2} ``` Taylor Expansion (expressing holomorphic function $$f(x)$$ in power series): ```math \displaystyle {\begin{aligned}T_{f}(z)&=\sum _{k=0}^{\infty }{\frac {(z-c)^{k}}{2\pi i}}\int _{\gamma }{\frac {f(w)}{(w-c)^{k+1}}}\,dw\\&={\frac {1}{2\pi i}}\int _{\gamma }{\frac {f(w)}{w-c}}\sum _{k=0}^{\infty }\left({\frac {z-c}{w-c}}\right)^{k}\,dw\\&={\frac {1}{2\pi i}}\int _{\gamma }{\frac {f(w)}{w-c}}\left({\frac {1}{1-{\frac {z-c}{w-c}}}}\right)\,dw\\&={\frac {1}{2\pi i}}\int _{\gamma }{\frac {f(w)}{w-z}}\,dw=f(z),\end{aligned}} ``` You can actually copy equations on Wikipedia, they will be converted into a KaTeX string when you paste it. ```math \displaystyle S[{\boldsymbol {q}}]=\int _{a}^{b}L(t,{\boldsymbol {q}}(t),{\dot {\boldsymbol {q}}}(t))\,dt. ``` # Fumadocs (Framework Mode): Mermaid URL: /docs/markdown/mermaid Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/markdown/mermaid.mdx Rendering diagrams in your docs ## Setup [#setup] Fumadocs doesn't have a built-in Mermaid wrapper provided, we recommend using `mermaid` directly. You can decide the Mermaid renderer to configure: ### Official Renderer [#official-renderer] Install the required dependencies, `next-themes` is used with Fumadocs to manage the light/dark mode. ```npm npm install mermaid next-themes ``` Create the Mermaid component: ```tsx title="components/mdx/mermaid.tsx" 'use client'; import { use, useId, useSyncExternalStore } from 'react'; import { useTheme } from 'next-themes'; export function Mermaid({ chart }: { chart: string }) { // `false` on the server and during hydration const isClient = useSyncExternalStore( () => () => {}, () => true, () => false, ); if (!isClient) return; return ; } const cache = new Map>(); function cachePromise(key: string, setPromise: () => Promise): Promise { const cached = cache.get(key); if (cached) return cached as Promise; const promise = setPromise(); cache.set(key, promise); return promise; } function MermaidContent({ chart }: { chart: string }) { const id = useId(); const { resolvedTheme } = useTheme(); const { default: mermaid } = use(cachePromise('mermaid', () => import('mermaid'))); mermaid.initialize({ startOnLoad: false, securityLevel: 'loose', fontFamily: 'inherit', themeCSS: 'margin: 1.5rem auto 0;', theme: resolvedTheme === 'dark' ? 'dark' : 'default', }); const { svg, bindFunctions } = use( cachePromise(`${chart}-${resolvedTheme}`, () => { return mermaid.render(id, chart.replaceAll('\\n', '\n')); }), ); return (
{ if (container) bindFunctions?.(container); }} dangerouslySetInnerHTML={{ __html: svg }} /> ); } ``` ### Beautiful Mermaid [#beautiful-mermaid] [`beautiful-mermaid`](https://github.com/lukilabs/beautiful-mermaid) is a 3rd party Mermaid renderer. ```npm npm install beautiful-mermaid ``` ```tsx title="components/mdx/mermaid.tsx" import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock'; import { renderMermaidSVG } from 'beautiful-mermaid'; export async function Mermaid({ chart }: { chart: string }) { try { const svg = renderMermaidSVG(chart, { bg: 'var(--color-fd-background)', fg: 'var(--color-fd-foreground)', interactive: true, transparent: true, }); return
; } catch { return (
{chart}
); } } ``` ## Usage [#usage] Add the component as a MDX component: ```tsx title="components/mdx.tsx" import defaultMdxComponents from 'fumadocs-ui/mdx'; import { Mermaid } from '@/components/mdx/mermaid'; // [!code ++] import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultMdxComponents, Mermaid, // [!code ++] ...components, } satisfies MDXComponents; } ``` Then, use it in MDX files. ```mdx ``` ### As CodeBlock [#as-codeblock] You can convert `mermaid` codeblocks into the MDX usage with the `remarkMdxMermaid` remark plugin. ```tsx tab="Fumadocs MDX" title="source.config.ts" import { remarkMdxMermaid } from 'fumadocs-core/mdx-plugins'; import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { remarkPlugins: [remarkMdxMermaid], }, }); ``` ````md ```mermaid graph TD; A-->B; A-->C; ``` ```` # Fumadocs (Framework Mode): Twoslash URL: /docs/markdown/twoslash Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/markdown/twoslash.mdx Use Typescript Twoslash in your docs ## Usage [#usage] Thanks to the Twoslash integration of [Shiki](https://github.com/shikijs/shiki), the default code syntax highlighter, it is as simple as adding a transformer. Type information is resolved by the native TypeScript 7 compiler, from the working directory of your project. ```package-install fumadocs-twoslash ``` > While being analyzed, code blocks are briefly written to the `.twoslash` directory, add it to your `.gitignore`. For Next.js, you need to externalize the following deps: ```js title="next.config.mjs (Next.js)" const config = { reactStrictMode: true, // [!code ++] serverExternalPackages: ['typescript'], }; ``` Add to your Shiki transformers. ```ts title="source.config.ts (Fumadocs MDX)" import { defineConfig } from 'fumadocs-mdx/config'; import { transformerTwoslash } from 'fumadocs-twoslash'; import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins'; export default defineConfig({ mdxOptions: { rehypeCodeOptions: { themes: { light: 'github-light', dark: 'github-dark', }, transformers: [...(rehypeCodeDefaultOptions.transformers ?? []), transformerTwoslash()], // important: Shiki doesn't support lazy loading languages for codeblocks in Twoslash popups // make sure to define them first (e.g. the common ones) langs: ['js', 'jsx', 'ts', 'tsx'], }, }, }); ``` Add styles, Tailwind CSS v4 is required. ```css title="Tailwind CSS" @import 'fumadocs-twoslash/twoslash.css'; ``` Add MDX components. ```tsx title="components/mdx.tsx" import * as Twoslash from 'fumadocs-twoslash/ui'; // [!code ++] import defaultComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultComponents, // [!code ++] ...Twoslash, ...components, } satisfies MDXComponents; } ``` Now you can add `twoslash` meta string to codeblocks. ````md ```ts twoslash console.log('Hello World'); ``` ```` ### Cache [#cache] Optionally, you can enable filesystem cache with `typesCache` option: ```ts twoslash title="source.config.ts" import { transformerTwoslash } from 'fumadocs-twoslash'; import { createFileSystemTypesCache } from 'fumadocs-twoslash/cache-fs'; transformerTwoslash({ typesCache: createFileSystemTypesCache(), }); ``` ### Example [#example] Learn more about [Twoslash notations](https://twoslash.netlify.app/refs/notations). ````ts twoslash tab="Output" lineNumbers type Player = { /** * The player name * ```php * $a = "60" * ``` * * @default 'user' */ name: string; }; /** * A custom function. * * @example * * ```ts * const str = fn("hello world") * console.log(str) * ``` */ function fn(s: T): T { return s; } // ---cut--- // @noErrors console.g; // ^| const player: Player = { name: 'Hello World' }; // ^? fn('test'); ```` `````md tab="Input" lineNumbers ````ts twoslash lineNumbers type Player = { /** * The player name * ```php * $a = "60" * ``` * * @default 'user' */ name: string; }; /** * A custom function. * * @example * * ```ts * const str = fn("hello world") * console.log(str) * ``` */ function fn(s: T): T { return s; } // ---cut--- // @noErrors console.g; // ^| const player: Player = { name: 'Hello World' }; // ^? fn('test'); ```` ````` ```ts twoslash const a = '123'; console.log(a); // ^^^ ``` ```ts twoslash // @errors: 2588 const a = '123'; a = 132; ``` # Fumadocs (Framework Mode): Navigation URL: /docs/navigation Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/navigation.mdx Organize sidebar pages, link to other resources, and publish multiple versions of your docs. ## Overview [#overview] **Fumadocs UI** provides different layouts to display content, you can specify navigation configurations via layouts. } title="Layout Links" href="/docs/ui/layouts/links"> Display **navigation links** in your layouts. It is useful for linking to frequently used resources like showcase and pricing pages. } title="Sidebar Items" href="/docs/ui/layouts/docs#sidebar-items"> Sidebar renders links to all documentation pages. Under the hood, **Page Tree** is used to represent the navigation structure. ## Versioning [#versioning] Keep multiple versions available when readers need documentation for older releases. Choose whether versions share a site or have separate deployments: | Approach | When | | ----------------------------------- | ----------------------------------------------------------------------- | | [Root Type](#root-type) | Versioning applies to part of your docs, versions live in the same site | | [Full Versioning](#full-versioning) | Versioning the entire website, one deployment per version | ### Root Type [#root-type] Create a folder for each version, and mark them as root folders of the same type in `meta.json`: ```json tab="v1/meta.json" { "title": "1.0.0", "root": "version" } ``` ```json tab="v2/meta.json" { "title": "2.0.0", "root": "version" } ``` The sidebar only shows the content of the opened version, and docs layouts render a dropdown to switch between them, labelled by their `title`. Switching keeps your place, it navigates to the same page in the other version, or its index page when the page doesn't exist there: | Page | Switching to 2.0.0 | | -------------------------- | -------------------------- | | `/docs/v1/getting-started` | `/docs/v2/getting-started` | | `/docs/v1/legacy-api` | `/docs/v2`, the index page | The type name (`"version"`) can be anything, it only identifies the type: root folders of the same type under the same parent are interchangeable. ### Full Versioning [#full-versioning] Sometimes you want to version the entire website, such as https://v14.fumadocs.dev (Fumadocs v14) and https://fumadocs.dev (Latest Fumadocs). You can create a Git branch for a version of docs (call it `v2` for example), and deploy it as a separate app on another subdomain like `v2.my-site.com`. Optionally, you can link to the other versions from your docs. This design allows some advantages over partial versioning: - Easy maintenance: Old docs/branches won't be affected when you iterate or upgrade dependencies. - Better consistency: Not just the docs itself, your landing page (and other pages) will also be versioned. # Fumadocs (Framework Mode): Page Slugs & Page Tree URL: /docs/page-conventions Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/page-conventions.mdx A shared convention for organizing your documents > This guide only applies for content sources that uses `loader()` API, such as **Fumadocs MDX**. ## Overview [#overview] Fumadocs generates **page slugs** and **page tree** (sidebar items) from your content directory using [`loader()`](/docs/headless/source-api), the routing functionality will be handled by your React framework. You can define folders and pages similar to file-system based routing. To customize its output further, you may use [Loader Plugins](/docs/headless/source-api/plugins). ## File [#file] For [MDX](https://mdxjs.com) & Markdown file, you can customize page information from frontmatter. ```mdx --- title: My Page description: Best document ever icon: House --- ## Learn More ``` Fumadocs detects from the following properties to construct page trees. | name | description | | ------------- | ------------------------------------- | | `title` | The title of page | | `description` | The description of page | | `icon` | The name of icon, see [Icons](#icons) | Page information is supplied by the content source such as **Fumadocs MDX**. On Fumadocs MDX, you can specify a [`schema`](/docs/mdx/collections#schema-1) option to customize frontmatter schema. ### Slugs [#slugs] The slugs of a page are generated from its file path. | path (relative to content folder) | slugs | | --------------------------------- | ----------------- | | `./dir/page.mdx` | `['dir', 'page']` | | `./dir/index.mdx` | `['dir']` | ## Folder [#folder] Organize multiple pages, you can create a [Meta file](#meta) to customize folders. ### Folder Group [#folder-group] By default, putting a file into folder will change its slugs. You can wrap the folder name in parentheses to avoid impacting the slugs of child files. | path (relative to content folder) | slugs | | --------------------------------- | ---------- | | `./(group-name)/page.mdx` | `['page']` | ### Root Folder [#root-folder] Marks the folder as a root folder, only items in the opened root folder will be visible. ```json title="meta.json" { "title": "Name of Folder", "description": "The description of root folder (optional)", "root": true } ``` For example, when you are opening root folder `framework`, the other folders (e.g. `headless`) are not shown on the sidebar and other navigation elements. Fumadocs UI renders root folders as [Layout Tabs](/docs/ui/layouts/docs#layout-tabs), which allows users to switch between them. ### Root Type [#root-type] `root` also accepts a string, the **type** of root folder: root folders of the same type under the same parent scope are interchangeable, such as versions of the same docs. ```json tab="v1/meta.json" { "title": "1.0.0", "root": "version" } ``` ```json tab="v2/meta.json" { "title": "2.0.0", "root": "version" } ``` `root: true` is simply the default type, displayed as tabs. Switching to another root folder of the same type maps a page to its **structural projection**, the page at the same path relative to the root folder: | Page | Projection in `v2` | | --------------------- | --------------------------------------------------- | | `v1/guide.mdx` | `v2/guide.mdx` | | `v1/python/guide.mdx` | `v2/python/guide.mdx`, nested root folders are kept | Fumadocs UI renders a [dropdown](/docs/ui/layouts/docs#tabs-groups) for each root folder on the current page's path to switch between root folders of its type, see [Versioning](/docs/navigation#versioning) for a complete example. ## Meta [#meta] Customize folders by creating a `meta.json` file in the folder. ```json title="meta.json" { "title": "Display Name", "icon": "MyIcon", "pages": ["index", "getting-started"], "defaultOpen": true } ``` | name | description | | ------------- | -------------------------------------------- | | `title` | Display name | | `icon` | The name of icon, see [Icons](#icons) | | `defaultOpen` | Open the folder by default | | `collapsible` | Collapsible folder content (default: `true`) | | `pages` | Folder items (see below) | | `pagesIndex` | The index item (see below) | ### Pages [#pages] Folder items are sorted alphabetically by default, you can add or control the order of items using `pages`. ```json title="meta.json" { "pages": ["index", "getting-started"] } ``` > When specified, items are not included unless they are listed in `pages`. | Type | Syntax | Description | | ----------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | **Path** | `./path/to/page` | a path to page or folder. | | **Separator** | `---Label---`
`---[Icon]Label---` | a separator between two sections (icon supported). | | **Link** | `[Text](url)`
`[Icon][Text](url)`
`external:[Text](url)` | insert links (icon supported)
add a `external:` prefix to mark links as external. | | **Rest** | `...` | include remaining pages (sorted alphabetically). | | **Reversed Rest** | `z...a` | reversed **Rest** item. | | **Extract** | `...folder` | extract the items from a folder. | | **Except** | `!item` | Exclude an item from `...` or `z...a`. | ```json title="meta.json" { "pages": [ "components", "---My Separator---", "...folder", "...", "!file", "!otherFolder", "[Vercel](https://vercel.com)", "[Triangle][Vercel](https://vercel.com)" ] } ``` The same page URL must not appear more than once in the entire page tree. Duplicated page items are not allowed for the same reason. This is intentional, such that Fumadocs can locate the active tree item purely based on current pathname. ### Folder Index [#folder-index] When folder owns a index item, it becomes clickable and jumps to the index item when clicked. By default, it is the `index` file under each folder. You can specify the index item of a folder, it accepts **Path** and **Link**. ```json tab="meta.json (Path)" { // assume there's a "overview.mdx" file under the folder "pagesIndex": "overview" } ``` ```json tab="meta.json (Link)" { "pagesIndex": "[Example](https://example.com)" } ``` ## Icons [#icons] Since Fumadocs doesn't include an icon library, you have to convert the icon names to JSX elements in runtime, and render it as a component. You can add an [`icon` handler](/docs/headless/source-api#icons) to `loader()`. ## i18n Routing [#i18n-routing] You can define different style for i18n routing. ```ts title="lib/i18n.ts" import type { I18nConfig } from 'fumadocs-core/i18n'; export const i18n: I18nConfig = { // default parser: 'dot', // or parser: 'dir', }; ``` Add Markdown/meta files for different languages by attending `.{locale}` to your file name, like: For the default locale, the locale code is optional. All content files are grouped by language folders: # Fumadocs (Framework Mode): Algolia URL: /docs/search/algolia Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/search/algolia.mdx Using Algolia with Fumadocs UI. ## Overview [#overview] For the setup guide, see [Integrate Algolia Search](/docs/headless/search/algolia). While generally we recommend building your own search with their client-side SDK, you can also plug the built-in dialog interface. ## Setup [#setup] Fumadocs CLI can set it up for you, it creates the search dialog, the `static.json` route and a script to sync the search index after each build: ```package-install npx @fumadocs/cli feature search --provider algolia ``` Or manually: Create a search dialog, replace `appId`, `apiKey` and `indexName` with your desired values. ```tsx title="components/search.tsx" 'use client'; import { liteClient } from 'algoliasearch/lite'; import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogFooter, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, type SharedProps, } from 'fumadocs-ui/components/dialog/search'; import { useAlgoliaSearch } from 'fumadocs-core/search/client'; import { useI18n } from 'fumadocs-ui/contexts/i18n'; const appId = 'replace me'; const apiKey = 'replace me'; const algolia = liteClient(appId, apiKey); export default function CustomSearchDialog(props: SharedProps) { const { locale } = useI18n(); // (optional) for i18n const search = useAlgoliaSearch({ client: algolia, indexName: 'document', locale, }); return ( Search powered by Algolia ); } ``` `useAlgoliaSearch()` doesn't use instant search (their official Javascript client). ### Replace Search Dialog [#replace-search-dialog] Replace the search dialog with yours from [``](/docs/ui/layouts/root-provider): ```tsx import { RootProvider } from 'fumadocs-ui/provider/'; // [!code ++] import SearchDialog from '@/components/search'; {children} ; ``` If it was in a server component, you would need a separate client component for provider to pass functions: ```tsx tab="provider.tsx" 'use client'; import { RootProvider } from 'fumadocs-ui/provider/'; import SearchDialog from '@/components/search'; import type { ReactNode } from 'react'; export function Provider({ children }: { children: ReactNode }) { return ( {children} ); } ``` ```tsx tab="app/layout.tsx" import { Provider } from './provider'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {/* [!code --] */} {children} {/* [!code ++] */} {children} ); } ``` ### Tag Filter [#tag-filter] Optionally, you can add UI for filtering results by tags. Configure [Tag Filter](/docs/headless/search/algolia#tag-filter) on search server and add the following: ```tsx 'use client'; import { SearchDialog, SearchDialogContent, SearchDialogFooter, SearchDialogOverlay, type SharedProps, TagsList, TagsListItem, } from 'fumadocs-ui/components/dialog/search'; import { useState } from 'react'; import { useFetchSearch } from 'fumadocs-core/search/client'; export default function CustomSearchDialog(props: SharedProps) { // [!code ++] const [tag, setTag] = useState(); const search = useFetchSearch({ tag, // [!code ++] }); return ( ... {/* [!code ++:3] */} My Value ); } ``` # Fumadocs (Framework Mode): Custom Search URL: /docs/search/custom Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/search/custom.mdx For other search solutions. ## Overview [#overview] Fumadocs is very flexible, you can integrate custom search with Fumadocs easily. ### Search Indexes [#search-indexes] With Fumadocs MDX, the generated search indexes are exposed during runtime. To expose search indexes statically, export the records with a pre-rendered `static.json` route, and process them with a script after each build: ```ts tab="Next.js" title="app/static.json/route.ts" import { source } from '@/lib/source'; async function getDocuments() { const documents = []; for (const page of source.getPages()) { documents.push({ structured: page.data.structuredData, url: page.url, title: page.data.title, description: page.data.description, }); } return documents; } export const revalidate = false; export async function GET() { return Response.json(await getDocuments()); } ``` ```ts tab="Next.js" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import type { StructuredData } from 'fumadocs-core/mdx-plugins'; interface DocumentRecord { title: string; description?: string; url: string; structured: StructuredData; } const records = JSON.parse(await fs.readFile('.next/server/app/static.json.body', 'utf-8')) as DocumentRecord[]; // sync the records to your search engine console.log(`search updated: ${records.length} records`); ``` ```json tab="Next.js" title="package.json" { "scripts": { "build": "next build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="React Router" title="app/routes/static.json.ts" import { source } from '@/lib/source'; async function getDocuments() { const documents = []; for (const page of source.getPages()) { documents.push({ structured: await page.data.structuredData(), url: page.url, title: page.data.title, description: page.data.description, }); } return documents; } export async function loader() { return Response.json(await getDocuments()); } ``` ```ts tab="React Router" title="app/routes.ts" import { index, route, type RouteConfig } from '@react-router/dev/routes'; export default [ index('routes/home.tsx'), route('static.json', 'routes/static.json.ts'), // [!code ++:1] ] satisfies RouteConfig; ``` ```ts tab="React Router" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import type { StructuredData } from 'fumadocs-core/mdx-plugins'; interface DocumentRecord { title: string; description?: string; url: string; structured: StructuredData; } const records = JSON.parse(await fs.readFile('build/client/static.json', 'utf-8')) as DocumentRecord[]; // sync the records to your search engine console.log(`search updated: ${records.length} records`); ``` ```json tab="React Router" title="package.json" { "scripts": { "build": "react-router build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Tanstack Start" title="src/routes/static[.]json.ts" import { createFileRoute } from '@tanstack/react-router'; import { source } from '@/lib/source'; async function getDocuments() { const documents = []; for (const page of source.getPages()) { documents.push({ structured: await page.data.structuredData(), url: page.url, title: page.data.title, description: page.data.description, }); } return documents; } export const Route = createFileRoute('/static.json')({ server: { handlers: { GET: async () => Response.json(await getDocuments()), }, }, }); ``` ```ts tab="Tanstack Start" title="vite.config.ts" import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import { nitro } from 'nitro/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ tanstackStart({ prerender: { enabled: true, }, pages: [ // [!code ++:3] { path: '/static.json' } ], }), nitro({ preset: 'vercel', }), ], }); ``` ```ts tab="Tanstack Start" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import type { StructuredData } from 'fumadocs-core/mdx-plugins'; interface DocumentRecord { title: string; description?: string; url: string; structured: StructuredData; } const records = JSON.parse(await fs.readFile('.vercel/output/static/static.json', 'utf-8')) as DocumentRecord[]; // sync the records to your search engine console.log(`search updated: ${records.length} records`); ``` ```json tab="Tanstack Start" title="package.json" { "scripts": { "build": "vite build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Waku" title="src/pages/_api/static.json.ts" import { source } from '@/lib/source'; async function getDocuments() { const documents = []; for (const page of source.getPages()) { documents.push({ structured: page.data.structuredData, url: page.url, title: page.data.title, description: page.data.description, }); } return documents; } export async function GET() { return Response.json(await getDocuments()); } export async function getConfig() { return { render: 'static' as const, } as const; } ``` ```ts tab="Waku" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import type { StructuredData } from 'fumadocs-core/mdx-plugins'; interface DocumentRecord { title: string; description?: string; url: string; structured: StructuredData; } const records = JSON.parse(await fs.readFile('dist/public/static.json', 'utf-8')) as DocumentRecord[]; // sync the records to your search engine console.log(`search updated: ${records.length} records`); ``` ```json tab="Waku" title="package.json" { "scripts": { "build": "waku build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` The `structured` field contains the headings and paragraphs of each page, use it to generate accurate search indexes. # Fumadocs (Framework Mode): FlexSearch URL: /docs/search/flexsearch Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/search/flexsearch.mdx A fast & minimal search engine. ## Overview [#overview] For the setup guide, see [Integrate FlexSearch](/docs/headless/search/flexsearch). It works through a API endpoint (running on server), or a statically cached JSON file for static websites. ## Setup [#setup] Create a search dialog. ```tsx title="components/search.tsx" 'use client'; import { useFetchSearch } from 'fumadocs-core/search/client'; import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, type SharedProps, } from 'fumadocs-ui/components/dialog/search'; import { useI18n } from 'fumadocs-ui/contexts/i18n'; export default function DefaultSearchDialog(props: SharedProps) { const { locale } = useI18n(); // (optional) for i18n const search = useFetchSearch({ locale, }); return ( ); } ``` For Static Export, you can configure [static mode](/docs/headless/search/flexsearch#static-export) on search server, and change the search client: ```tsx title="components/search.tsx" 'use client'; import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, type SharedProps, } from 'fumadocs-ui/components/dialog/search'; import { useFlexsearchStatic } from 'fumadocs-core/search/client/flexsearch-static'; import { useI18n } from 'fumadocs-ui/contexts/i18n'; export default function DefaultSearchDialog(props: SharedProps) { const { locale } = useI18n(); // (optional) for i18n const search = useFlexsearchStatic({ locale }); return ( ); } ``` ### Replace Search Dialog [#replace-search-dialog] Replace the search dialog with yours from [``](/docs/ui/layouts/root-provider): ```tsx import { RootProvider } from 'fumadocs-ui/provider/'; // [!code ++] import SearchDialog from '@/components/search'; {children} ; ``` If it was in a server component, you would need a separate client component for provider to pass functions: ```tsx tab="provider.tsx" 'use client'; import { RootProvider } from 'fumadocs-ui/provider/'; import SearchDialog from '@/components/search'; import type { ReactNode } from 'react'; export function Provider({ children }: { children: ReactNode }) { return ( {children} ); } ``` ```tsx tab="app/layout.tsx" import { Provider } from './provider'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {/* [!code --] */} {children} {/* [!code ++] */} {children} ); } ``` ### Tag Filter [#tag-filter] Optionally, you can add UI for filtering results by tags. Configure [Tag Filter](/docs/headless/search/flexsearch#tag-filter) on search server and add the following: ```tsx 'use client'; import { SearchDialog, SearchDialogContent, SearchDialogFooter, SearchDialogOverlay, type SharedProps, TagsList, TagsListItem, } from 'fumadocs-ui/components/dialog/search'; import { useState } from 'react'; import { useFlexsearchStatic } from 'fumadocs-core/search/client/flexsearch-static'; export default function CustomSearchDialog(props: SharedProps) { // [!code ++] const [tag, setTag] = useState(); const search = useFlexsearchStatic({ tag, // [!code ++] }); return ( ... {/* [!code ++:3] */} My Value ); } ``` # Fumadocs (Framework Mode): Search URL: /docs/search Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/search/index.mdx Implement document search in your docs ## Search UI [#search-ui] See [Search UI](/docs/ui/search) to learn how to customize the UI of document search. ## Search Client [#search-client] You can choose & configure the search client according to your search engine, it defaults to ZBSearch. ### Search Hooks [#search-hooks] Search clients come with React hooks, like `useFetchSearch()` and `useAlgoliaSearch()`, returning the props of ``: ```tsx import { useFetchSearch } from 'fumadocs-core/search/client'; const search = useFetchSearch(); ; ``` 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](https://github.com/syntax-tree/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 => { const res = await fetch(`/api/my-search?${new URLSearchParams({ query, locale })}`); return res.json(); }, [locale], ); return experimental_useSearch(search); } ``` ### Community Integrations [#community-integrations] A list of integrations maintained by community. - [Trieve Search](/docs/headless/search/trieve) - [Typesense Search](/docs/headless/search/typesense) # Fumadocs (Framework Mode): Meilisearch URL: /docs/search/meilisearch Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/search/meilisearch.mdx Using Meilisearch with Fumadocs UI. ## Setup [#setup] Fumadocs CLI can set it up for you, it creates the search dialog, the `static.json` route and a script to sync the search index after each build: ```package-install npx @fumadocs/cli feature search --provider meilisearch ``` Or manually: 1. Integrate [Meilisearch](/docs/headless/search/meilisearch). 2. Create a search dialog, replace `host` and `apiKey` with your host URL and search API key. ```tsx title="components/search.tsx" 'use client'; import { Meilisearch } from 'meilisearch'; import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogFooter, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, type SharedProps, } from 'fumadocs-ui/components/dialog/search'; import { useMeilisearch } from 'fumadocs-core/search/client'; import { useI18n } from 'fumadocs-ui/contexts/i18n'; const client = new Meilisearch({ host: 'replace me', apiKey: 'replace me', }); export default function CustomSearchDialog(props: SharedProps) { const { locale } = useI18n(); // (optional) for i18n const search = useMeilisearch({ client, indexName: 'docs', locale, }); return ( Search powered by Meilisearch ); } ``` ### Replace Search Dialog [#replace-search-dialog] Replace the search dialog with yours from [``](/docs/ui/layouts/root-provider): ```tsx import { RootProvider } from 'fumadocs-ui/provider/'; // [!code ++] import SearchDialog from '@/components/search'; {children} ; ``` If it was in a server component, you would need a separate client component for provider to pass functions: ```tsx tab="provider.tsx" 'use client'; import { RootProvider } from 'fumadocs-ui/provider/'; import SearchDialog from '@/components/search'; import type { ReactNode } from 'react'; export function Provider({ children }: { children: ReactNode }) { return ( {children} ); } ``` ```tsx tab="app/layout.tsx" import { Provider } from './provider'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {/* [!code --] */} {children} {/* [!code ++] */} {children} ); } ``` ### Tag Filter [#tag-filter] Optionally, you can add UI for filtering results by tags. Configure [Tag Filter](/docs/headless/search/meilisearch#tag-filter) on documents and add the following: ```tsx 'use client'; import { SearchDialog, SearchDialogContent, SearchDialogFooter, SearchDialogOverlay, type SharedProps, TagsList, TagsListItem, } from 'fumadocs-ui/components/dialog/search'; import { useState } from 'react'; import { useMeilisearch } from 'fumadocs-core/search/client'; export default function CustomSearchDialog(props: SharedProps) { // [!code ++] const [tag, setTag] = useState(); const search = useMeilisearch({ tag, // [!code ++] // other options... }); return ( ... {/* [!code ++:3] */} My Value ); } ``` # Fumadocs (Framework Mode): Mixedbread URL: /docs/search/mixedbread Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/search/mixedbread.mdx Using Mixedbread with Fumadocs UI. ## Setup [#setup] Mixedbread search runs on your server at request time, it isn't available for static sites. Fumadocs CLI can set it up for you, it creates the search dialog, the search route, the `static.json` route and a script to sync your store after each build: ```package-install npx @fumadocs/cli feature search --provider mixedbread ``` Or manually: 1. Integrate [Mixedbread Search](/docs/headless/search/mixedbread). 2. Create a search dialog component. ```tsx title="components/search.tsx" 'use client'; import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogFooter, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, type SharedProps, } from 'fumadocs-ui/components/dialog/search'; import { useFetchSearch } from 'fumadocs-core/search/client'; import { useI18n } from 'fumadocs-ui/contexts/i18n'; export default function CustomSearchDialog(props: SharedProps) { const { locale } = useI18n(); // (optional) for i18n const search = useFetchSearch({ api: '/api/search', locale, // every search is a request to Mixedbread delayMs: 300, }); return ( Search powered by Mixedbread ); } ``` ### Replace Search Dialog [#replace-search-dialog] Replace the search dialog with yours from [``](/docs/ui/layouts/root-provider): ```tsx import { RootProvider } from 'fumadocs-ui/provider/'; // [!code ++] import SearchDialog from '@/components/search'; {children} ; ``` If it was in a server component, you would need a separate client component for provider to pass functions: ```tsx tab="provider.tsx" 'use client'; import { RootProvider } from 'fumadocs-ui/provider/'; import SearchDialog from '@/components/search'; import type { ReactNode } from 'react'; export function Provider({ children }: { children: ReactNode }) { return ( {children} ); } ``` ```tsx tab="app/layout.tsx" import { Provider } from './provider'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {/* [!code --] */} {children} {/* [!code ++] */} {children} ); } ``` ### Tag Filter [#tag-filter] Optionally, you can add UI for filtering results by tags. Configure [Tag Filter](/docs/headless/search/mixedbread#tag-filter) on search server and add the following: ```tsx 'use client'; import { SearchDialog, SearchDialogContent, SearchDialogFooter, SearchDialogOverlay, type SharedProps, TagsList, TagsListItem, } from 'fumadocs-ui/components/dialog/search'; import { useState } from 'react'; import { useFetchSearch } from 'fumadocs-core/search/client'; export default function CustomSearchDialog(props: SharedProps) { // [!code ++] const [tag, setTag] = useState(); const search = useFetchSearch({ tag, // [!code ++] }); return ( ... {/* [!code ++:3] */} My Value ); } ``` # Fumadocs (Framework Mode): Orama Cloud URL: /docs/search/orama-cloud Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/search/orama-cloud.mdx Using Orama Cloud with Fumadocs UI. ## Setup [#setup] Fumadocs CLI can set it up for you, it creates the search dialog, the `static.json` route and a script to sync the search index after each build: ```package-install npx @fumadocs/cli feature search --provider orama-cloud ``` Or manually: 1. Integrate [Orama Cloud](/docs/headless/search/orama-cloud). 2. Create a search dialog, replace `endpoint` and `api_key` with your desired values. ```tsx title="components/search.tsx" 'use client'; import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogFooter, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, type SharedProps, } from 'fumadocs-ui/components/dialog/search'; import { useOramaCloudSearch } from 'fumadocs-core/search/client'; import { OramaCloud } from '@orama/core'; import { useI18n } from 'fumadocs-ui/contexts/i18n'; const orama = new OramaCloud({ projectId: '', apiKey: '', }); export default function CustomSearchDialog(props: SharedProps) { const { locale } = useI18n(); // (optional) for i18n const search = useOramaCloudSearch({ client: orama, locale, }); return ( Search powered by Orama ); } ``` ### Replace Search Dialog [#replace-search-dialog] Replace the search dialog with yours from [``](/docs/ui/layouts/root-provider): ```tsx import { RootProvider } from 'fumadocs-ui/provider/'; // [!code ++] import SearchDialog from '@/components/search'; {children} ; ``` If it was in a server component, you would need a separate client component for provider to pass functions: ```tsx tab="provider.tsx" 'use client'; import { RootProvider } from 'fumadocs-ui/provider/'; import SearchDialog from '@/components/search'; import type { ReactNode } from 'react'; export function Provider({ children }: { children: ReactNode }) { return ( {children} ); } ``` ```tsx tab="app/layout.tsx" import { Provider } from './provider'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {/* [!code --] */} {children} {/* [!code ++] */} {children} ); } ``` # Fumadocs (Framework Mode): ZBSearch (default) URL: /docs/search/orama Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/search/orama.mdx The default search engine powered by ZBSearch. ## Overview [#overview] Fumadocs configures [ZBSearch search engine](/docs/headless/search/orama) out-of-the-box. It works through a API endpoint (running on server), or a statically cached JSON file for static websites. ## Setup [#setup] Create a search dialog. The UI has been configured by default, you can also re-create it for further customizations: ```tsx title="components/search.tsx" 'use client'; import { useFetchSearch } from 'fumadocs-core/search/client'; import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, type SharedProps, } from 'fumadocs-ui/components/dialog/search'; import { useI18n } from 'fumadocs-ui/contexts/i18n'; export default function DefaultSearchDialog(props: SharedProps) { const { locale } = useI18n(); // (optional) for i18n const search = useFetchSearch({ locale, }); return ( ); } ``` For Static Export, you can configure [static mode](/docs/headless/search/orama#static-export) on search server, and use the `static` client: ```tsx title="components/search.tsx" 'use client'; import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, type SharedProps, } from 'fumadocs-ui/components/dialog/search'; import { useStaticSearch } from 'fumadocs-core/search/client'; import { useI18n } from 'fumadocs-ui/contexts/i18n'; export default function DefaultSearchDialog(props: SharedProps) { const { locale } = useI18n(); // (optional) for i18n const search = useStaticSearch({ locale, }); return ( ); } ``` ### Replace Search Dialog [#replace-search-dialog] Replace the search dialog with yours from [``](/docs/ui/layouts/root-provider): ```tsx import { RootProvider } from 'fumadocs-ui/provider/'; // [!code ++] import SearchDialog from '@/components/search'; {children} ; ``` If it was in a server component, you would need a separate client component for provider to pass functions: ```tsx tab="provider.tsx" 'use client'; import { RootProvider } from 'fumadocs-ui/provider/'; import SearchDialog from '@/components/search'; import type { ReactNode } from 'react'; export function Provider({ children }: { children: ReactNode }) { return ( {children} ); } ``` ```tsx tab="app/layout.tsx" import { Provider } from './provider'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {/* [!code --] */} {children} {/* [!code ++] */} {children} ); } ``` ### Tag Filter [#tag-filter] Optionally, you can add UI for filtering results by tags. Configure [Tag Filter](/docs/headless/search/orama#tag-filter) on search server and add the following: ```tsx 'use client'; import { SearchDialog, SearchDialogContent, SearchDialogFooter, SearchDialogOverlay, type SharedProps, TagsList, TagsListItem, } from 'fumadocs-ui/components/dialog/search'; import { useState } from 'react'; import { useFetchSearch } from 'fumadocs-core/search/client'; export default function CustomSearchDialog(props: SharedProps) { // [!code ++] const [tag, setTag] = useState(); const search = useFetchSearch({ tag, // [!code ++] }); return ( ... {/* [!code ++:3] */} My Value ); } ``` # Fumadocs (Framework Mode): Typesense URL: /docs/search/typesense Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/search/typesense.mdx Using Typesense with Fumadocs UI. > This is a community-maintained integration. ## Setup [#setup] Fumadocs CLI can set it up for you, it creates the search dialog, the `static.json` route and a script to sync the search index after each build: ```package-install npx @fumadocs/cli feature search --provider typesense ``` Or manually: 1. Integrate [Typesense Search](/docs/headless/search/typesense). 2. Create a search dialog component. ```tsx title="components/search.tsx" 'use client'; import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogFooter, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, type SharedProps, } from 'fumadocs-ui/components/dialog/search'; import { useI18n } from 'fumadocs-ui/contexts/i18n'; import { Client } from 'typesense'; import { useTypesenseSearch } from 'typesense-fumadocs-adapter/client'; const client = new Client({ nodes: [{ url: 'YOUR_TYPESENSE_SERVER_URL' }], apiKey: 'YOUR_TYPESENSE_SEARCH_ONLY_API_KEY', }); export default function TypesenseSearchDialog(props: SharedProps) { const { locale } = useI18n(); // optional const { search, setSearch, query } = useTypesenseSearch({ typesenseCollectionName: 'docs', locale, legacy: false, // optional, set to true for fumadocs-ui version < 16.6.0 client, }); return ( ); } ``` ### Replace Search Dialog [#replace-search-dialog] Replace the search dialog with yours from [``](/docs/ui/layouts/root-provider): ```tsx import { RootProvider } from 'fumadocs-ui/provider/'; // [!code ++] import SearchDialog from '@/components/search'; {children} ; ``` If it was in a server component, you would need a separate client component for provider to pass functions: ```tsx tab="provider.tsx" 'use client'; import { RootProvider } from 'fumadocs-ui/provider/'; import SearchDialog from '@/components/search'; import type { ReactNode } from 'react'; export function Provider({ children }: { children: ReactNode }) { return ( {children} ); } ``` ```tsx tab="app/layout.tsx" import { Provider } from './provider'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {/* [!code --] */} {children} {/* [!code ++] */} {children} ); } ``` ### Tag Filter [#tag-filter] Optionally, you can add UI for filtering results by tags. Configure [Tag Filter](/docs/headless/search/typesense#tag-filter) on search server and add the following: ```tsx 'use client'; import { SearchDialog, SearchDialogContent, SearchDialogFooter, SearchDialogOverlay, type SharedProps, TagsList, TagsListItem, } from 'fumadocs-ui/components/dialog/search'; import { useState } from 'react'; import { useTypesenseSearch } from 'typesense-fumadocs-adapter/client'; export default function TypesenseSearchDialog(props: SharedProps) { // [!code ++] const [tag, setTag] = useState(); const { search, setSearch, query } = useTypesenseSearch({ tag, // [!code ++] // other options... }); return ( ... {/* [!code ++:3] */} My Value ); } ``` # Fumadocs (Framework Mode): What is Fumadocs URL: /docs/what-is-fumadocs Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/what-is-fumadocs.mdx Introducing Fumadocs, a docs framework that you can break. The **Fumadocs** framework refers to a combination of UI + Core + Content Source, Fumadocs is more of a mental framework and each layer can be a library on its own. ## Philosophy [#philosophy] Fumadocs was created to bring a more customisable experience for building docs, to be a docs framework that is not opinionated, **a "framework" that you can break**. ### Less Abstraction [#less-abstraction] While most frameworks are configured with a configuration file, they usually lack flexibility when you hope to tune its details. You can’t control how they render the page nor the internal logic. Fumadocs expects you to write code and cooperate with the rest of your software, it shows you how the app works and fully customisable, instead of a configuration file. ### Seamless Integration [#seamless-integration] Fumadocs integrates tightly with your React framework, bringing useful utilities and a good-looking UI. For Next.js developers, you are still using features of App Router, like **Static Site Generation**. There is nothing new for Next.js developers, that everything is familiar to you. ### Composable UI [#composable-ui] The only thing Fumadocs UI (the default theme) offers is **User Interface**. The UI is opinionated for bringing better mobile responsiveness and user experience. Instead, we follow a much more flexible approach inspired by Shadcn UI — [Fumadocs CLI](/docs/cli), allowing you to "fork" a part of Fumadocs UI, and fully customize it. ### Server-first Approach [#server-first-approach] Traditionally, static site generators are **static**. However, Fumadocs introduce a server-first approach powered by React Server Component. With perfect server-client boundary, content becomes dynamic and interactive. You can fetch data from server to display content, or integrate with CMS receiving realtime updates. It is always up-to-date and easy to maintain. ### Minimal [#minimal] Fumadocs is maintained by Fuma and many contributors, with care on the maintainability of codebase. While we don't aim to offer every functionality people wanted, we're more focused on making basic features perfect and well-maintained. You can also help Fumadocs to be more useful by contributing! ## When to use Fumadocs [#when-to-use-fumadocs] Fumadocs is designed with flexibility in mind, it is not limited to certain usages. - `fumadocs-core` is a headless UI library for building docs. - `fumadocs-mdx` is a useful library to handle MDX content. For most of the web applications, vanilla React.js is no longer enough. Nowadays, we also wish to have a blog, a showcase page, a FAQ page, etc. In these cases, Fumadocs can help you build the docs easier, with less boilerplate. You can read [Comparisons](/docs/comparisons) if you're interested. ### Documentation [#documentation] Fumadocs focuses on **authoring experience**, it provides a beautiful theme and many docs automation tools. It helps you to iterate your codebase faster while never leaving your docs behind. You can take this site as an example of docs site built with Fumadocs. ### Blog sites [#blog-sites] Most React.js frameworks can already suffice the needs of a blog site. Fumadocs provides additional tooling, including syntax highlighting, document search, and a default theme (Fumadocs UI). It helps you to avoid reinventing the wheels. # Fumadocs (Framework Mode): Scalar Example URL: /docs/asyncapi Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/asyncapi/index.mdx View the Scalar Galaxy example AsyncAPI schema. # Fumadocs CLI (the CLI tool for automating Fumadocs apps): create-fumadocs-app URL: /docs/cli/create-fumadocs-app Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/cli/create-fumadocs-app.mdx The CLI to create new Fumadocs apps ## Usage [#usage] ```npm npx create-fumadocs-app@latest my-docs ``` It asks for the template and features to configure, you can also pass them as options. ### Options [#options] | Option | Description | | ------------------- | ----------------------------------------------------------------------------------------- | | `--template ` | The template to use, see [Templates](#templates). | | `--src` | (Next.js only) Use the `src/` directory. | | `--linter ` | Configure a linter: `eslint` (Next.js only), `oxlint` or `biome`. | | `--search ` | Search solution: `orama` (default, local search) or `orama-cloud`. | | `--og-image ` | OG image: `next-og` (Next.js only) or `takumi`, others use Takumi. | | `--ai-chat ` | AI chat: `openrouter`, `llmgateway` or `inkeep`, not available on static & SPA templates. | | `--install` | Install dependencies automatically. | | `--no-git` | Skip Git repository initialization. | | `--pm ` | Package manager: `npm`, `pnpm`, `yarn` or `bun`, detected from how the CLI is invoked. | | `-y, --yes` | Skip prompts, use defaults for options not specified. | ### Templates [#templates] | Value | Description | | ---------------------------- | ------------------------------------------ | | `+next+fuma-docs-mdx` | Next.js (default) | | `+next+fuma-docs-mdx+static` | Next.js with static export | | `waku` | Waku | | `react-router` | React Router | | `react-router-spa` | React Router in SPA mode | | `tanstack-start` | Tanstack Start | | `tanstack-start-spa` | Tanstack Start in SPA mode | | `astro` | Astro with React islands (partial support) | ### Non-interactive [#non-interactive] Pass `--yes` to skip prompts, useful for scripts, CI and AI agents: ```npm npx create-fumadocs-app@latest my-docs --template +next+fuma-docs-mdx --install --yes ``` Options you didn't specify use their defaults: no linter, local search, no AI chat. It fails when the target directory already exists and is not empty. ## Programmatic API [#programmatic-api] Despite the classical CLI usage, you can also use it in scripts like: ```npm npm install create-fumadocs-app ``` ```ts import { create } from 'create-fumadocs-app'; await create({ outputDir: 'my-app', template: '+next+fuma-docs-mdx', packageManager: 'pnpm', }); ``` # Fumadocs CLI (the CLI tool for automating Fumadocs apps): User Guide URL: /docs/cli Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/cli/index.mdx The CLI tool that automates setups and installs components. ## Installation [#installation] Initialize a config for CLI: ```package-install npx @fumadocs/cli ``` You can change the output paths of components in the config. ### Init [#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](https://github.com/fuma-nama/fumadocs/tree/dev/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 `` element of your root layout, for the theme switch. - `.source` in `.gitignore`. Pass `--i18n` to set up [internationalization](/docs/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 [#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](/docs/integrations/llms#ask-ai) dialog, `--provider`. | | `llms` | [LLM routes](/docs/integrations/llms): `llms.txt`, `llms-full.txt` and per-page Markdown. | | `mcp` | [MCP server](/docs/integrations/llms#mcp-server) to search and read the docs. | | `webmcp` | [WebMCP](/docs/integrations/llms#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](/docs/search), `--provider`. | | `feedback` | [Feedback](/docs/integrations/feedback) component on docs pages. | | `epub` | Route to [export EPUB](/docs/guides/export-epub). | | `lint` | Configure ESLint (Next.js only), Biome or Oxlint, `--linter`. | Features adapt to your setup: - Fumadocs MDX with the [Macro API](/docs/mdx/macro) 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 [#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? [#how-the-magic-works] The CLI fetches the latest version of component from the GitHub repository of Fumadocs. 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 [#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 [#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 [#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 [#example-output] ```tsx title="output.tsx" import { File, Folder, Files } from 'fumadocs-ui/components/files'; export default ( ); ``` # Fumadocs CLI (the CLI tool for automating Fumadocs apps): Preview URL: /docs/cli/preview Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/cli/preview.mdx A light tool to view Markdown files. ## Introduction [#introduction] `fumadocs-preview` allows you to view Markdown files with Fumadocs, it is useful for reading skills files & notes. Run the command under your project directory. ```npm npx fumadocs-preview dir/to/content ``` ### Configurations [#configurations] For other configurations, you can install it locally: ```npm npm install fumadocs-preview -D ``` Then init the config file: ```npm npx fumadocs-preview init ``` # Fumadocs (Framework Mode): Store Example URL: /docs/graphql Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/graphql/index.mdx View the example store GraphQL schema. # Fumadocs Core (the core library of Fumadocs): Breadcrumb URL: /docs/headless/components/breadcrumb Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/components/breadcrumb.mdx The navigation component at the top of the screen A hook for implementing Breadcrumb in your documentation. It returns breadcrumb items for a page based on the given page tree. > If present, the index page of a folder will be used as the item. ## Usage [#usage] It exports a `useBreadcrumb` hook: ```ts twoslash declare const tree: any; import { usePathname } from 'next/navigation'; // ---cut--- import { useBreadcrumb } from 'fumadocs-core/breadcrumb'; // obtain `pathname` using the hook provided by your React framework. const pathname = usePathname(); const items = useBreadcrumb(pathname, tree); // ^? ``` ### Example [#example] A styled example for Next.js. ```tsx 'use client'; import { usePathname } from 'next/navigation'; import { useBreadcrumb } from 'fumadocs-core/breadcrumb'; import type { PageTree } from 'fumadocs-core/page-tree'; import { Fragment } from 'react'; import { ChevronRight } from 'lucide-react'; import Link from 'next/link'; export function Breadcrumb({ tree }: { tree: PageTree.Root }) { const pathname = usePathname(); const items = useBreadcrumb(pathname, tree); if (items.length === 0) return null; return (
{items.map((item, i) => ( {i !== 0 && } {item.url ? ( {item.name} ) : ( {item.name} )} ))}
); } ``` You can use it by passing the page tree via `tree` prop in a server component. ### Breadcrumb Item [#breadcrumb-item] ### BreadcrumbItem | Prop | Type | Description | | ------ | ----------- | ----------- | | `name` | `ReactNode` | | | `url?` | `string` | | # Fumadocs Core (the core library of Fumadocs): Components URL: /docs/headless/components Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/components/index.mdx Blocks for your docs # Fumadocs Core (the core library of Fumadocs): Link URL: /docs/headless/components/link Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/components/link.mdx A Link component that handles external links A component that wraps the Link component of your React framework (e.g. `next/link`) and automatically handles external links in the document.~ When an external URL is detected, it uses `` instead of the Link Component. The `rel` property is automatically generated. ## Usage [#usage] Usage is the same as using ``. ```mdx import Link from 'fumadocs-core/link'; Click Me ``` ### External [#external] You can force a URL to be external by passing an `external` prop. ### Dynamic hrefs [#dynamic-hrefs] You can enable dynamic hrefs by importing `dynamic-link`. ```mdx import { DynamicLink } from 'fumadocs-core/dynamic-link'; Click Me ``` # Fumadocs Core (the core library of Fumadocs): TOC URL: /docs/headless/components/toc Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/components/toc.mdx Table of Contents A Table of Contents with active anchor observer and auto scroll. ## Usage [#usage] ```tsx import * as Base from 'fumadocs-core/toc'; return ( ); ``` ### Anchor Provider [#anchor-provider] Watches for the active anchor using the Intersection Observer API. ### AnchorProviderProps | Prop | Type | Description | | ----------- | ----------- | ---------------------------------------------------- | | `toc` | `array` | | | `single?` | `union` | Only accept one active item at most Default: `false` | | `children?` | `ReactNode` | | ### Scroll Provider [#scroll-provider] Scrolls the scroll container to the active anchor. ### ScrollProviderProps | Prop | Type | Description | | -------------- | ----------- | --------------------------------------------- | | `containerRef` | `object` | Scroll into the view of container when active | | `children?` | `ReactNode` | | ### TOC Item [#toc-item] An anchor item for jumping to the target anchor. | Data Attribute | Values | Description | | -------------- | ------------- | -------------------- | | `data-active` | `true, false` | Is the anchor active | ## Example [#example] ```tsx import { AnchorProvider, ScrollProvider, TOCItem, type TOCItemType } from 'fumadocs-core/toc'; import { type ReactNode, useRef } from 'react'; export function Page({ items, children }: { items: TOCItemType[]; children: ReactNode }) { const viewRef = useRef(null); return (
{items.map((item) => ( {item.title} ))}
{children}
); } ``` # Fumadocs Core (the core library of Fumadocs): Content Collections URL: /docs/headless/content-collections Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/content-collections/index.mdx Use Content Collections for Fumadocs [Content Collections](https://www.content-collections.dev) is a library that transforms your content into type-safe data collections. ## Setup [#setup] Follow their [official guide](https://content-collections.dev/docs) 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`. ```ts title="content-collections.ts" import { defineCollection, defineConfig } from '@content-collections/core'; import { transformMDX } from '@fumadocs/content-collections/configuration'; import { metaSchema, pageSchema } from 'fumadocs-core/source/schema'; const docs = defineCollection({ name: 'docs', directory: 'content/docs', include: '**/*.mdx', schema: pageSchema, transform: transformMDX, }); const metas = defineCollection({ name: 'meta', directory: 'content/docs', include: '**/meta.json', parser: 'json', schema: metaSchema, }); export default defineConfig({ collections: [docs, metas], }); ``` And pass it to `loader()`. ```ts title="lib/source.ts" import { allDocs, allMetas } from 'content-collections'; import { loader } from 'fumadocs-core/source'; import { createMDXSource } from '@fumadocs/content-collections'; export const source = loader({ baseUrl: '/docs', source: createMDXSource(allDocs, allMetas), }); ``` 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 [#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 [#import-components] To use components from other packages like Fumadocs UI, pass them to your `` component. ```tsx import { MDXContent } from '@content-collections/mdx/react'; import { getMDXComponents } from '@/components/mdx'; return ; ``` You can also import them in MDX Files, but it is not recommended. Content Collections uses `mdx-bundler` to bundle MDX files. 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. # Fumadocs Core (the core library of Fumadocs): Introduction URL: /docs/headless Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/index.mdx Getting started with core library ## What is this? [#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. - [Search](/docs/headless/search) - Breadcrumb, Sidebar, TOC Components - Remark/Rehype Plugins - [MCP tools](/docs/headless/utils/mcp) for AI agents to search and read docs - [Additional utilities](/docs/headless/utils) It can be used without Fumadocs UI, in other words, it's headless. For beginners and normal usages, use [Fumadocs UI](/docs/ui). ## Installation [#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 `` return {children}; } ``` ```tsx tab="React Router" import type { ReactNode } from 'react'; import { ReactRouterProvider } from 'fumadocs-core/framework/react-router'; export function Root({ children }: { children: ReactNode }) { return {children}; } ``` ```tsx tab="Tanstack Start/Router" import type { ReactNode } from 'react'; import { TanstackProvider } from 'fumadocs-core/framework/tanstack'; export function Root({ children }: { children: ReactNode }) { return {children}; } ``` ```tsx tab="Waku" import type { ReactNode } from 'react'; import { WakuProvider } from 'fumadocs-core/framework/waku'; export function Root({ children }: { children: ReactNode }) { return {children}; } ``` ```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; }) { return ( {children} ); } ``` Explore the APIs and components: # Fumadocs Core (the core library of Fumadocs): Configuration URL: /docs/headless/internationalization/config Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/internationalization/config.mdx Shared i18n configuration For framework integration guide, see [Internationalization](/docs/internationalization). ## Define Config [#define-config] Fumadocs core provides necessary middleware and utilities for i18n support. You can define a config to share between utilities. ```ts title="lib/i18n.ts" import { defineI18n } from 'fumadocs-core/i18n'; export const i18n = defineI18n({ defaultLanguage: 'en', languages: ['en', 'cn'], }); ``` ### Hide Locale Prefix [#hide-locale-prefix] To hide the locale prefix (e.g. `/en/page` -> `/page`), use the `hideLocale` option. ```ts import { defineI18n } from 'fumadocs-core/i18n'; export const i18n = defineI18n({ defaultLanguage: 'en', languages: ['en', 'cn'], hideLocale: 'default-locale', }); ``` | Mode | Description | | ---------------- | -------------------------------------------------- | | `always` | Always hide the prefix, detect locale from cookies | | `default-locale` | Only hide the default locale | | `never` | Never hide the prefix (default) | Using always}> On `always` mode, locale is stored as a cookie (set by the middleware), which isn't optimal for static sites. This may cause undesired cache problems, and need to pay extra attention on SEO to ensure search engines can index your pages correctly. ### Fallback Language [#fallback-language] The fallback language to use when translations are missing for a file, default to your `defaultLanguage`. It accepts one of the languages in your `languages` array. ```ts import { defineI18n } from 'fumadocs-core/i18n'; export const i18n = defineI18n({ languages: ['en', 'cn'], defaultLanguage: 'en', // Chinese variant is used if a file is missing in English fallbackLanguage: 'cn', }); ``` To disable the fallback behaviour, set it to `null`: ```ts import { defineI18n } from 'fumadocs-core/i18n'; export const i18n = defineI18n({ languages: ['en', 'cn'], defaultLanguage: 'en', fallbackLanguage: null, }); ``` When disabled, no fallback will be used (including `meta.json`). You can still create a shared file for every locale with `$`: # Fumadocs Core (the core library of Fumadocs): Middleware URL: /docs/headless/internationalization/middleware Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/internationalization/middleware.mdx Next.js proxy for implementing i18n routing ## Setup [#setup] Redirects users to appropriate locale, it can be customized from `i18n.ts` config file. ```ts title="proxy.ts" import { createI18nMiddleware } from 'fumadocs-core/i18n/middleware'; import { i18n } from '@/lib/i18n'; export default createI18nMiddleware(i18n); export const config = { // Matcher ignoring `/_next/` and `/api/` // You may need to adjust it to ignore static assets in `/public` folder matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], }; ``` > When `hideLocale` is enabled, it uses `NextResponse.rewrite` to hide locale prefixes. # Fumadocs Core (the core library of Fumadocs): Headings URL: /docs/headless/mdx/headings Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/headings.mdx Process headings from your document ## Remark Heading [#remark-heading] Applies ids to headings. ```ts title="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkHeading } from 'fumadocs-core/mdx-plugins'; await compile('...', { remarkPlugins: [remarkHeading], }); ``` > This plugin is included by default on Fumadocs MDX. ### Extract TOC [#extract-toc] By default, it extracts the headings (table of contents) of a document to `vfile.data.toc`. You can disable it with: ```ts import { remarkHeading } from 'fumadocs-core/mdx-plugins'; export default { remarkPlugins: [[remarkHeading, { generateToc: false }]], }; ``` ### Custom Ids [#custom-heading-id] You can customize the heading id with `[#slug]`. ```md # heading [#slug] ``` ### Output [#output] An array of `TOCItemType`. ### TOCItemType | Prop | Type | Description | | -------- | ----------- | ------------------------------ | | `title` | `ReactNode` | | | `url` | `string` | | | `depth` | `number` | | | `_step?` | `number` | [remark-steps] the step number | ## Rehype TOC [#rehype-toc] Exports table of contents (an array of `TOCItemType`), it allows JSX nodes which is not possible with a Remark plugin. > It requires MDX.js. ### Usage [#usage] ```ts import { rehypeToc } from 'fumadocs-core/mdx-plugins'; export default { rehypePlugins: [rehypeToc], }; ``` ### Output [#output-1] For a Markdown document: ```md ## Hello `code` ``` An export will be created: ```jsx export const toc = [ { title: ( <> Hello code ), depth: 2, url: '#hello-code', }, ]; ``` # Fumadocs Core (the core library of Fumadocs): MDX Plugins URL: /docs/headless/mdx Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/index.mdx Useful remark & rehype plugins for your docs. # Fumadocs Core (the core library of Fumadocs): Package Install URL: /docs/headless/mdx/install Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/install.mdx Generate code blocks for installing packages For Fumadocs MDX, it is now enabled by default. You can use the [`remarkNpm`](/docs/headless/mdx/remark-npm) plugin instead. ## Usage [#usage] ```package-install fumadocs-docgen ``` Add the remark plugin. ```ts title="source.config.ts" tab="Fumadocs MDX" import { remarkInstall } from 'fumadocs-docgen'; import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { remarkPlugins: [remarkInstall], }, }); ``` ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkInstall } from 'fumadocs-docgen'; await compile('...', { remarkPlugins: [remarkInstall], }); ``` Define the required components. ```tsx title="components/mdx.tsx (Fumadocs UI)" import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import defaultComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultComponents, // [!code ++:2] Tab, Tabs, ...components, } satisfies MDXComponents; } ``` | Component | | | --------- | ---------------------------------- | | Tabs | Accepts an array of item (`items`) | | Tab | Accepts the name of item (`value`) | Create code blocks with `package-install` as language. ````mdx ```package-install my-package ``` ```package-install npm i my-package -D ``` ```` ### Output [#output] The following structure should be generated by the plugin. ```mdx ... ... ... ... ``` ```package-install my-package ``` ## Options [#options] ### Persistent [#persistent] When using with Fumadocs UI, you can enable persistence with the `persist` option. ```ts title="source.config.ts" tab="Fumadocs MDX" import { remarkInstall } from 'fumadocs-docgen'; import { defineConfig } from 'fumadocs-mdx/config'; const remarkInstallOptions = { persist: { id: 'some-id', }, }; export default defineConfig({ mdxOptions: { remarkPlugins: [[remarkInstall, remarkInstallOptions]], }, }); ``` ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkInstall } from 'fumadocs-docgen'; const remarkInstallOptions = { persist: { id: 'some-id', }, }; await compile('...', { remarkPlugins: [[remarkInstall, remarkInstallOptions]], }); ``` This will instead generate: ```mdx ... ``` # Fumadocs Core (the core library of Fumadocs): Rehype Code URL: /docs/headless/mdx/rehype-code Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/rehype-code.mdx Code syntax highlighter A wrapper of [`@shikijs/rehype`](https://shiki.style/packages/rehype), the syntax highlighter used by Fumadocs. It converts raw `
` codeblocks into highlighted codeblocks:

````md tab="Input"
```ts
console.log('Hello World');
```
````

```html tab="Output"
  
    
      console...
    
  
``` ## Usage [#usage] Add the rehype plugin. ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { rehypeCode, type RehypeCodeOptions } from 'fumadocs-core/mdx-plugins'; const rehypeCodeOptions: RehypeCodeOptions = { themes: { light: 'github-light', dark: 'github-dark', }, }; await compile('...', { rehypePlugins: [ // using default settings rehypeCode, // or with custom options [rehypeCode, rehypeCodeOptions], ], }); ``` ```ts tab="Fumadocs MDX" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { rehypeCodeOptions: { // enabled by default on Fumadocs MDX // configure options here }, }, }); ``` ### Meta [#meta] It parses the `title` meta string, and adds it to the `pre` element as an attribute. ````mdx tab="Input" ```js title="Title" console.log('Hello'); ``` ```` ```jsx tab="Output"
...
``` You may filter the meta string before processing it with the `filterMetaString` option. ### Inline Code [#inline-code] You can enable it with `inline` option: ```ts import { type RehypeCodeOptions } from 'fumadocs-core/mdx-plugins'; const rehypeCodeOptions: RehypeCodeOptions = { // [!code ++] inline: 'tailing-curly-colon', }; ``` ```md title="Syntax" This is a highlighted inline code: `console.log("hello world"){:js}`. ``` This is a highlighted inline code: `console.log("hello world"){:js}`. ### Icon [#icon] The plugin will automatically adds an `icon` attribute according to the language meta string. It is a HTML string, you can render it with React `dangerouslySetInnerHTML`. ````md tab="Input" ```ts console.log('This should shows the logo of TypeScript'); ``` ```` ```jsx tab="Output"
...
``` Disable or customize icons with the `icon` option. ### More Options [#more-options] The options are inherited from [Shiki](https://shiki.style), see their docs for other options. # Fumadocs Core (the core library of Fumadocs): Remark Admonition URL: /docs/headless/mdx/remark-admonition Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/remark-admonition.mdx Use Admonition in Fumadocs In Docusaurus, there's an [Admonition syntax](https://docusaurus.io/docs/markdown-features/admonitions). For people migrating from Docusaurus, you can enable this remark plugin to support the Admonition syntax. ## Usage [#usage] It requires `remark-directive` to work. ```npm npm i remark-directive ``` Configure both plugins in your config. ```ts title="source.config.ts" tab="Fumadocs MDX" import remarkDirective from 'remark-directive'; import { remarkDirectiveAdmonition } from 'fumadocs-core/mdx-plugins'; import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { remarkPlugins: [remarkDirective, remarkDirectiveAdmonition], }, }); ``` ```ts tab="MDX Compiler" import remarkDirective from 'remark-directive'; import { remarkDirectiveAdmonition } from 'fumadocs-core/mdx-plugins'; import { compile } from '@mdx-js/mdx'; await compile('...', { remarkPlugins: [remarkDirective, remarkDirectiveAdmonition], }); ``` ### Example [#example] ```md tab="Input" :::tip[This is a `title`] Hello World ::: :::warning Hello World ::: ``` ```mdx tab="Output" This is a `title` Hello World Hello World ``` ### When to use [#when-to-use] We highly recommend using the JSX syntax of MDX instead. It's more flexible, some editors support IntelliSense in MDX files. ```mdx Hello World ``` # Fumadocs Core (the core library of Fumadocs): Remark Image URL: /docs/headless/mdx/remark-image Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/remark-image.mdx Adding size attributes to images. This plugin adds `width` and `height` attributes to your image elements, which is needed for Image Optimization on Next.js and some other frameworks. ## Usage [#usage] Add it to your Remark plugins. ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkImage } from 'fumadocs-core/mdx-plugins'; await compile('...', { remarkPlugins: [remarkImage], }); ``` ```ts title="source.config.ts" tab="Fumadocs MDX" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { remarkImageOptions: { // it is enabled by default, customize it here }, }, }); ``` Supported: - Local Images - External URLs - Next.js static imports ### How It Works [#how-it-works] For Next.js, it uses **static imports** to import local images, which supports the `placeholder` option of Next.js Image. Next.js can handle image imports with its built-in image loader. Otherwise, it uses the file system or an HTTP request to download the image and obtain its size. ### Options [#options] ### RemarkImageOptions | Prop | Type | Description | | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `publicDir?` | `string` | Directory or base URL to resolve absolute image paths | | `placeholder?` | `union` | Preferred placeholder type, only available with `useImport` + local images. Default: `'none'` | | `onError?` | `union` | Define how to handle errors when fetching image size. - `error` (default): throw an error. - `ignore`: do absolutely nothing (Next.js Image component may complain). - `hide`: remove that image element. Default: `'error'` | | `useImport?` | `union` | Import images in the file, and let bundlers handle it. ```tsx import MyImage from "./public/img.png"; ``` When disabled, `placeholder` will be ignored. Default: `true` | | `external?` | `union` | Fetch image size of external URLs Default: `true` | ### Example: With Imports [#example-with-imports] ```mdx ![Hello](/hello.png) ![Test](https://example.com/image.png) ``` Yields: ```mdx import HelloImage from './public/hello.png'; Hello Test ``` Where `./public/hello.png` points to the image in public directory. ### Example: Without Imports [#example-without-imports] For Next.js, you can disable static imports on local images. ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkImage } from 'fumadocs-core/mdx-plugins'; await compile('...', { remarkPlugins: [[remarkImage, { useImport: false }]], }); ``` ```ts title="source.config.ts" tab="Fumadocs MDX" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { remarkImageOptions: { useImport: false, }, }, }); ``` ```mdx ![Hello](/hello.png) ![Test](https://example.com/image.png) ``` Yields: ```mdx Hello Test ``` ### Example: Relative Paths [#example-relative-paths] When `useImport` is enabled, you can reference local images using relative paths. ```mdx ![Hello](./hello.png) ``` Be careful that using it with `useImport` disabled **doesn't work**. Next.js will not add the image to public assets unless you have imported it in code. For images in public directory, you can just reference them without relative paths. ### Example: Public Directory [#example-public-directory] Customize the path of public directory ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkImage } from 'fumadocs-core/mdx-plugins'; import path from 'node:path'; await compile('...', { remarkPlugins: [[remarkImage, { publicDir: path.join(process.cwd(), 'dir') }]], }); ``` ```ts title="source.config.ts" tab="Fumadocs MDX" import { defineConfig } from 'fumadocs-mdx/config'; import path from 'node:path'; export default defineConfig({ mdxOptions: { remarkImageOptions: { publicDir: path.join(process.cwd(), 'dir'), }, }, }); ``` You can pass a URL too. ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkImage } from 'fumadocs-core/mdx-plugins'; await compile('...', { remarkPlugins: [[remarkImage, { publicDir: 'https://my-cdn.com/images' }]], }); ``` ```ts title="source.config.ts" tab="Fumadocs MDX" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { remarkImageOptions: { publicDir: 'https://my-cdn.com/images', }, }, }); ``` # Fumadocs Core (the core library of Fumadocs): Remark LLMs URL: /docs/headless/mdx/remark-llms Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/remark-llms.mdx Export the Markdown of your documents for LLMs. The `remarkLLMs` plugin exports the Markdown of a document as `_markdown`, for LLMs. It keeps the Markdown you wrote, see [Stringification](/docs/headless/mdx/stringification) for how it's generated. ```mdx tab="Input" import { Callout } from 'fumadocs-ui/components/callout'; ## Getting Started Hello **world**. ``` ```md tab="Output" ## Getting Started [#getting-started] Hello **world**. ``` ESM is removed, headings get their IDs (see `headingIds`), and included content is expanded with Fumadocs MDX. ## Usage [#usage] ```ts title="source.config.ts" tab="Fumadocs MDX" import { defineDocs } from 'fumadocs-mdx/config'; export default defineDocs({ docs: { postprocess: { // `true`, or the options of `remarkLLMs` includeProcessedMarkdown: true, }, }, }); ``` ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkLLMs } from 'fumadocs-core/mdx-plugins/remark-llms'; await compile('...', { remarkPlugins: [remarkLLMs], }); ``` With Fumadocs MDX, read it from your pages: ```ts const markdown = await page.data.getText('processed'); ``` ## JSX Elements [#jsx-elements] JSX elements are kept as you wrote them. To change them: | Option | Elements | | -------------------- | --------------------------------------------- | | `filterElement` | Removed, or replaced by their children | | `mdxAsPlaceholder` | Turned into placeholders, rendered at runtime | | `output: 'function'` | Rendered by their components, into Markdown | ### Placeholders [#placeholders] List the components to output as placeholders: ```ts import type { LLMsOptions } from 'fumadocs-core/mdx-plugins/remark-llms'; const llmsOptions: LLMsOptions = { mdxAsPlaceholder: ['Callout'], }; ``` Then render them, from runtime data if needed: ```ts import { renderPlaceholder } from 'fumadocs-core/mdx-plugins/remark-llms.runtime'; const markdown = await renderPlaceholder(await page.data.getText('processed'), { // `children` is the Markdown of its content Callout: ({ attributes, children }) => `> **${attributes.title}**\n>\n> ${children}`, }); ``` ### Function Output [#output] With `output: 'function'`, `_markdown` is a component: Markdown stays as you wrote it, JSX elements are rendered by your components. ```ts import type { LLMsOptions } from 'fumadocs-core/mdx-plugins/remark-llms'; const llmsOptions: LLMsOptions = { output: 'function', }; ``` Render it with your components: ```tsx tab="Fumadocs MDX" import { getMDXComponents } from '@/components/mdx'; const markdown = await page.data.getText('processed', { components: getMDXComponents(), }); ``` ```tsx tab="MDX Compiler" import { renderToMarkdown } from 'fumadocs-core/server'; import { getMDXComponents } from '@/components/mdx'; import { _markdown as Markdown } from './page.mdx'; const markdown = await renderToMarkdown(); ``` Components define their Markdown form with `asMarkdown()`, others are kept as JSX, see [Markdown Rendering](/docs/headless/utils/markdown). ## Options [#options] ### $Fumadocs | Prop | Type | Description | | ------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `as?` | `string` | export name for output Markdown. Default: `_markdown` | | `headingIds?` | `union` | Explicit heading IDs in output. Default: `true` | | `filterElement?` | `function` | Filter the elements to be included in the output: - `true`: include element & its children. - `children-only`: exclude element but keep its children. - `false`: exclude element & its children. ESM nodes are always excluded. | | `mdxAsPlaceholder?` | `array` | Tag names of MDX components to be output as placeholders, render them with `renderPlaceholder()`. Ignored with `output: 'function'`. | | `output?` | `union` | Form of the export: - `string`: the output Markdown. - `function`: a component: Markdown content is kept as authored, while JSX elements are kept as JSX, receiving their original props and resolving from `props.components`. Render it with `renderToMarkdown` from `fumadocs-core/server`, where a component can call `asMarkdown()` to define its own Markdown form. Default: `string` | # Fumadocs Core (the core library of Fumadocs): Remark Files URL: /docs/headless/mdx/remark-mdx-files Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/remark-mdx-files.mdx Generate files from codeblocks. ## Introduction [#introduction] This plugin takes a codeblock like: ````md ```files project ├── src │ ├── index.js │ └── utils │ └── helper.js ├── package.json ``` ```` and convert into: ```mdx ``` ## Setup [#setup] Add the remark plugin: ```tsx tab="Fumadocs MDX" title="source.config.ts" import { remarkMdxFiles } from 'fumadocs-core/mdx-plugins'; import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { remarkPlugins: [remarkMdxFiles], }, }); ``` ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkMdxFiles } from 'fumadocs-core/mdx-plugins'; await compile('...', { remarkPlugins: [remarkMdxFiles], }); ``` And make sure you have defined ``, ``, `` MDX components. # Fumadocs Core (the core library of Fumadocs): Remark NPM URL: /docs/headless/mdx/remark-npm Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/remark-npm.mdx Generate code blocks for package managers ## Usage [#usage] Add the remark plugin. ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkNpm } from 'fumadocs-core/mdx-plugins'; await compile('...', { remarkPlugins: [remarkNpm], }); ``` ```ts title="source.config.ts" tab="Fumadocs MDX" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { remarkNpmOptions: { // it is enabled by default, customize it here }, }, }); ``` Define the required components: ```tsx tab="Custom" title="components/mdx.tsx" import { CodeBlockTabs, CodeBlockTab, CodeBlockTabsList, CodeBlockTabsTrigger } from 'my-ui'; import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { CodeBlockTabs, CodeBlockTab, CodeBlockTabsList, CodeBlockTabsTrigger, ...components, } satisfies MDXComponents; } ``` ```tsx tab="Fumadocs UI" title="components/mdx.tsx" import defaultComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { // it's included by default in `defaultComponents` ...defaultComponents, ...components, } satisfies MDXComponents; } ``` | Component | | | -------------------- | ----------------------------------------------- | | CodeBlockTabs | Accepts a `defaultValue` prop for default value | | CodeBlockTabsList | N/A | | CodeBlockTab | Accepts a `value` prop | | CodeBlockTabsTrigger | Accepts a `value` prop | ### Input [#input] Create code blocks with `npm` as language. ````mdx ```npm npm i my-package ``` ```npm npm i my-package -D ``` ```` ### Output [#output] The following structure should be generated by the plugin. ```mdx npm pnpm yarn bun ... ... ... ... ``` ```npm npm i my-package ``` ### Persistent [#persistent] When using with Fumadocs UI, you can enable persist the selected value with the `persist` option. ```ts title="source.config.ts" tab="Fumadocs MDX" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { remarkNpmOptions: { persist: { id: 'package-manager', }, }, }, }); ``` ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkNpm, type RemarkNpmOptions } from 'fumadocs-core/mdx-plugins'; const remarkNpmOptions: RemarkNpmOptions = { persist: { id: 'package-manager', }, }; await compile('...', { remarkPlugins: [[remarkNpm, remarkNpmOptions]], }); ``` This will generate a [`persist`](/docs/ui/components/tabs#persistent) prop to ``. # Fumadocs Core (the core library of Fumadocs): Remark Steps URL: /docs/headless/mdx/remark-steps Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/remark-steps.mdx Generate steps from headings ## Usage [#usage] Add the remark plugin. ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkSteps } from 'fumadocs-core/mdx-plugins/remark-steps'; await compile('...', { remarkPlugins: [remarkSteps], }); ``` ```ts title="source.config.ts" tab="Fumadocs MDX" import { defineConfig } from 'fumadocs-mdx/config'; import { remarkSteps } from 'fumadocs-core/mdx-plugins/remark-steps'; export default defineConfig({ mdxOptions: { remarkPlugins: [remarkSteps], }, }); ``` ### Input [#input] Create step headings: ```md tab="Using Tag" # Heading 1 [step] ## Sub Heading 1 [step] ## Sub Heading 2 [step] # Heading 2 [step] ``` ```md tab="Using Number" # 1. Heading 1 ## 1. Sub Heading 1 ## 2. Sub Heading 2 # 2. Heading 2 ``` ### Output [#output] The following structure should be generated by the plugin. ```mdx
# Heading 1
## Sub Heading 1
## Sub Heading 2
# Heading 2
``` # Fumadocs Core (the core library of Fumadocs): Remark TS to JS URL: /docs/headless/mdx/remark-ts2js Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/remark-ts2js.mdx A remark plugin to transform TypeScript codeblocks into two tabs of codeblock with its JavaScript variant. ## Usage [#usage] Install dependencies: ```npm tab="Fumadocs MDX" npm i fumadocs-docgen ``` ```npm tab="Satteri" npm i @fumadocs/satteri ``` Add the remark plugin: ```ts title="source.config.ts" tab="Fumadocs MDX" import { remarkTypeScriptToJavaScript } from 'fumadocs-docgen/remark-ts2js'; import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { remarkPlugins: [remarkTypeScriptToJavaScript], }, }); ``` ```ts title="source.config.ts" tab="Satteri" import { remarkTs2js } from '@fumadocs/satteri/remark-ts2js'; export default defineConfig({ docs: { compiler: 'satteri', async satteriOptions() { return { mdastPlugins: [remarkTs2js()], }; }, }, }); ``` ```ts tab="MDX Compiler" import { remarkTypeScriptToJavaScript } from 'fumadocs-docgen/remark-ts2js'; import { compile } from '@mdx-js/mdx'; await compile('...', { remarkPlugins: [remarkTypeScriptToJavaScript], }); ``` Finally, make sure to define the required MDX components: `Tabs` and `Tab`. ```tsx title="components/mdx.tsx (Fumadocs UI)" import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import defaultComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultComponents, // [!code ++:2] Tab, Tabs, ...components, } satisfies MDXComponents; } ``` You can now enable it on TypeScript/TSX codeblocks, like: ````md ```tsx ts2js import { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return
{children}
; } ``` ```` ```tsx ts2js import { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return
{children}
; } ``` # Fumadocs Core (the core library of Fumadocs): Stringification URL: /docs/headless/mdx/stringification Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/stringification.mdx How Fumadocs turns your documents into Markdown. [Remark LLMs](/docs/headless/mdx/remark-llms) and [Remark Structure](/docs/headless/mdx/structure) output Markdown, for LLMs and search. Instead of stringifying the syntax tree, they slice your authored source: - Markdown stays as you wrote it, nothing is escaped or reformatted. - Remark plugins change it only by recording edits. - It's fast, no syntax tree is serialized. ## How It Works [#how-it-works] The output is the source of your document, with the edits of plugins applied. Nodes are located by their `position`: - A node with a `position` outputs its source. - A node without one, like those inserted by plugins, outputs nothing. Its plugin defines its Markdown instead. For example, [Remark Image](/docs/headless/mdx/remark-image) turns `![alt](./image.png)` into an `` element, the output keeps `![alt](./image.png)`. ## Editing the Output [#editing-the-output] A plugin replacing authored content records the Markdown of its node 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... }); }; } ``` - A string replaces the node, along with the edits inside it. - A function runs after every plugin. `s.stringify(node)` and `s.inner(node)` (its children) give their Markdown, with the edits inside. - When a node is edited more than once, the last edit wins. To edit a namespace only, like `search` for the records of Remark Structure, pass it as the last argument: ```ts // the Markdown for LLMs keeps the tag, search records don't replaceSource(file, node, (s) => s.stringify(node).replace(' [step]', ''), 'search'); ``` For search, a plugin adds the records of its nodes with [`data.structuredData`](/docs/headless/mdx/structure#plugins). ## Embedded Content [#embedded-content] Nodes parsed from another source, like an [included file](/docs/mdx/include), have positions in that source. Embed them with `embedSource()` to stringify them like your own content: ```ts import { embedSource } from 'fumadocs-core/mdx-plugins/stringifier'; // replaces `node` with the nodes parsed from `otherSource` embedSource(file, node, otherSource, otherTree.children); ``` Other nodes parsed from a string must not keep their `position`, it doesn't point into your document. The Sätteri compiler of Fumadocs MDX works the same way, with its own versions of these plugins. # Fumadocs Core (the core library of Fumadocs): Remark Structure URL: /docs/headless/mdx/structure Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/mdx/structure.mdx Extract search records from your documents. The `remarkStructure` plugin extracts the headings and contents of a document as records for [search](/docs/headless/search). Records keep the Markdown you wrote, see [Stringification](/docs/headless/mdx/stringification) for how it's generated. ```mdx tab="Input" ## Installation Run the [command](/docs/cli) below. Restart the **server** after it. | Option | Default | | ------ | ------- | | `dir` | `docs` | ``` ```json tab="Output" { "headings": [{ "id": "installation", "content": "Installation" }], "contents": [ { "heading": "installation", "content": "Run the command below." }, { "heading": "installation", "content": "Restart the **server** after it." }, { "heading": "installation", "content": "| Option | Default |\n| --- | --- |\n| `dir` | `docs` |", "table": "table-0" } ] } ``` ## Usage [#usage] ```ts title="source.config.ts" tab="Fumadocs MDX" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { // enabled by default, exported as `structuredData` remarkStructureOptions: {}, }, }); ``` ```ts tab="MDX Compiler" import { compile } from '@mdx-js/mdx'; import { remarkStructure } from 'fumadocs-core/mdx-plugins'; const file = await compile('...', { // `exportAs` exports it from the document too remarkPlugins: [[remarkStructure, { exportAs: 'structuredData' }]], }); console.log(file.data.structuredData); ``` ## Records [#records] Headings, paragraphs, blockquotes, tables and JSX elements are recorded (see `types`): - Links are replaced by their text, images are removed. - A table has a record per row, as a table of its header and that row. The rows share a `table` field. - JSX elements are replaced by their content, except those kept as HTML tags by `filterElement`, which search dialogs render as components. - A JSX element is a record when it has no children (see `mdxTypes`), or fits on one line. - Content inside a record isn't recorded again. By default, `File`, `TypeTable`, `Callout` and `Card` are kept as HTML tags: ```ts title="source.config.ts" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { remarkStructureOptions: { filterElement: (node) => node.name === 'Callout' || node.name === 'Banner', }, }, }); ``` ## Plugins [#plugins] Nodes inserted by plugins aren't recorded, a plugin adds the records of its node with `data.structuredData`: ```ts node.data = { ...node.data, structuredData: { // `heading` defaults to the current heading contents: [{ heading: undefined, content: 'Generated **content**' }], }, }; ``` To change the Markdown of authored content in records only, edit the `search` namespace with [`replaceSource()`](/docs/headless/mdx/stringification#editing-the-output). ## As a Function [#as-a-function] ```ts import { structure } from 'fumadocs-core/mdx-plugins'; import remarkMath from 'remark-math'; // with the remark plugins your content needs const data = structure(content, [remarkMath], options); ``` ## Options [#options] ### StructureOptions | Prop | Type | Description | | ---------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `types?` | `union` | MDAST node types to be scanned as a content block. If a node's type is listed in this array, it will be converted into a single content block, except tables which become a content block per row, along with the header row. Default: `['heading', 'paragraph', 'blockquote', 'table', 'mdxJsxFlowElement']` | | `mdxTypes?` | `function` | Whether the MDX element should be treated as a single content block, only effective if `types` has `mdxJsxFlowElement`. Default: return `true` if the element is a leaf node, otherwise `false`. | | `filterElement?` | `function` | Return `true` to keep a JSX element as an HTML tag, other elements are replaced by their content. Default: keep `File`, `TypeTable`, `Callout` and `Card` elements. | | `exportAs?` | `union` | export as `structuredData` (if true) or specified variable name. | # Fumadocs Core (the core library of Fumadocs): Page Slugs & Page Tree URL: /docs/headless/page-conventions Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/page-conventions.mdx Customize generated page slugs and page tree. > This guide only applies for content sources that uses `loader()` API, such as **Fumadocs MDX**. ## Overview [#overview] Fumadocs generates **page slugs** and **page tree** (sidebar items) from your content directory using [`loader()`](/docs/headless/source-api), the routing functionality will be handled by your React framework. You can define folders and pages similar to file-system based routing. To customize its output further, you may use [Loader Plugins](/docs/headless/source-api/plugins). ## File [#file] For [MDX](https://mdxjs.com) & Markdown file, you can customize page information from frontmatter. ```mdx --- title: My Page description: Best document ever icon: House --- ## Learn More ``` Fumadocs detects from the following properties to construct page trees. | name | description | | ------------- | ------------------------------------- | | `title` | The title of page | | `description` | The description of page | | `icon` | The name of icon, see [Icons](#icons) | Page information is supplied by the content source such as **Fumadocs MDX**. On Fumadocs MDX, you can specify a [`schema`](/docs/mdx/collections#schema-1) option to customize frontmatter schema. ### Slugs [#slugs] The slugs of a page are generated from its file path. | path (relative to content folder) | slugs | | --------------------------------- | ----------------- | | `./dir/page.mdx` | `['dir', 'page']` | | `./dir/index.mdx` | `['dir']` | ## Folder [#folder] Organize multiple pages, you can create a [Meta file](#meta) to customize folders. ### Folder Group [#folder-group] By default, putting a file into folder will change its slugs. You can wrap the folder name in parentheses to avoid impacting the slugs of child files. | path (relative to content folder) | slugs | | --------------------------------- | ---------- | | `./(group-name)/page.mdx` | `['page']` | ### Root Folder [#root-folder] Marks the folder as a root folder, only items in the opened root folder will be visible. ```json title="meta.json" { "title": "Name of Folder", "description": "The description of root folder (optional)", "root": true } ``` For example, when you are opening root folder `framework`, the other folders (e.g. `headless`) are not shown on the sidebar and other navigation elements. Fumadocs UI renders root folders as [Layout Tabs](/docs/ui/layouts/docs#layout-tabs), which allows users to switch between them. ### Root Type [#root-type] `root` also accepts a string, the **type** of root folder: root folders of the same type under the same parent scope are interchangeable, such as versions of the same docs. ```json tab="v1/meta.json" { "title": "1.0.0", "root": "version" } ``` ```json tab="v2/meta.json" { "title": "2.0.0", "root": "version" } ``` `root: true` is simply the default type, displayed as tabs. Switching to another root folder of the same type maps a page to its **structural projection**, the page at the same path relative to the root folder: | Page | Projection in `v2` | | --------------------- | --------------------------------------------------- | | `v1/guide.mdx` | `v2/guide.mdx` | | `v1/python/guide.mdx` | `v2/python/guide.mdx`, nested root folders are kept | Fumadocs UI renders a [dropdown](/docs/ui/layouts/docs#tabs-groups) for each root folder on the current page's path to switch between root folders of its type, see [Versioning](/docs/navigation#versioning) for a complete example. ## Meta [#meta] Customize folders by creating a `meta.json` file in the folder. ```json title="meta.json" { "title": "Display Name", "icon": "MyIcon", "pages": ["index", "getting-started"], "defaultOpen": true } ``` | name | description | | ------------- | -------------------------------------------- | | `title` | Display name | | `icon` | The name of icon, see [Icons](#icons) | | `defaultOpen` | Open the folder by default | | `collapsible` | Collapsible folder content (default: `true`) | | `pages` | Folder items (see below) | | `pagesIndex` | The index item (see below) | ### Pages [#pages] Folder items are sorted alphabetically by default, you can add or control the order of items using `pages`. ```json title="meta.json" { "pages": ["index", "getting-started"] } ``` > When specified, items are not included unless they are listed in `pages`. | Type | Syntax | Description | | ----------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | **Path** | `./path/to/page` | a path to page or folder. | | **Separator** | `---Label---`
`---[Icon]Label---` | a separator between two sections (icon supported). | | **Link** | `[Text](url)`
`[Icon][Text](url)`
`external:[Text](url)` | insert links (icon supported)
add a `external:` prefix to mark links as external. | | **Rest** | `...` | include remaining pages (sorted alphabetically). | | **Reversed Rest** | `z...a` | reversed **Rest** item. | | **Extract** | `...folder` | extract the items from a folder. | | **Except** | `!item` | Exclude an item from `...` or `z...a`. | ```json title="meta.json" { "pages": [ "components", "---My Separator---", "...folder", "...", "!file", "!otherFolder", "[Vercel](https://vercel.com)", "[Triangle][Vercel](https://vercel.com)" ] } ``` The same page URL must not appear more than once in the entire page tree. Duplicated page items are not allowed for the same reason. This is intentional, such that Fumadocs can locate the active tree item purely based on current pathname. ### Folder Index [#folder-index] When folder owns a index item, it becomes clickable and jumps to the index item when clicked. By default, it is the `index` file under each folder. You can specify the index item of a folder, it accepts **Path** and **Link**. ```json tab="meta.json (Path)" { // assume there's a "overview.mdx" file under the folder "pagesIndex": "overview" } ``` ```json tab="meta.json (Link)" { "pagesIndex": "[Example](https://example.com)" } ``` ## Icons [#icons] Since Fumadocs doesn't include an icon library, you have to convert the icon names to JSX elements in runtime, and render it as a component. You can add an [`icon` handler](/docs/headless/source-api#icons) to `loader()`. ## i18n Routing [#i18n-routing] You can define different style for i18n routing. ```ts title="lib/i18n.ts" import type { I18nConfig } from 'fumadocs-core/i18n'; export const i18n: I18nConfig = { // default parser: 'dot', // or parser: 'dir', }; ``` Add Markdown/meta files for different languages by attending `.{locale}` to your file name, like: For the default locale, the locale code is optional. All content files are grouped by language folders: # Fumadocs Core (the core library of Fumadocs): Page Tree URL: /docs/headless/page-tree Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/page-tree.mdx The structure of page tree. Page tree is a tree structure that describes all navigation links, with other items like separator and folders. It will be sent to the client and being referenced in navigation elements including the sidebar and breadcrumb. Hence, you shouldn't store any sensitive or large data in page tree. By design, page tree only contains necessary information of all pages and folders. Unserializable data such as functions can't be passed to page tree. ## Conventions [#conventions] You can import the type definitions of page tree, useful if you want to hardcode or generate it. ```ts import type * as PageTree from 'fumadocs-core/page-tree'; const tree: PageTree.Root = { // props }; ``` Certain nodes contain a `$ref` property, they are internal and not used when hardcoding it. ### Root [#root] The initial root of page trees. ### PageTreeRoot | Prop | Type | Description | | -------------- | ----------- | ------------------------------------------------------------------------- | | `type?` | `"root"` | | | `name` | `ReactNode` | | | `description?` | `ReactNode` | | | `children` | `array` | | | `fallback?` | `object` | Another page tree that won't be displayed unless being opened. | | `$id?` | `string` | ID for the node, unique in all page trees (even across different locales) | ### Page [#page] A node representing link. ```json { "type": "page", "name": "Quick Start", "url": "/docs" } ``` > External urls are also supported ### PageTreeItem | Prop | Type | Description | | -------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------- | | `type` | `"page"` | | | `name` | `ReactNode` | | | `url` | `string` | | | `external?` | `union` | Whether the link should be treated as external (e.g. use HTML
tag). When unspecified, it depends on the value of `url`. | | `description?` | `ReactNode` | | | `icon?` | `ReactNode` | | | `$id?` | `string` | ID for the node, unique in all page trees (even across different locales) | ### Folder [#folder] A node containing multiple children nodes. ```json { "type": "folder", "name": "Guide", "index": { "type": "page", ... } "children": [ ... ] } ``` ### PageTreeFolder | Prop | Type | Description | | -------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `"folder"` | | | `name` | `ReactNode` | | | `description?` | `ReactNode` | | | `root?` | `union` | Mark as a root folder, `true` for the default type. A string value specifies its **root type**: root folders of the same type under the same parent scope are interchangeable (e.g. versions), and pages can map to their structural projection in another root. | | `defaultOpen?` | `union` | | | `collapsible?` | `union` | | | `index?` | `object` | | | `icon?` | `ReactNode` | | | `children` | `array` | | | `$id?` | `string` | ID for the node, unique in all page trees (even across different locales) | ### Separator [#separator] A label between items. ```json { "type": "separator", "name": "Components" } ``` ### PageTreeSeparator | Prop | Type | Description | | ------- | ------------- | ------------------------------------------------------------------------- | | `type` | `"separator"` | | | `name?` | `ReactNode` | | | `icon?` | `ReactNode` | | | `$id?` | `string` | ID for the node, unique in all page trees (even across different locales) | ## Icons [#icons] Icon is a `ReactElement`, supported by pages and folders. # Fumadocs Core (the core library of Fumadocs): Algolia Search URL: /docs/headless/search/algolia Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/search/algolia.mdx Integrate Algolia Search with Fumadocs If you're using Algolia's free tier, you have to [display their logo on your search dialog](https://algolia.com/policies/free-services-terms). ## Setup [#setup] Install dependencies: ```package-install algoliasearch ``` ### Sign up on Algolia [#sign-up-on-algolia] Sign up and obtain the app id and API keys for your search, store them in `.env.local` (`VITE_` instead of `NEXT_PUBLIC_` outside Next.js): ```dotenv NEXT_PUBLIC_ALGOLIA_APP_ID="app ID" NEXT_PUBLIC_ALGOLIA_SEARCH_KEY="search-only API key" ALGOLIA_WRITE_KEY="write API key" ``` ### Sync Search Indexes [#sync-search-indexes] Pre-render a `static.json` route to export the search indexes on build, and sync them to Algolia with a script after each build: ```ts tab="Next.js" title="app/static.json/route.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/algolia'; export const revalidate = false; export async function GET() { return Response.json(await toDocuments(source)); } ``` ```ts tab="Next.js" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'fumadocs-core/search/algolia'; import { algoliasearch } from 'algoliasearch'; const records = JSON.parse(await fs.readFile('.next/server/app/static.json.body', 'utf-8')) as DocumentRecord[]; const client = algoliasearch( process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!, process.env.ALGOLIA_WRITE_KEY!, ); await sync(client, { indexName: 'document', documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Next.js" title="package.json" { "scripts": { "build": "next build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="React Router" title="app/routes/static.json.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/algolia'; export async function loader() { return Response.json(await toDocuments(source)); } ``` ```ts tab="React Router" title="app/routes.ts" import { index, route, type RouteConfig } from '@react-router/dev/routes'; export default [ index('routes/home.tsx'), route('static.json', 'routes/static.json.ts'), // [!code ++:1] ] satisfies RouteConfig; ``` ```ts tab="React Router" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'fumadocs-core/search/algolia'; import { algoliasearch } from 'algoliasearch'; const records = JSON.parse(await fs.readFile('build/client/static.json', 'utf-8')) as DocumentRecord[]; const client = algoliasearch( process.env.VITE_ALGOLIA_APP_ID!, process.env.ALGOLIA_WRITE_KEY!, ); await sync(client, { indexName: 'document', documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="React Router" title="package.json" { "scripts": { "build": "react-router build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Tanstack Start" title="src/routes/static[.]json.ts" import { createFileRoute } from '@tanstack/react-router'; import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/algolia'; export const Route = createFileRoute('/static.json')({ server: { handlers: { GET: async () => Response.json(await toDocuments(source)), }, }, }); ``` ```ts tab="Tanstack Start" title="vite.config.ts" import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import { nitro } from 'nitro/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ tanstackStart({ prerender: { enabled: true, }, pages: [ // [!code ++:3] { path: '/static.json' } ], }), nitro({ preset: 'vercel', }), ], }); ``` ```ts tab="Tanstack Start" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'fumadocs-core/search/algolia'; import { algoliasearch } from 'algoliasearch'; const records = JSON.parse(await fs.readFile('.vercel/output/static/static.json', 'utf-8')) as DocumentRecord[]; const client = algoliasearch( process.env.VITE_ALGOLIA_APP_ID!, process.env.ALGOLIA_WRITE_KEY!, ); await sync(client, { indexName: 'document', documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Tanstack Start" title="package.json" { "scripts": { "build": "vite build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Waku" title="src/pages/_api/static.json.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/algolia'; export async function GET() { return Response.json(await toDocuments(source)); } export async function getConfig() { return { render: 'static' as const, } as const; } ``` ```ts tab="Waku" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'fumadocs-core/search/algolia'; import { algoliasearch } from 'algoliasearch'; const records = JSON.parse(await fs.readFile('dist/public/static.json', 'utf-8')) as DocumentRecord[]; const client = algoliasearch( process.env.VITE_ALGOLIA_APP_ID!, process.env.ALGOLIA_WRITE_KEY!, ); await sync(client, { indexName: 'document', documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Waku" title="package.json" { "scripts": { "build": "waku build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` `sync()` updates the index settings and replaces all records. The `build` script syncs on every build, set the same variables on your deployment platform. ### Search UI [#search-ui] You can consider different options for implementing the UI: - Using [Fumadocs UI search dialog](/docs/search/algolia). - Build your own using the built-in search client hook: ```ts twoslash import { liteClient } from 'algoliasearch/lite'; import { useAlgoliaSearch } from 'fumadocs-core/search/client'; const algolia = liteClient('id', 'key'); const { search, onSearchChange, isLoading, data } = useAlgoliaSearch({ indexName: 'document', client: algolia, }); ``` - Use their official clients directly. ## Advanced [#advanced] ### Tag Filter [#tag-filter] To configure tag filtering, pass a `tag` to `toDocuments()` in the `static.json` route: ```ts toDocuments(source, { tag: (page) => page.slugs[0] }); ``` And update your search client: - **Fumadocs UI**: Enable [Tag Filter](/docs/search/algolia#tag-filter) on Search UI. - **Search Client**: You can add the tag filter like: ```ts import { useAlgoliaSearch } from 'fumadocs-core/search/client'; const { search, onSearchChange, isLoading, data } = useAlgoliaSearch({ tag: '', // ... }); ``` The `tag` field is an attribute for faceting. You can also use the filter `tag:value` on Algolia search clients. ### Under the Hood [#under-the-hood] The Algolia Integration automatically configures Algolia Search for document search. It creates a record for **each paragraph** in your document, it is also recommended by Algolia. Each record contains searchable attributes: | Attribute | Description | | --------- | --------------------- | | `title` | Page Title | | `section` | Heading ID (nullable) | | `content` | Paragraph content | The `section` field only exists in paragraphs under a heading. Headings and paragraphs are indexed as an individual record, grouped by their page ID. Notice that it expects the `url` property of a page to be unique, you shouldn't have two pages with the same url. # Fumadocs Core (the core library of Fumadocs): FlexSearch URL: /docs/headless/search/flexsearch Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/search/flexsearch.mdx Fast & minimal document search ## Setup [#setup] ```npm npm install flexsearch ``` ```npm npm install @types/flexsearch -D ``` ### From Source [#from-source] Create the server from source object. ```ts title="app/api/search/route.ts (Next.js)" import { source } from '@/lib/source'; import { flexsearchFromSource } from 'fumadocs-core/search/flexsearch'; export const { GET } = flexsearchFromSource(source); ``` ### From Search Indexes [#from-search-indexes] Create the server from search indexes, each index needs a `structuredData` field. Usually, it is provided by your content source (e.g. Fumadocs MDX). You can also extract it from Markdown/MDX document using the [Remark Structure](/docs/headless/mdx/structure) plugin. ```ts title="app/api/search/route.ts (Next.js)" import { source } from '@/lib/source'; import { flexsearch } from 'fumadocs-core/search/flexsearch'; export const { GET } = flexsearch({ indexes: source.getPages().map((page) => ({ title: page.data.title!, description: page.data.description, url: page.url, id: page.url, structuredData: page.data.structuredData, })), }); ``` ### Searching Documents [#searching-documents] You can search documents using: - **Fumadocs UI**: Supported out-of-the-box, see [Search UI](/docs/search/flexsearch) for details. - **Search Client**: ```ts twoslash import { useFetchSearch } from 'fumadocs-core/search/client'; const { search, onSearchChange, isLoading, data } = useFetchSearch(); ``` ### $Fumadocs | Prop | Type | Description | | --------- | -------- | ------------------------------------------------------------------------------ | | `api?` | `string` | API route for search endpoint, support absolute URLs. Default: `'/api/search'` | | `tag?` | `union` | Filter results with specific tag(s). | | `locale?` | `string` | Filter by locale | | `cache?` | `object` | | ## Configurations [#configurations] ### Tag Filter [#tag-filter] Support filtering results by tag, it's useful for implementing multi-docs similar to this documentation. ```ts import { source } from '@/lib/source'; import { flexsearchFromSource } from 'fumadocs-core/search/flexsearch'; const server = flexsearchFromSource(source, { buildIndex(page) { return { title: page.data.title!, description: page.data.description, url: page.url, id: page.url, structuredData: page.data.structuredData, // use your desired value, like page.slugs[0] [!code ++] tag: '', }; }, }); ``` and update your search client: - **Fumadocs UI**: Configure [Tag Filter](/docs/search/flexsearch#tag-filter) on Search UI. - **Search Client**: pass a tag to `useFetchSearch()`. ```ts import { useFetchSearch } from 'fumadocs-core/search/client'; const { search, onSearchChange, isLoading, data } = useFetchSearch({ tag: '', // [!code ++] }); ``` ### Static Mode [#static-export] To support usage with static site, use `staticGET` from search server and make the route static or pre-rendered. ```ts title="app/api/search/route.ts (Next.js)" import { source } from '@/lib/source'; import { flexsearchFromSource } from 'fumadocs-core/search/flexsearch'; // statically cached [!code highlight:2] export const revalidate = false; export const { staticGET: GET } = flexsearchFromSource(source); ``` > `staticGET` is also available on `flexsearch()`. and update your search clients: - **Fumadocs UI**: use [static client](/docs/search/flexsearch#static) on Search UI. - **Search Client**: change your client adapter. ```ts import { useFlexsearchStatic } from 'fumadocs-core/search/client/flexsearch-static'; const { search, onSearchChange, isLoading, data } = useFlexsearchStatic({ // optional: pass tag & locale tag, }); ``` ### $Fumadocs | Prop | Type | Description | | --------- | -------- | ------------------------ | | `from?` | `string` | Default: ``/api/search`` | | `locale?` | `string` | | | `tag?` | `union` | | Static Search requires clients to download the exported search indexes. For large docs sites, it can be expensive. You should use cloud solutions like Orama Cloud or Algolia for these cases. ## Internationalization [#internationalization] Flexsearch doesn't require configurations to work for other languages, but you can improve the results by specifying Flexsearch options for each locale. ```ts title="app/api/search/route.ts" tab="From Source" import { source } from '@/lib/source'; import { flexsearchFromSource } from 'fumadocs-core/search/flexsearch'; import { Charset } from 'flexsearch'; const server = flexsearchFromSource(source, { localeMap: { // [locale]: Flexsearch options [!code ++] jp: { encoder: Charset.CJK }, }, }); ``` ```ts tab="From Search Indexes" import { source } from '@/lib/source'; import { flexsearchI18n } from 'fumadocs-core/search/flexsearch'; import { i18n } from '@/lib/i18n'; import { Charset } from 'flexsearch'; const server = flexsearchI18n({ i18n, // [!code ++] localeMap: { // [locale]: Flexsearch options [!code ++] jp: { encoder: Charset.CJK }, }, indexes: source.getPages().map((page) => ({ title: page.data.title!, description: page.data.description, structuredData: page.data.structuredData, id: page.url, url: page.url, locale: page.locale!, })), }); ``` # Fumadocs Core (the core library of Fumadocs): Search URL: /docs/headless/search Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/search/index.mdx Configure Search in Fumadocs # Fumadocs Core (the core library of Fumadocs): Meilisearch URL: /docs/headless/search/meilisearch Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/search/meilisearch.mdx Integrate Meilisearch with Fumadocs [Meilisearch](https://www.meilisearch.com) is an open-source search engine with typo tolerance and filtering, you can self-host it or use Meilisearch Cloud. ## Setup [#setup] Install dependencies: ```package-install meilisearch ``` ### API Keys [#api-keys] Start a Meilisearch instance by [self-hosting](https://www.meilisearch.com/docs/learn/self_hosted/getting_started_with_self_hosted_meilisearch) or on [Meilisearch Cloud](https://www.meilisearch.com/cloud), and store its credentials in `.env.local` (`VITE_` instead of `NEXT_PUBLIC_` outside Next.js): ```dotenv NEXT_PUBLIC_MEILISEARCH_HOST="URL of your instance" NEXT_PUBLIC_MEILISEARCH_SEARCH_KEY="search API key" MEILISEARCH_ADMIN_KEY="Default Admin API Key" ``` The search key is exposed to the browser, [create one](https://www.meilisearch.com/docs/learn/security/basic_security) that can only search the `docs` index. Keep the admin key private. ### Sync Documents [#sync-documents] Pre-render a `static.json` route to export the search documents on build, and sync them to Meilisearch with a script after each build: ```ts tab="Next.js" title="app/static.json/route.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/meilisearch'; export const revalidate = false; export async function GET() { return Response.json(await toDocuments(source)); } ``` ```ts tab="Next.js" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'fumadocs-core/search/meilisearch'; import { Meilisearch } from 'meilisearch'; const records = JSON.parse(await fs.readFile('.next/server/app/static.json.body', 'utf-8')) as DocumentRecord[]; const client = new Meilisearch({ host: process.env.NEXT_PUBLIC_MEILISEARCH_HOST!, apiKey: process.env.MEILISEARCH_ADMIN_KEY!, }); await sync(client, { indexName: 'docs', documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Next.js" title="package.json" { "scripts": { "build": "next build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="React Router" title="app/routes/static.json.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/meilisearch'; export async function loader() { return Response.json(await toDocuments(source)); } ``` ```ts tab="React Router" title="app/routes.ts" import { index, route, type RouteConfig } from '@react-router/dev/routes'; export default [ index('routes/home.tsx'), route('static.json', 'routes/static.json.ts'), // [!code ++:1] ] satisfies RouteConfig; ``` ```ts tab="React Router" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'fumadocs-core/search/meilisearch'; import { Meilisearch } from 'meilisearch'; const records = JSON.parse(await fs.readFile('build/client/static.json', 'utf-8')) as DocumentRecord[]; const client = new Meilisearch({ host: process.env.VITE_MEILISEARCH_HOST!, apiKey: process.env.MEILISEARCH_ADMIN_KEY!, }); await sync(client, { indexName: 'docs', documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="React Router" title="package.json" { "scripts": { "build": "react-router build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Tanstack Start" title="src/routes/static[.]json.ts" import { createFileRoute } from '@tanstack/react-router'; import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/meilisearch'; export const Route = createFileRoute('/static.json')({ server: { handlers: { GET: async () => Response.json(await toDocuments(source)), }, }, }); ``` ```ts tab="Tanstack Start" title="vite.config.ts" import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import { nitro } from 'nitro/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ tanstackStart({ prerender: { enabled: true, }, pages: [ // [!code ++:3] { path: '/static.json' } ], }), nitro({ preset: 'vercel', }), ], }); ``` ```ts tab="Tanstack Start" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'fumadocs-core/search/meilisearch'; import { Meilisearch } from 'meilisearch'; const records = JSON.parse(await fs.readFile('.vercel/output/static/static.json', 'utf-8')) as DocumentRecord[]; const client = new Meilisearch({ host: process.env.VITE_MEILISEARCH_HOST!, apiKey: process.env.MEILISEARCH_ADMIN_KEY!, }); await sync(client, { indexName: 'docs', documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Tanstack Start" title="package.json" { "scripts": { "build": "vite build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Waku" title="src/pages/_api/static.json.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/meilisearch'; export async function GET() { return Response.json(await toDocuments(source)); } export async function getConfig() { return { render: 'static' as const, } as const; } ``` ```ts tab="Waku" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'fumadocs-core/search/meilisearch'; import { Meilisearch } from 'meilisearch'; const records = JSON.parse(await fs.readFile('dist/public/static.json', 'utf-8')) as DocumentRecord[]; const client = new Meilisearch({ host: process.env.VITE_MEILISEARCH_HOST!, apiKey: process.env.MEILISEARCH_ADMIN_KEY!, }); await sync(client, { indexName: 'docs', documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Waku" title="package.json" { "scripts": { "build": "waku build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` `sync()` configures the index and replaces its documents, the old documents stay searchable until the new ones are indexed. The `build` script syncs on every build, set the same variables on your deployment platform. ### Search UI [#search-ui] You can consider different options for implementing the UI: - Using [Fumadocs UI search dialog](/docs/search/meilisearch). - Build your own using the built-in search client hook: ```ts twoslash import { Meilisearch } from 'meilisearch'; import { useMeilisearch } from 'fumadocs-core/search/client'; const client = new Meilisearch({ host: 'host url', apiKey: 'search api key', }); const { search, onSearchChange, isLoading, data } = useMeilisearch({ client, indexName: 'docs', }); ``` ### $Fumadocs | Prop | Type | Description | | ----------- | -------- | ---------------------------------------- | | `client` | `object` | Meilisearch client with a search API key | | `indexName` | `string` | | | `tag?` | `union` | Filter results with specific tag(s). | | `locale?` | `string` | Filter results by locale. | - Use their official clients directly. ## Options [#options] ### Tag Filter [#tag-filter] To configure tag filtering, pass a `tag` to `toDocuments()` in the `static.json` route: ```ts toDocuments(source, { tag: (page) => page.slugs[0] }); ``` And update your search client: - **Fumadocs UI**: Enable [Tag Filter](/docs/search/meilisearch#tag-filter) on Search UI. - **Search Client**: pass a tag to `useMeilisearch()`. ```ts import { useMeilisearch } from 'fumadocs-core/search/client'; const { search, onSearchChange, isLoading, data } = useMeilisearch({ tag: '', // [!code ++] // ... }); ``` ### Internationalization [#internationalization] Documents include the locale of their pages, pass `locale` to search in a language: ```ts import { useI18n } from 'fumadocs-ui/contexts/i18n'; import { useMeilisearch } from 'fumadocs-core/search/client'; const { locale } = useI18n(); const { search, onSearchChange, isLoading, data } = useMeilisearch({ locale, // [!code ++] // ... }); ``` ### Index Settings [#index-settings] `sync()` only sets `searchableAttributes` and `filterableAttributes`, other settings like synonyms and typo tolerance are kept. You can tune them from the Meilisearch dashboard or the `meilisearch` client. ## Under the Hood [#under-the-hood] Each heading and paragraph is a document, with the page title, URL and breadcrumbs. Only `content` is searchable, the search client groups results by page. | Attribute | Description | | --------- | ------------------------------------------ | | `type` | `page`, `heading` or `text` | | `content` | Page title, heading or paragraph | | `heading` | ID of the heading it belongs to (optional) | | `tag` | Tag(s) to filter results (optional) | | `locale` | Locale of page (optional) | # Fumadocs Core (the core library of Fumadocs): Mixedbread URL: /docs/headless/search/mixedbread Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/search/mixedbread.mdx Integrate Mixedbread Search with Fumadocs [Mixedbread](https://www.mixedbread.com) provides semantic search, users can search with natural language and find content by meaning rather than keywords. ## Setup [#setup] Install dependencies: ```package-install @mixedbread/sdk ``` ### API Key [#api-key] Sign up at [Mixedbread](https://platform.mixedbread.com), create an [API key](https://platform.mixedbread.com/platform?next=api-keys) and a [store](https://platform.mixedbread.com/platform?next=stores) for your docs, and store them in `.env.local`: ```dotenv MXBAI_API_KEY="your API key" MIXEDBREAD_STORE_ID="your store ID" ``` Searching runs on your server, the API key is never exposed to the browser. ### Sync Documents [#sync-documents] Pre-render a `static.json` route to export the search documents on build, and sync them to your store with a script after each build: ```ts tab="Next.js" title="app/static.json/route.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/mixedbread'; export const revalidate = false; export async function GET() { return Response.json(await toDocuments(source)); } ``` ```ts tab="Next.js" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'fumadocs-core/search/mixedbread'; import Mixedbread from '@mixedbread/sdk'; const records = JSON.parse(await fs.readFile('.next/server/app/static.json.body', 'utf-8')) as DocumentRecord[]; await sync(new Mixedbread(), { storeIdentifier: process.env.MIXEDBREAD_STORE_ID!, documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Next.js" title="package.json" { "scripts": { "build": "next build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="React Router" title="app/routes/static.json.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/mixedbread'; export async function loader() { return Response.json(await toDocuments(source)); } ``` ```ts tab="React Router" title="app/routes.ts" import { index, route, type RouteConfig } from '@react-router/dev/routes'; export default [ index('routes/home.tsx'), route('static.json', 'routes/static.json.ts'), // [!code ++:1] ] satisfies RouteConfig; ``` ```ts tab="React Router" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'fumadocs-core/search/mixedbread'; import Mixedbread from '@mixedbread/sdk'; const records = JSON.parse(await fs.readFile('build/client/static.json', 'utf-8')) as DocumentRecord[]; await sync(new Mixedbread(), { storeIdentifier: process.env.MIXEDBREAD_STORE_ID!, documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="React Router" title="package.json" { "scripts": { "build": "react-router build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Tanstack Start" title="src/routes/static[.]json.ts" import { createFileRoute } from '@tanstack/react-router'; import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/mixedbread'; export const Route = createFileRoute('/static.json')({ server: { handlers: { GET: async () => Response.json(await toDocuments(source)), }, }, }); ``` ```ts tab="Tanstack Start" title="vite.config.ts" import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import { nitro } from 'nitro/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ tanstackStart({ prerender: { enabled: true, }, pages: [ // [!code ++:3] { path: '/static.json' } ], }), nitro({ preset: 'vercel', }), ], }); ``` ```ts tab="Tanstack Start" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'fumadocs-core/search/mixedbread'; import Mixedbread from '@mixedbread/sdk'; const records = JSON.parse(await fs.readFile('.vercel/output/static/static.json', 'utf-8')) as DocumentRecord[]; await sync(new Mixedbread(), { storeIdentifier: process.env.MIXEDBREAD_STORE_ID!, documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Tanstack Start" title="package.json" { "scripts": { "build": "vite build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Waku" title="src/pages/_api/static.json.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/mixedbread'; export async function GET() { return Response.json(await toDocuments(source)); } export async function getConfig() { return { render: 'static' as const, } as const; } ``` ```ts tab="Waku" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'fumadocs-core/search/mixedbread'; import Mixedbread from '@mixedbread/sdk'; const records = JSON.parse(await fs.readFile('dist/public/static.json', 'utf-8')) as DocumentRecord[]; await sync(new Mixedbread(), { storeIdentifier: process.env.MIXEDBREAD_STORE_ID!, documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Waku" title="package.json" { "scripts": { "build": "waku build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` `sync()` uploads a file for each page, and skips the pages unchanged since the last sync. Files of removed pages are deleted, use a store dedicated to your docs. The `build` script syncs on every build, set the same variables on your deployment platform. ### Search API [#search-api] Create a route to search your store: ```ts title="app/api/search/route.ts" import { createMixedbreadSearchAPI } from 'fumadocs-core/search/mixedbread'; import Mixedbread from '@mixedbread/sdk'; export const { GET } = createMixedbreadSearchAPI({ client: new Mixedbread(), // reads `MXBAI_API_KEY` storeIdentifier: process.env.MIXEDBREAD_STORE_ID!, }); ``` ### Search UI [#search-ui] You can consider different options for implementing the UI: - Using [Fumadocs UI search dialog](/docs/search/mixedbread). - Build your own using the built-in search client hook: ```ts twoslash import { useFetchSearch } from 'fumadocs-core/search/client'; const { search, onSearchChange, isLoading, data } = useFetchSearch({ api: '/api/search', // every search is a request to Mixedbread delayMs: 300, }); ``` ## Options [#options] ### Tag Filter [#tag-filter] To configure tag filtering, pass a `tag` to `toDocuments()` in the `static.json` route: ```ts toDocuments(source, { tag: (page) => page.slugs[0] }); ``` And update your search client: - **Fumadocs UI**: Enable [Tag Filter](/docs/search/mixedbread#tag-filter) on Search UI. - **Search Client**: pass a tag to `useFetchSearch()`. ```ts import { useFetchSearch } from 'fumadocs-core/search/client'; const { search, onSearchChange, isLoading, data } = useFetchSearch({ tag: '', // [!code ++] // ... }); ``` ### Internationalization [#internationalization] Documents include the locale of their pages, pass `locale` to search in a language: ```ts import { useI18n } from 'fumadocs-ui/contexts/i18n'; import { useFetchSearch } from 'fumadocs-core/search/client'; const { locale } = useI18n(); const { search, onSearchChange, isLoading, data } = useFetchSearch({ locale, // [!code ++] // ... }); ``` ## Under the Hood [#under-the-hood] Each page is a file keyed by its URL, with the page title, URL, breadcrumbs, tags and locale as metadata. Its chunks are the title, headings and paragraphs of the page, every result is a chunk: | Chunk Metadata | Description | | -------------- | ------------------------------------------ | | `kind` | `page`, `heading` or `text` | | `heading` | ID of the heading it belongs to (optional) | | `table` | Table of a table row (optional) | # Fumadocs Core (the core library of Fumadocs): Orama Cloud URL: /docs/headless/search/orama-cloud Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/search/orama-cloud.mdx Integrate with Orama Cloud To begin, create an account on Orama Cloud. ## REST API [#rest-api] REST API integration requires your docs to upload the indexes. 1. Create a new project with **REST API** data source on Orama Cloud dashboard. Store your credentials in `.env.local`, for example (`VITE_` instead of `NEXT_PUBLIC_` outside Next.js): ```dotenv NEXT_PUBLIC_ORAMA_DATASOURCE_ID="Rest API data source ID" NEXT_PUBLIC_ORAMA_PROJECT_ID="project ID" NEXT_PUBLIC_ORAMA_API_KEY="public API key" ORAMA_PRIVATE_API_KEY="private API key" ``` 2. Pre-render a `static.json` route to export the search indexes on build, and sync them to Orama Cloud with a script after each build: ```ts tab="Next.js" title="app/static.json/route.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/orama-cloud'; export const revalidate = false; export async function GET() { return Response.json(await toDocuments(source)); } ``` ```ts tab="Next.js" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type OramaDocument as DocumentRecord, sync } from 'fumadocs-core/search/orama-cloud'; import { OramaCloud } from '@orama/core'; const records = JSON.parse(await fs.readFile('.next/server/app/static.json.body', 'utf-8')) as DocumentRecord[]; const orama = new OramaCloud({ projectId: process.env.NEXT_PUBLIC_ORAMA_PROJECT_ID!, apiKey: process.env.ORAMA_PRIVATE_API_KEY!, }); await sync(orama, { index: process.env.NEXT_PUBLIC_ORAMA_DATASOURCE_ID!, documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Next.js" title="package.json" { "scripts": { "build": "next build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="React Router" title="app/routes/static.json.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/orama-cloud'; export async function loader() { return Response.json(await toDocuments(source)); } ``` ```ts tab="React Router" title="app/routes.ts" import { index, route, type RouteConfig } from '@react-router/dev/routes'; export default [ index('routes/home.tsx'), route('static.json', 'routes/static.json.ts'), // [!code ++:1] ] satisfies RouteConfig; ``` ```ts tab="React Router" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type OramaDocument as DocumentRecord, sync } from 'fumadocs-core/search/orama-cloud'; import { OramaCloud } from '@orama/core'; const records = JSON.parse(await fs.readFile('build/client/static.json', 'utf-8')) as DocumentRecord[]; const orama = new OramaCloud({ projectId: process.env.VITE_ORAMA_PROJECT_ID!, apiKey: process.env.ORAMA_PRIVATE_API_KEY!, }); await sync(orama, { index: process.env.VITE_ORAMA_DATASOURCE_ID!, documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="React Router" title="package.json" { "scripts": { "build": "react-router build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Tanstack Start" title="src/routes/static[.]json.ts" import { createFileRoute } from '@tanstack/react-router'; import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/orama-cloud'; export const Route = createFileRoute('/static.json')({ server: { handlers: { GET: async () => Response.json(await toDocuments(source)), }, }, }); ``` ```ts tab="Tanstack Start" title="vite.config.ts" import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import { nitro } from 'nitro/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ tanstackStart({ prerender: { enabled: true, }, pages: [ // [!code ++:3] { path: '/static.json' } ], }), nitro({ preset: 'vercel', }), ], }); ``` ```ts tab="Tanstack Start" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type OramaDocument as DocumentRecord, sync } from 'fumadocs-core/search/orama-cloud'; import { OramaCloud } from '@orama/core'; const records = JSON.parse(await fs.readFile('.vercel/output/static/static.json', 'utf-8')) as DocumentRecord[]; const orama = new OramaCloud({ projectId: process.env.VITE_ORAMA_PROJECT_ID!, apiKey: process.env.ORAMA_PRIVATE_API_KEY!, }); await sync(orama, { index: process.env.VITE_ORAMA_DATASOURCE_ID!, documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Tanstack Start" title="package.json" { "scripts": { "build": "vite build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Waku" title="src/pages/_api/static.json.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/orama-cloud'; export async function GET() { return Response.json(await toDocuments(source)); } export async function getConfig() { return { render: 'static' as const, } as const; } ``` ```ts tab="Waku" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type OramaDocument as DocumentRecord, sync } from 'fumadocs-core/search/orama-cloud'; import { OramaCloud } from '@orama/core'; const records = JSON.parse(await fs.readFile('dist/public/static.json', 'utf-8')) as DocumentRecord[]; const orama = new OramaCloud({ projectId: process.env.VITE_ORAMA_PROJECT_ID!, apiKey: process.env.ORAMA_PRIVATE_API_KEY!, }); await sync(orama, { index: process.env.VITE_ORAMA_DATASOURCE_ID!, documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Waku" title="package.json" { "scripts": { "build": "waku build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` The `build` script syncs on every build, set the same variables on your deployment platform. ### Search Client [#search-client] To search documents on the client side, consider: - Using [Fumadocs UI search dialog](/docs/search/orama-cloud). - Custom search UI using the built-in hook of Fumadocs: ```ts import { useOramaCloudSearch } from 'fumadocs-core/search/client'; import { OramaCloud } from '@orama/core'; const orama = new OramaCloud({ projectId: process.env.NEXT_PUBLIC_ORAMA_PROJECT_ID, apiKey: process.env.NEXT_PUBLIC_ORAMA_API_KEY, }); const params = { // optional search params }; const { search, onSearchChange, isLoading, data } = useOramaCloudSearch({ client: orama, params, }); ``` - Use their search client directly. ## Web Crawler [#web-crawler] 1. Create a Crawler data source from dashboard, and configure it correctly with the "Documentation" preset. 2. Copy the credentials from dashboard. ### Search Client [#search-client-1] Same as REST API integration, but make sure to set `index` to `crawler`. ```ts import { useOramaCloudSearch } from 'fumadocs-core/search/client'; import { OramaCloud } from '@orama/core'; const orama = new OramaCloud({ projectId: '', apiKey: '', }); const params = { // optional search params }; const { search, onSearchChange, isLoading, data } = useOramaCloudSearch({ index: 'crawler', client: orama, params, }); ``` It's same for Fumadocs UI. # Fumadocs Core (the core library of Fumadocs): Built-in Search URL: /docs/headless/search/orama Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/search/orama.mdx Built-in document search of Fumadocs Fumadocs supports document search with [ZBSearch](https://www.zbsearch.dev), It is the default but also the recommended option since it can be self-hosted and totally free. ## Setup [#setup] Host the server for handling search requests. ### From Source [#from-source] Create the server from source object. ```ts tab="Next.js" title="app/api/search/route.ts" import { source } from '@/lib/source'; import { createFromSource } from 'fumadocs-core/search/server'; export const { GET } = createFromSource(source); ``` ```ts tab="React Router" title="app/routes/search.ts" import type { Route } from './+types/search'; import { createFromSource } from 'fumadocs-core/search/server'; import { source } from '@/lib/source'; const server = createFromSource(source); export async function loader({ request }: Route.LoaderArgs) { return server.GET(request); } ``` ```ts tab="Tanstack Start" title="src/routes/api/search.ts" import { createFileRoute } from '@tanstack/react-router'; import { source } from '@/lib/source'; import { createFromSource } from 'fumadocs-core/search/server'; const server = createFromSource(source); export const Route = createFileRoute('/api/search')({ server: { handlers: { GET: async ({ request }) => server.GET(request), }, }, }); ``` ```ts tab="Waku" title="src/pages/_api/api/search.ts" import { createFromSource } from 'fumadocs-core/search/server'; import { source } from '@/lib/source'; export const { GET } = createFromSource(source); ``` ```ts tab="Astro" title="src/pages/api/search.ts" import type { APIRoute } from 'astro'; import { createFromSource } from 'fumadocs-core/search/server'; import { getStructuredData, source } from '@/lib/source'; const server = createFromSource(source, { buildIndex(page) { return { id: page.data._raw.id, title: page.data.title, description: page.data.description, structuredData: getStructuredData(page.data._raw), url: page.url, }; }, }); export const GET: APIRoute = () => { return server.staticGET(); }; ``` ### From Search Indexes [#from-search-indexes] Create the server from search indexes, each index needs a `structuredData` field. Usually, it is provided by your content source (e.g. Fumadocs MDX). You can also extract it from Markdown/MDX document using the [Remark Structure](/docs/headless/mdx/structure) plugin. ```ts tab="Next.js" title="app/api/search/route.ts" import { source } from '@/lib/source'; import { createSearchAPI } from 'fumadocs-core/search/server'; export const { GET } = createSearchAPI('advanced', { indexes: source.getPages().map((page) => ({ title: page.data.title, description: page.data.description, url: page.url, id: page.url, structuredData: page.data.structuredData, })), }); ``` ```ts tab="React Router" title="app/routes/search.ts" import type { Route } from './+types/search'; import { createSearchAPI } from 'fumadocs-core/search/server'; import { source } from '@/lib/source'; const server = createSearchAPI('advanced', { indexes: source.getPages().map((page) => ({ title: page.data.title, description: page.data.description, url: page.url, id: page.url, structuredData: page.data.structuredData, })), }); export async function loader({ request }: Route.LoaderArgs) { return server.GET(request); } ``` ```ts tab="Tanstack Start" title="src/routes/api/search.ts" import { createFileRoute } from '@tanstack/react-router'; import { source } from '@/lib/source'; import { createSearchAPI } from 'fumadocs-core/search/server'; const server = createSearchAPI('advanced', { indexes: source.getPages().map((page) => ({ title: page.data.title, description: page.data.description, url: page.url, id: page.url, structuredData: page.data.structuredData, })), }); export const Route = createFileRoute('/api/search')({ server: { handlers: { GET: async ({ request }) => server.GET(request), }, }, }); ``` ```ts tab="Waku" title="src/pages/_api/api/search.ts" import { source } from '@/lib/source'; import { createSearchAPI } from 'fumadocs-core/search/server'; export const { GET } = createSearchAPI('advanced', { indexes: source.getPages().map((page) => ({ title: page.data.title, description: page.data.description, url: page.url, id: page.url, structuredData: page.data.structuredData, })), }); ``` ```ts tab="Astro" title="src/pages/api/search.ts" import type { APIRoute } from 'astro'; import { createSearchAPI } from 'fumadocs-core/search/server'; import { getStructuredData, source } from '@/lib/source'; const server = createSearchAPI('advanced', { indexes: source.getPages().map((page) => ({ title: page.data.title, description: page.data.description, url: page.url, id: page.url, structuredData: getStructuredData(page.data._raw), })), }); export const GET: APIRoute = ({ request }) => server.GET(request); ``` ### Searching Documents [#searching-documents] You can search documents using: - **Fumadocs UI**: Supported out-of-the-box, see [Search UI](/docs/search/orama) for details. - **Search Client**: ```ts twoslash import { useFetchSearch } from 'fumadocs-core/search/client'; const { search, onSearchChange, isLoading, data } = useFetchSearch(); ``` ### $Fumadocs | Prop | Type | Description | | --------- | -------- | ------------------------------------------------------------------------------ | | `api?` | `string` | API route for search endpoint, support absolute URLs. Default: `'/api/search'` | | `tag?` | `union` | Filter results with specific tag(s). | | `locale?` | `string` | Filter by locale | | `cache?` | `object` | | ## Configurations [#configurations] ### Tag Filter [#tag-filter] Support filtering results by tag, it's useful for implementing multi-docs similar to this documentation. ```ts import { source } from '@/lib/source'; import { createFromSource } from 'fumadocs-core/search/server'; const server = createFromSource(source, { buildIndex(page) { return { title: page.data.title, description: page.data.description, url: page.url, id: page.url, structuredData: page.data.structuredData, // use your desired value, like page.slugs[0] [!code ++] tag: '', }; }, }); ``` and update your search client: - **Fumadocs UI**: Configure [Tag Filter](/docs/search/orama#tag-filter) on Search UI. - **Search Client**: pass a tag to `useFetchSearch()`. ```ts import { useFetchSearch } from 'fumadocs-core/search/client'; const { search, onSearchChange, isLoading, data } = useFetchSearch({ tag: '', // [!code ++] }); ``` ### Static Mode [#static-export] To support usage with static site, use `staticGET` from search server and make the route static or pre-rendered. ```ts tab="Next.js" title="app/api/search/route.ts" import { source } from '@/lib/source'; import { createFromSource } from 'fumadocs-core/search/server'; // statically cached [!code highlight:2] export const revalidate = false; export const { staticGET: GET } = createFromSource(source); ``` ```ts tab="React Router" title="app/routes/search.ts" // make sure this route is pre-rendered in `react-router.config.ts`. export async function loader() { // [!code highlight] return server.staticGET(); } ``` ```ts tab="Tanstack Start" title="src/routes/api/search.ts" import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/api/search')({ server: { handlers: { // [!code highlight] GET: async () => server.staticGET(), }, }, }); ``` ```ts tab="Tanstack Start" title="vite.config.ts" import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ tanstackStart({ prerender: { enabled: true, }, // [!code ++] pre-render the index file pages: [{ path: '/api/search' }], }), ], }); ``` ```ts tab="Waku" title="src/pages/_api/api/search.ts" import { source } from '@/lib/source'; import { createFromSource } from 'fumadocs-core/search/server'; // [!code highlight] export const { staticGET: GET } = createFromSource(source); // statically cached [!code highlight:3] export const getConfig = async () => ({ render: 'static', }); ``` ```ts tab="Astro" title="src/pages/api/search.ts" import type { APIRoute } from 'astro'; // [!code highlight] export const GET: APIRoute = () => server.staticGET(); ``` > `staticGET` is also available on `createSearchAPI`. and update your search clients: - **Fumadocs UI**: use [static client](/docs/search/orama#static) on Search UI. - **Search Client**: use `useStaticSearch()` instead of `useFetchSearch()`. ```ts import { useStaticSearch } from 'fumadocs-core/search/client'; const { search, onSearchChange, isLoading, data } = useStaticSearch(); ``` ### $Fumadocs | Prop | Type | Description | | ------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------- | | `from?` | `string` | Where to download exported search indexes (URL) Default: `'/api/search'` | | `initDB?` | `function` | Customize how the search database is initialized (advanced). For legacy per-locale exports, it is called once per locale. | | `initOrama?` | `function` | **Deprecated.** | | `tag?` | `union` | Filter results with specific tag(s). | | `locale?` | `string` | Filter by locale (for i18n) | | `search?` | `union` | extra options for search | Static Search requires clients to download the exported search indexes. For large docs sites, it can be expensive. You should use cloud solutions like Orama Cloud or Algolia for these cases. ## Internationalization [#internationalization] Search works with every language out of the box - the default `multilingual` mode uses Unicode word segmentation, so all locales (including Chinese and Japanese) share a single search database with zero config. ```ts title="app/api/search/route.ts" tab="From Source" import { source } from '@/lib/source'; import { createFromSource } from 'fumadocs-core/search/server'; // no extra configuration needed for i18n const server = createFromSource(source); ``` ```ts tab="From Search Indexes" import { source } from '@/lib/source'; import { createI18nSearchAPI } from 'fumadocs-core/search/server'; import { i18n } from '@/lib/i18n'; const server = createI18nSearchAPI('advanced', { i18n, // [!code ++] indexes: source.getLanguages().flatMap(({ language, pages }) => pages.map((page) => ({ title: page.data.title, description: page.data.description, structuredData: page.data.structuredData, id: page.url, url: page.url, locale: language, // [!code ++] })), ), }); ``` and update your search clients: - **Fumadocs UI**: No changes needed, Fumadocs UI handles this when you have i18n configured correctly. - **Search Client**: Add `locale` to the search client, this will only allow pages with specified locale to be searchable by the user. ```ts import { useFetchSearch } from 'fumadocs-core/search/client'; const { search, onSearchChange, isLoading, data } = useFetchSearch({ locale: 'cn', }); ``` This also applies to **Static Mode**, pass `locale` to `useStaticSearch()` instead. The `multilingual` mode doesn't apply language-specific stemming or stop-words. Without stemming, a plural query like `recordings` won't match a page containing `recording`. To attach a stemmer while keeping multilingual segmentation and a single database, pass a `tokenizer`: ```ts import { source } from '@/lib/source'; import { createFromSource } from 'fumadocs-core/search/server'; import { stemmer } from '@zbsearch/stemmers/english'; const server = createFromSource(source, { tokenizer: { language: 'multilingual', stemming: true, stemmer }, // [!code ++] }); ``` Alternatively, use a language on the [supported languages](https://www.zbsearch.dev/docs/zbsearch/supported-languages) list per locale with `localeMap`: ```ts import { source } from '@/lib/source'; import { createFromSource } from 'fumadocs-core/search/server'; const server = createFromSource(source, { localeMap: { // [locale]: search options [!code ++:2] ru: { language: 'russian' }, en: { language: 'english' }, }, }); ``` Note that a separate search database is created for each locale when `localeMap` is enabled. A custom `tokenizer` must also be given to the client, since it needs to tokenize the query the same way. Pass it via `initDB` on `useStaticSearch()`. ## Headless [#headless] You can host the search server on other backend such as Express and Elysia. ```ts import { initAdvancedSearch } from 'fumadocs-core/search/server'; const server = initAdvancedSearch({ // you still have to pass indexes }); server.search('query', { // you can specify `locale` and `tag` here }); ``` # Fumadocs Core (the core library of Fumadocs): Trieve Search URL: /docs/headless/search/trieve Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/search/trieve.mdx Integrate Trieve Search with Fumadocs > This is a community maintained integration. ## Introduction [#introduction] The Trieve Integration automatically configures Trieve Search for site search. By default, it creates a chunk for **each paragraph** in your document, it is officially recommended by Trieve. ## Setup [#setup] ### Install Dependencies [#install-dependencies] ```package-install trieve-ts-sdk trieve-fumadocs-adapter ``` ### Sign up on Trieve [#sign-up-on-trieve] Sign up and create a dataset. Then obtain 2 API keys where one has only read access and the other has admin access to create and delete chunks. Store the dataset ID and admin key in `.env.local` for syncing: ```dotenv TRIEVE_DATASET_ID="dataset ID" TRIEVE_ADMIN_API_KEY="admin API key" ``` One API Key should have only read access for the public facing search and the other should have admin access to create and delete chunks. ### Sync Dataset [#sync-dataset] Trieve takes the records of the [Algolia integration](/docs/headless/search/algolia). Export them with a pre-rendered `static.json` route, and sync them to Trieve with a script after each build: ```ts tab="Next.js" title="app/static.json/route.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/algolia'; export const revalidate = false; export async function GET() { return Response.json(await toDocuments(source)); } ``` ```ts tab="Next.js" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { sync, type TrieveDocument as DocumentRecord } from 'trieve-fumadocs-adapter/search/sync'; import { TrieveSDK } from 'trieve-ts-sdk'; const records = JSON.parse(await fs.readFile('.next/server/app/static.json.body', 'utf-8')) as DocumentRecord[]; const client = new TrieveSDK({ apiKey: process.env.TRIEVE_ADMIN_API_KEY!, datasetId: process.env.TRIEVE_DATASET_ID!, }); await sync(client, records); console.log(`search updated: ${records.length} records`); ``` ```json tab="Next.js" title="package.json" { "scripts": { "build": "next build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="React Router" title="app/routes/static.json.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/algolia'; export async function loader() { return Response.json(await toDocuments(source)); } ``` ```ts tab="React Router" title="app/routes.ts" import { index, route, type RouteConfig } from '@react-router/dev/routes'; export default [ index('routes/home.tsx'), route('static.json', 'routes/static.json.ts'), // [!code ++:1] ] satisfies RouteConfig; ``` ```ts tab="React Router" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { sync, type TrieveDocument as DocumentRecord } from 'trieve-fumadocs-adapter/search/sync'; import { TrieveSDK } from 'trieve-ts-sdk'; const records = JSON.parse(await fs.readFile('build/client/static.json', 'utf-8')) as DocumentRecord[]; const client = new TrieveSDK({ apiKey: process.env.TRIEVE_ADMIN_API_KEY!, datasetId: process.env.TRIEVE_DATASET_ID!, }); await sync(client, records); console.log(`search updated: ${records.length} records`); ``` ```json tab="React Router" title="package.json" { "scripts": { "build": "react-router build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Tanstack Start" title="src/routes/static[.]json.ts" import { createFileRoute } from '@tanstack/react-router'; import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/algolia'; export const Route = createFileRoute('/static.json')({ server: { handlers: { GET: async () => Response.json(await toDocuments(source)), }, }, }); ``` ```ts tab="Tanstack Start" title="vite.config.ts" import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import { nitro } from 'nitro/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ tanstackStart({ prerender: { enabled: true, }, pages: [ // [!code ++:3] { path: '/static.json' } ], }), nitro({ preset: 'vercel', }), ], }); ``` ```ts tab="Tanstack Start" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { sync, type TrieveDocument as DocumentRecord } from 'trieve-fumadocs-adapter/search/sync'; import { TrieveSDK } from 'trieve-ts-sdk'; const records = JSON.parse(await fs.readFile('.vercel/output/static/static.json', 'utf-8')) as DocumentRecord[]; const client = new TrieveSDK({ apiKey: process.env.TRIEVE_ADMIN_API_KEY!, datasetId: process.env.TRIEVE_DATASET_ID!, }); await sync(client, records); console.log(`search updated: ${records.length} records`); ``` ```json tab="Tanstack Start" title="package.json" { "scripts": { "build": "vite build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Waku" title="src/pages/_api/static.json.ts" import { source } from '@/lib/source'; import { toDocuments } from 'fumadocs-core/search/algolia'; export async function GET() { return Response.json(await toDocuments(source)); } export async function getConfig() { return { render: 'static' as const, } as const; } ``` ```ts tab="Waku" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { sync, type TrieveDocument as DocumentRecord } from 'trieve-fumadocs-adapter/search/sync'; import { TrieveSDK } from 'trieve-ts-sdk'; const records = JSON.parse(await fs.readFile('dist/public/static.json', 'utf-8')) as DocumentRecord[]; const client = new TrieveSDK({ apiKey: process.env.TRIEVE_ADMIN_API_KEY!, datasetId: process.env.TRIEVE_DATASET_ID!, }); await sync(client, records); console.log(`search updated: ${records.length} records`); ``` ```json tab="Waku" title="package.json" { "scripts": { "build": "waku build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` The `build` script syncs on every build, set the same variables on your deployment platform. ### Search UI [#search-ui] You can use their `SearchDialog` component: ```tsx title="components/search.tsx" 'use client'; import type { SharedProps } from 'fumadocs-ui/components/dialog/search'; import SearchDialog from 'trieve-fumadocs-adapter/components/dialog/search'; import { TrieveSDK } from 'trieve-ts-sdk'; const trieveClient = new TrieveSDK({ apiKey: 'readOnlyApiKey', datasetId: 'datasetId', }); export default function CustomSearchDialog(props: SharedProps) { return ; } ``` 1. Replace `apiKey` and `datasetId` with your desired values. 2. Replace the default search dialog with your new one. ### Search Client [#search-client] Add the `useTrieveSearch` hook: ```ts import { TrieveSDK } from 'trieve-ts-sdk'; import { useTrieveSearch } from 'trieve-fumadocs-adapter/search/trieve'; const client = new TrieveSDK({ apiKey: 'readOnlyApiKey', datasetId: 'datasetId', }); const { search, setSearch, query } = useTrieveSearch(client); ``` ## Options [#options] ### Tag Filter [#tag-filter] To configure tag filtering, add a `tag` value to indexes. ```js import { sync } from 'trieve-fumadocs-adapter/search/sync'; import { TrieveSDK } from 'trieve-ts-sdk'; const client = new TrieveSDK({ apiKey: 'adminApiKey', datasetId: 'datasetId', }); const documents = records.map((index) => ({ ...index, tag: 'value', // [!code highlight] })); sync(client, documents); ``` #### Search UI [#search-ui-1] Enable Tag Filter. ```tsx title="components/search.tsx" import SearchDialog from 'trieve-fumadocs-adapter/components/dialog/search'; ; ``` #### Search Client [#search-client-1] The `tag_set` field is an attribute for filtering. To filter indexes by tag, use the filter on Trieve search clients. ```json { "must": [ { "field": "tag_set", "match": ["value"] } ] } ``` Or with `useTrieveSearch` hook: ```ts import { TrieveSDK } from 'trieve-ts-sdk'; import { useTrieveSearch } from 'trieve-fumadocs-adapter/search/trieve'; const client = new TrieveSDK({ apiKey: 'readOnlyApiKey', datasetId: 'datasetId', }); const { search, setSearch, query } = useTrieveSearch(client, undefined, ''); ``` # Fumadocs Core (the core library of Fumadocs): Typesense Search URL: /docs/headless/search/typesense Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/search/typesense.mdx Integrate Typesense Search with Fumadocs > This is a community-maintained integration. ## Setup [#setup] ### Install Dependencies [#install-dependencies] ```package-install typesense typesense-fumadocs-adapter ``` ### Start Typesense Server [#start-typesense-server] You can either self-host the Typesense server or use their cloud service. Follow their [getting started guide](https://typesense.org/docs/guide/install-typesense.html) to set up your server, and store its credentials in `.env.local` (`VITE_` instead of `NEXT_PUBLIC_` outside Next.js): ```dotenv NEXT_PUBLIC_TYPESENSE_URL="server URL" NEXT_PUBLIC_TYPESENSE_SEARCH_KEY="search-only API key" TYPESENSE_API_KEY="API key with write access" ``` ### Sync Dataset [#sync-dataset] Export the search indexes with a pre-rendered `static.json` route, and sync them to Typesense with a script after each build: ```ts tab="Next.js" title="app/static.json/route.ts" import { source } from '@/lib/source'; import type { DocumentRecord } from 'typesense-fumadocs-adapter'; async function getDocuments() { const documents: DocumentRecord[] = []; for (const page of source.getPages()) { documents.push({ _id: page.url, structured: page.data.structuredData, url: page.url, title: page.data.title, description: page.data.description, locale: page.locale, }); } return documents; } export const revalidate = false; export async function GET() { return Response.json(await getDocuments()); } ``` ```ts tab="Next.js" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'typesense-fumadocs-adapter'; import { Client } from 'typesense'; const records = JSON.parse(await fs.readFile('.next/server/app/static.json.body', 'utf-8')) as DocumentRecord[]; const client = new Client({ nodes: [{ url: process.env.NEXT_PUBLIC_TYPESENSE_URL! }], apiKey: process.env.TYPESENSE_API_KEY!, connectionTimeoutSeconds: 60 * 15, }); await sync(client, { typesenseCollectionName: 'docs', documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Next.js" title="package.json" { "scripts": { "build": "next build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="React Router" title="app/routes/static.json.ts" import { source } from '@/lib/source'; import type { DocumentRecord } from 'typesense-fumadocs-adapter'; async function getDocuments() { const documents: DocumentRecord[] = []; for (const page of source.getPages()) { documents.push({ _id: page.url, structured: await page.data.structuredData(), url: page.url, title: page.data.title, description: page.data.description, locale: page.locale, }); } return documents; } export async function loader() { return Response.json(await getDocuments()); } ``` ```ts tab="React Router" title="app/routes.ts" import { index, route, type RouteConfig } from '@react-router/dev/routes'; export default [ index('routes/home.tsx'), route('static.json', 'routes/static.json.ts'), // [!code ++:1] ] satisfies RouteConfig; ``` ```ts tab="React Router" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'typesense-fumadocs-adapter'; import { Client } from 'typesense'; const records = JSON.parse(await fs.readFile('build/client/static.json', 'utf-8')) as DocumentRecord[]; const client = new Client({ nodes: [{ url: process.env.VITE_TYPESENSE_URL! }], apiKey: process.env.TYPESENSE_API_KEY!, connectionTimeoutSeconds: 60 * 15, }); await sync(client, { typesenseCollectionName: 'docs', documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="React Router" title="package.json" { "scripts": { "build": "react-router build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Tanstack Start" title="src/routes/static[.]json.ts" import { createFileRoute } from '@tanstack/react-router'; import { source } from '@/lib/source'; import type { DocumentRecord } from 'typesense-fumadocs-adapter'; async function getDocuments() { const documents: DocumentRecord[] = []; for (const page of source.getPages()) { documents.push({ _id: page.url, structured: await page.data.structuredData(), url: page.url, title: page.data.title, description: page.data.description, locale: page.locale, }); } return documents; } export const Route = createFileRoute('/static.json')({ server: { handlers: { GET: async () => Response.json(await getDocuments()), }, }, }); ``` ```ts tab="Tanstack Start" title="vite.config.ts" import { tanstackStart } from '@tanstack/react-start/plugin/vite'; import { nitro } from 'nitro/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ tanstackStart({ prerender: { enabled: true, }, pages: [ // [!code ++:3] { path: '/static.json' } ], }), nitro({ preset: 'vercel', }), ], }); ``` ```ts tab="Tanstack Start" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'typesense-fumadocs-adapter'; import { Client } from 'typesense'; const records = JSON.parse(await fs.readFile('.vercel/output/static/static.json', 'utf-8')) as DocumentRecord[]; const client = new Client({ nodes: [{ url: process.env.VITE_TYPESENSE_URL! }], apiKey: process.env.TYPESENSE_API_KEY!, connectionTimeoutSeconds: 60 * 15, }); await sync(client, { typesenseCollectionName: 'docs', documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Tanstack Start" title="package.json" { "scripts": { "build": "vite build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` ```ts tab="Waku" title="src/pages/_api/static.json.ts" import { source } from '@/lib/source'; import type { DocumentRecord } from 'typesense-fumadocs-adapter'; async function getDocuments() { const documents: DocumentRecord[] = []; for (const page of source.getPages()) { documents.push({ _id: page.url, structured: page.data.structuredData, url: page.url, title: page.data.title, description: page.data.description, locale: page.locale, }); } return documents; } export async function GET() { return Response.json(await getDocuments()); } export async function getConfig() { return { render: 'static' as const, } as const; } ``` ```ts tab="Waku" title="scripts/sync-content.ts" import * as fs from 'node:fs/promises'; import { type DocumentRecord, sync } from 'typesense-fumadocs-adapter'; import { Client } from 'typesense'; const records = JSON.parse(await fs.readFile('dist/public/static.json', 'utf-8')) as DocumentRecord[]; const client = new Client({ nodes: [{ url: process.env.VITE_TYPESENSE_URL! }], apiKey: process.env.TYPESENSE_API_KEY!, connectionTimeoutSeconds: 60 * 15, }); await sync(client, { typesenseCollectionName: 'docs', documents: records, }); console.log(`search updated: ${records.length} records`); ``` ```json tab="Waku" title="package.json" { "scripts": { "build": "waku build && node --env-file-if-exists=.env.local scripts/sync-content.ts" } } ``` The `build` script syncs on every build, set the same variables on your deployment platform. ### Search UI [#search-ui] To implement the search UI, you can either: - Use [Fumadocs UI search dialog](/docs/search/typesense). - Build your own search UI with the Typesense search client hook. ```ts import { Client } from 'typesense'; import { useTypesenseSearch } from 'typesense-fumadocs-adapter/client'; const client = new Client({ nodes: [{ url: 'YOUR_TYPESENSE_SERVER_URL' }], apiKey: 'YOUR_TYPESENSE_SEARCH_ONLY_API_KEY', }); const { search, setSearch, query } = useTypesenseSearch({ typesenseCollectionName: 'docs', client, }); ``` ## Options [#options] ### Tag Filter [#tag-filter] To configure tag filtering, add a `tag` value to the documents in the `static.json` route: ```ts documents.push({ // other fields... // [!code ++] tag: '', }); ``` And update your search client: - **Fumadocs UI**: Enable [Tag Filter](/docs/search/typesense#tag-filter) on Search UI. - **Search Client**: You can add the tag filter like: ```ts import { useTypesenseSearch } from 'typesense-fumadocs-adapter/client'; const { search, setSearch, query } = useTypesenseSearch({ tag: '', // ... }); ``` ### Internationalization (i18n) [#internationalization-i18n] Documents include the `locale` of their pages, pass `locale` to the search client: ```ts import { useI18n } from 'fumadocs-ui/contexts/i18n'; import { useTypesenseSearch } from 'typesense-fumadocs-adapter/client'; const { locale } = useI18n(); const { search, setSearch, query } = useTypesenseSearch({ // [!code ++] locale, // other fields... }); ``` Under the hood, Typesense will create separate collections for each locale. The collection name is appended with the `_{locale}` suffix (e.g., `docs_en`, `docs_fr`). # Fumadocs Core (the core library of Fumadocs): Loader API URL: /docs/headless/source-api Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/source-api/index.mdx Turn content sources into a unified interface ## What it does? [#what-it-does] `loader()` provides an interface for Fumadocs to integrate with different content sources. - Generate [page slugs and page tree](/docs/headless/page-conventions). - Assign URL to each page. - Output useful utilities to interact with content. - `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 [#usage] You can use it with content sources like Fumadocs MDX, see [`source`](/docs/headless/source-api/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 [#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 [#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 [#icons] Load the [icon](/docs/headless/page-conventions#icons) 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 [#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`](/docs/headless/internationalization/config#fallback-language). ## Output [#output] It outputs a content loader instance. ### Get Page [#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-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 [#page-tree] ```ts import { source } from '@/lib/source'; // without i18n source.getPageTree(); // with i18n source.getPageTree('locale'); ``` ### Get from Node [#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 [#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 [#language-entries] Get available languages and its pages. ```ts import { source } from '@/lib/source'; // language -> pages const entries = source.getLanguages(); ``` ## Client API [#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 ...; } ``` ## Dynamic Loader [#dynamic-loader] `dynamicLoader()` provides extra revalidation functionality to content sources, you should use it with a [dynamic source](/docs/headless/source-api/source#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 [#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](/docs/headless/source-api/source#multiple-sources) setups, you can invalidate/revalidate a specific source: ```ts source.invalidate('docs'); await source.revalidate('docs'); ``` # Fumadocs Core (the core library of Fumadocs): Loader Plugins URL: /docs/headless/source-api/plugins Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/source-api/plugins.mdx Extend Loader API ## Overview [#overview] Loader plugins can extend `loader()` to customize its output. A list of built-in plugins: - **Lucide Icons**: use icons from [Lucide React](https://lucide.dev) (require `lucide-react` to be installed). ```ts import { loader } from 'fumadocs-core/source'; import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons'; export const source = loader({ // ... plugins: [lucideIconsPlugin()], }); ``` ## Creating Plugins [#creating-plugins] Each plugin is an object: ```ts tab="Strictly Typed" import { loader } from 'fumadocs-core/source'; export const source = loader({ plugins: ({ typedPlugin }) => [ typedPlugin({ // plugin config }), ], }); ``` ```ts tab="Simple" import { loader, type LoaderPlugin } from 'fumadocs-core/source'; export const source = loader({ plugins: [myPlugin()], }); function myPlugin(): LoaderPlugin { return { // plugin config }; } ``` `typedPlugin` enforces a more accurate type for plugin, including custom properties from your content source. But to make the plugin reusable (across different loaders), use the `LoaderPlugin` type instead. ### Storage [#storage] During the process, your input source files will be parsed and form a virtual storage in memory. To perform virtual file-system operations before processing, you can hook `transformStorage`. ```ts import { loader } from 'fumadocs-core/source'; export const source = loader({ plugins: ({ typedPlugin }) => [ typedPlugin({ transformStorage({ storage }) { storage.read('my/path'); }, }), ], }); ``` ### Page Tree [#page-tree] The page tree is generated from your file system, some unnecessary information (e.g. unused frontmatter properties) will be filtered. You can customize it using the `transformPageTree`, such as attaching custom properties to nodes, or customising the display name of pages. ```tsx import { loader } from 'fumadocs-core/source'; export const source = loader({ plugins: ({ typedPlugin }) => [ typedPlugin({ transformPageTree: { file(node, file) { // access the original (unfiltered) file data if (file) console.log(this.storage.read(file)); // modify nodes node.name = <>Some JSX Nodes here; return node; }, }, }), ], }); ``` # Fumadocs Core (the core library of Fumadocs): Source URL: /docs/headless/source-api/source Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/source-api/source.mdx Setup sources for Loader API ## Overview [#overview] Content loader accepts different content sources based on a `source` interface. ### Multiple Sources [#multiple-sources] Pass a record of source objects instead ```ts import { loader } from 'fumadocs-core/source'; import { blog, docs } from 'collections/server'; import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons'; export const source = loader( { docs: docs.toFumadocsSource(), openapi: blog.toFumadocsSource(), }, { baseUrl: '/docs', }, ); ``` To access properties exclusive to each source: ```ts const page = source.getPage(['...']); if (page.type === 'docs') { console.log(page.data); } else { console.log(page.data); } ``` ### Static Source [#static-source] To plug your own content source, create a `StaticSource` object. Since Loader API doesn't rely on file system, file paths only allow virtual paths like `file.mdx` and `content/file.mdx`, `./file.mdx` and `D://content/file.mdx` are not allowed. ```ts twoslash import type { StaticSource } from 'fumadocs-core/source'; export function createMySource(): StaticSource<{ metaData: { title: string; pages: string[] }; // Your custom type pageData: { title: string; description?: string }; // Your custom type }> { return { files: [ { type: 'page', path: 'folder/index.mdx', data: { title: 'Hello World', // ... }, }, { type: 'meta', path: 'meta.json', data: { title: 'Docs', pages: ['folder'], // ... }, }, ], }; } ``` ### Dynamic Source [#dynamic-source] For content sources with revalidation, return a `DynamicSource` instead: ```ts import type { DynamicSource } from 'fumadocs-core/source'; export function createMySource(): DynamicSource<{ metaData: { title: string; pages: string[] }; pageData: { title: string; description?: string }; }> { return { async files() { return [ { type: 'page', path: 'folder/index.mdx', data: { title: 'Hello World', }, }, { type: 'meta', path: 'meta.json', data: { title: 'Docs', pages: ['folder'], }, }, ]; }, configure(loader) { // you can revalidate the loader from source, e.g. onMyEvent(() => { void loader.revalidate(); }); }, }; } ``` Dynamic sources can only be consumed by [`dynamicLoader()`](/docs/headless/source-api#dynamic-loader). # Fumadocs Core (the core library of Fumadocs): Get TOC URL: /docs/headless/utils/get-toc Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/utils/get-toc.mdx Parse Table of contents from markdown/mdx content Parse Table of contents from markdown/mdx content. > [You can use the remark plugin directly](/docs/headless/mdx/headings) ## Usage [#usage] Note: If you're using a CMS, you should use the API provided by the CMS instead. ```ts import { getTableOfContents } from 'fumadocs-core/content/toc'; const toc = getTableOfContents('## markdown content'); ``` ### Output [#output] An array of [`TOCItemType`](/docs/headless/mdx/headings#output) is returned. # Fumadocs Core (the core library of Fumadocs): Last Modified Time URL: /docs/headless/utils/git-last-edit Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/utils/git-last-edit.mdx Get the last edit time of a file in Github repository ## Usage [#usage] Pass your repository name, and the path to file. ```ts import { getGithubLastEdit } from 'fumadocs-core/content/github'; const time = await getGithubLastEdit({ owner: 'fuma-nama', repo: 'fumadocs', // example: "content/docs/index.mdx" path: `content/docs/${page.path}`, }); ``` ### Github Token [#github-token] Notice that you may easily reach the rate limit in development mode. Hence, you should pass a Github token for a higher rate limit. Learn more about [Authenticating to the REST API](https://docs.github.com/en/rest/overview/authenticating-to-the-rest-api). ```ts import { getGithubLastEdit } from 'fumadocs-core/content/github' const time = await getGithubLastEdit({ ..., token: `Bearer ${process.env.GIT_TOKEN}` }) ``` Also, you can skip this in development mode if you don't need that functionality. ```ts process.env.NODE_ENV === 'development'? null : getGithubLastEdit(...) ``` ### Custom GitHub Base URL [#custom-github-base-url] If you need to access GitHub a instance at a url other than `https://api.github.com`, you can override the base URL: ```ts import { getGithubLastEdit } from 'fumadocs-core/content/github'; const time = await getGithubLastEdit({ owner: 'your-org', repo: 'your-repo', path: `content/docs/${page.path}`, baseUrl: 'https://api.octocorp.ghe.com', // Your custom GitHub API URL }); ``` The `baseUrl` parameter defaults to `'https://api.github.com'` if not specified. # Fumadocs Core (the core library of Fumadocs): Utilities URL: /docs/headless/utils Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/utils/index.mdx Utilities to provide extra functionality to your docs # Fumadocs Core (the core library of Fumadocs): Markdown Rendering URL: /docs/headless/utils/markdown Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/utils/markdown.mdx Render React elements into Markdown. ## Overview [#overview] `fumadocs-core/server` renders React Server Components, into a Markdown document: ```tsx tab="Input" import { renderToMarkdown } from 'fumadocs-core/server'; const text = await renderToMarkdown(

Hello World

Some rich content.

, ); ``` ```md tab="Output" ## Hello World Some **rich** content. ``` It powers the component exported by [Remark LLMs](/docs/headless/mdx/remark-llms#output) with `output: "function"`, and works on any server-rendered content you want to serve as Markdown. > `renderToMarkdown()` is server-only. ## `asMarkdown` [#asmarkdown] Components define their own Markdown form. Calling `asMarkdown()` during render is the opt-in: it returns `true` when the component is being rendered into Markdown, and the HTML elements it returns are converted. ```tsx import { asMarkdown, md } from 'fumadocs-core/server'; function Callout({ title, children }) { if (asMarkdown()) return md.linePrefix('> ')`**${title}**\n${children}`; return
...
; } ``` A component that never calls it is kept as JSX syntax with its props, like: ```mdx ... ``` This includes client components, which don't execute on the server. ## `md` [#md] Tagged template for Markdown output, interpolated values can be strings, React nodes, promises, or arrays of them. Use it to stringify `children` before manipulating them. Two variants return the content as a block: - `md.linePrefix(prefix)`: prefix every line, e.g. `'> '` for quote blocks. - `md.indent(size)`: indent every line. ## `renderRoute` [#renderroute] Render a page element into Markdown, or `undefined` when its component doesn't call `asMarkdown()`. ```tsx import { renderRoute } from 'fumadocs-core/server'; const text = await renderRoute(); ``` Useful for serving a Markdown variant of a route: pages that opt in produce Markdown, other pages return `undefined` so you can fall back to a 404 or another source. # Fumadocs Core (the core library of Fumadocs): MCP URL: /docs/headless/utils/mcp Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/utils/mcp.mdx Expose your docs to AI agents as MCP tools. `fumadocs-core/mcp` registers tools on a [MCP](https://modelcontextprotocol.io) server, for AI agents to read and search your docs. The [MCP Server](/docs/integrations/llms#mcp-server) guide sets up a route with these tools for you. ## Usage [#usage] ```package-install fumadocs-core @modelcontextprotocol/server zod ``` Register the tools with your source, its [`docsLlms`](/docs/integrations/llms#docsllms) renderer, and a search server: ```ts title="lib/mcp.ts" import { McpServer } from '@modelcontextprotocol/server'; import { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp'; import { createFromSource } from 'fumadocs-core/search/server'; import { source, docsLlms } from '@/lib/source'; export const mcp = new McpServer({ name: 'docs', version: '1.0.0' }); // `list_pages` & `get_page` registerSourceTools(mcp, source, docsLlms); // `search` registerSearchTool(mcp, createFromSource(source)); ``` For runtime content sources, pass the getter of your source: ```ts registerSourceTools(mcp, getSource, docsLlms); ``` Then connect the server to a transport, like the streamable HTTP transport. ## Tools [#tools] | Tool | Input | Output | | ------------ | ------------------ | --------------------------------------------------- | | `list_pages` | None | `llms.index()`, the index of pages | | `get_page` | `url` of a page | `llms.page(page)`, or an error when no page matches | | `search` | `query`, `locale?` | the search results, as JSON | # Fumadocs Core (the core library of Fumadocs): Negotiation URL: /docs/headless/utils/negotiation Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/utils/negotiation.mdx Content negotiation utilities. ## Overview [#overview] For React.js frameworks supporting middlewares, you can use the Negotiation API of Fumadocs for basic functionalities. ### Accept Markdown [#accept-markdown] Serve markdown or HTML depending on the `Accept` header, this can improve the experience for AI agents. ```ts title="proxy.ts (Next.js)" import { NextRequest, NextResponse } from 'next/server'; import { isMarkdownPreferred, rewritePath } from 'fumadocs-core/negotiation'; const { rewrite: rewriteLLM } = rewritePath('/docs/*path', '/llms.mdx/*path'); export default function proxy(request: NextRequest) { if (isMarkdownPreferred(request)) { const result = rewriteLLM(request.nextUrl.pathname); if (result) { return NextResponse.rewrite(new URL(result, request.nextUrl)); } } return NextResponse.next(); } ``` # Fumadocs Core (the core library of Fumadocs): Page Tree Utils URL: /docs/headless/utils/page-tree Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/utils/page-tree.mdx Common utilities to interact with page tree. ## Usage [#usage] See [Definitions](/docs/headless/page-tree) to learn more about Page Tree. ```ts import { findNeighbour, findSiblings, getPageTreeRoots, findParent, findPath, type Root, } from 'fumadocs-core/page-tree'; const pageTree: Root = { // ...the root of page tree }; // Get neighbours of a page const neighbours = findNeighbour(pageTree, '/url/to/page'); // Get a list of root folders const roots = getPageTreeRoots(pageTree); // Get the sibling nodes of a page const siblings = findSiblings(pageTree, '/url/to/page'); // Get the parent node of a page const parent = findParent(pageTree, '/url/to/page'); // Find the path to the first matching node const path = findPath( pageTree.children, // matcher (node) => node.type === 'page' && node.url === '/url/to/page', ); ``` # Fumadocs Core (the core library of Fumadocs): Shiki (Internal) URL: /docs/headless/utils/shiki Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/utils/shiki.mdx Manage the Shiki instances. ## Overview [#overview] Fumadocs use its own Shiki factory API to manage internal Shiki instances. You can customize it for special purposes like using fine-grained bundles, for example: ```ts import { createShikiFactory } from 'fumadocs-core/highlight/shiki'; export const myShikiFactory = createShikiFactory({ async init(options) { const { createHighlighter, createJavaScriptRegexEngine } = await import('shiki'); return createHighlighter({ langs: [], themes: [], langAlias: options?.langAlias, engine: createJavaScriptRegexEngine(), }); }, }); ``` Then pass it to different modules/features: ```tsx tab="Dynamic CodeBlock (UI)" 'use client'; import * as Base from 'fumadocs-ui/dynamic-codeblock.core'; export type DynamicCodeblockProps = Omit; export function DynamicCodeBlock(props: DynamicCodeblockProps) { return myShikiFactory.getOrInit()} {...props} />; } ``` ```ts tab="Rehype Code (Core)" import { createRehypeCode } from 'fumadocs-core/mdx-plugins/rehype-code.core'; const rehypeCode = createRehypeCode(myShikiFactory); ``` # Fumadocs MDX (the built-in content source): Bun URL: /docs/mdx/loader/bun Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/(integrations)/loader/bun.mdx Access content in Bun. ## Setup [#setup] Register the plugin in preload script: ```toml tab="bunfig.toml" preload = ["./scripts/preload.ts"] ``` ```ts tab="scripts/preload.ts" import { createMdxPlugin } from 'fumadocs-mdx/bun'; Bun.plugin(createMdxPlugin()); ``` It will add support for loading MDX/meta files on runtime. Now running any script with Bun, you can access your content files directly. ### Re-generate `.source` folder [#re-generate-source-folder] Optionally, you can re-generate the `.source` folder. It is useful when the folder could be missing, or you are using Vite, which defaults to generating code with Vite-specific APIs. ```ts title="preload.ts" tab="Next.js" import { postInstall } from 'fumadocs-mdx/next'; await postInstall({ configPath: 'source.config.ts' }); ``` ```ts title="preload.ts" tab="Vite" import { postInstall } from 'fumadocs-mdx/vite'; await postInstall({ configPath: 'source.config.ts', index: { target: 'bun', }, }); ``` # Fumadocs MDX (the built-in content source): Runtime Loader URL: /docs/mdx/loader Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/(integrations)/loader/index.mdx Fumadocs MDX loader integration. ## Introduction [#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. # Fumadocs MDX (the built-in content source): Node.js URL: /docs/mdx/loader/node Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/(integrations)/loader/node.mdx Access content in Node.js runtime. ## Setup [#setup] Make sure to run the script under ESM environment. ```js title="scripts/example.js" import { register } from 'fumadocs-mdx/node'; // register the Node.js loader register(); // accessing content const { source } = await import('./lib/source'); console.log(source.getPages()); ``` The `register()` function accepts options: ```ts register({ configPath: '...', // ... }); ``` # Fumadocs MDX (the built-in content source): Next.js URL: /docs/mdx/next Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/(integrations)/next.mdx Use Fumadocs MDX with Next.js ## Setup [#setup] Set up Fumadocs MDX for your Next.js application.
### Installation [#installation] ```npm npm i fumadocs-mdx fumadocs-core @types/mdx ``` Add the plugin to Next.js config: ```js title="next.config.mjs" import { createMDX } from 'fumadocs-mdx/next'; /** @type {import('next').NextConfig} */ const config = { reactStrictMode: true, }; // [!code ++:4] const withMDX = createMDX(); // [!code highlight] export default withMDX(config); ``` Fumadocs MDX is ESM-only, it's recommended to use `next.config.mjs` for accurate ESM resolution. For TypeScript config file, it requires Native Node.js TypeScript Resolver, you can see [Next.js docs](https://nextjs.org/docs/app/api-reference/config/typescript#using-nodejs-native-typescript-resolver-for-nextconfigts) for details. ### Integrate with Fumadocs [#integrate-with-fumadocs] Create a `lib/source.ts` file: ```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(), }); ``` ### Done [#done] You can now write content in `content/docs` folder.
## What is Next? [#what-is-next] Define collections in your app modules with `fumadocs-mdx/macro`. Define collections in `source.config.ts` with generated entry files. Hit performance bottleneck? You can try lazy loading. # Fumadocs MDX (the built-in content source): Vite URL: /docs/mdx/vite Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/(integrations)/vite.mdx Use Fumadocs MDX with Vite ## Setup [#setup]
### Installation [#installation] ```npm npm i fumadocs-mdx fumadocs-core @types/mdx ``` Add the Vite plugin: ```ts tab="Vite" title="vite.config.ts" import { defineConfig } from 'vite'; import { fumadocsMdx } from 'fumadocs-mdx/vite'; export default defineConfig({ plugins: [ // [!code ++] fumadocsMdx(), // ... ], }); ``` ```ts tab="Waku" title="waku.config.ts" import { type Config, defineConfig } from 'waku/config'; import { fumadocsMdx } from 'fumadocs-mdx/vite'; import type { UserConfig } from 'vite'; export default defineConfig({ vite: { plugins: [ // [!code ++] fumadocsMdx(), ], resolve: { tsconfigPaths: true, }, } satisfies UserConfig as Config['vite'], }); ``` ### Integrate with Fumadocs [#integrate-with-fumadocs] Create a `lib/source.ts` file: ```ts title="app/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(), }); ``` ### Done [#done] You can now write content in `content/docs` folder.
## What is Next? [#what-is-next] Define collections in your app modules with `fumadocs-mdx/macro`. Define collections in `source.config.ts` with generated entry files. Hit performance bottleneck? You can try lazy loading. # Fumadocs MDX (the built-in content source): Lazy Loading URL: /docs/mdx/async Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/async.mdx Configure lazy loading on collection outputs. ## Introduction [#introduction] By default, all Markdown and MDX files need to be pre-compiled first. The same constraint also applies to the development server. This may result in longer dev server start times for large docs sites. You can enable Async or Dynamic mode on doc collections to improve this. ## Async Mode [#async-mode] Async mode will generate collection outputs using async imports. ```ts tab="Docs Collection" import { defineDocs } from 'fumadocs-mdx/config'; export const docs = defineDocs({ dir: 'content/docs', docs: { // [!code ++] async: true, }, }); ``` ```ts tab="Doc Collection" import { defineCollections } from 'fumadocs-mdx/config'; export const doc = defineCollections({ type: 'doc', dir: 'content/docs', // [!code ++] async: true, }); ``` Depending on your bundler, this may reduce compilation time significantly on development server. Turbopack doesn't support lazy bundling at the moment, async mode will only improve server performance. ## Dynamic Mode [#dynamic-mode] Dynamic mode works by performing **on-demand compilation**, then execute the output on runtime. It is recommended to install & externalize `shiki`: ```npm npm i shiki ``` Enable dynamic mode in collections like: ```ts tab="Docs Collection" import { defineDocs } from 'fumadocs-mdx/config'; export const docs = defineDocs({ dir: 'content/docs', docs: { // [!code ++] dynamic: true, }, }); ``` ```ts tab="Doc Collection" import { defineCollections } from 'fumadocs-mdx/config'; export const doc = defineCollections({ type: 'doc', dir: 'content/docs', // [!code ++] dynamic: true, }); ``` Then, follow [Dynamic Entry](/docs/mdx/entry/dynamic) to access the collection from dynamic entry instead. Dynamic mode comes with some limitations on MDX features. - **No import/export allowed in MDX files.** For MDX components, pass them from the `components` prop instead. - **Images must be referenced with URL (e.g. `/images/test.png`).** Don't use **file paths** like `./image.png`. You should locate your images in the `public` folder and reference them with URLs. ## Usage [#usage] Frontmatter properties are still available on `page` object, but you need to invoke the `load()` async function to load the compiled content (and its exports). For example: ```tsx title="page.tsx" import { source } from '@/lib/source'; import { getMDXComponents } from '@/components/mdx'; const page = source.getPage(['...']); if (page) { // frontmatter properties are available console.log(page.data); // Markdown content requires await const { body: MdxContent, toc } = await page.data.load(); console.log(toc); return ; } ``` When using Async or Dynamic mode, we highly recommend to use third-party services to implement search, which usually have better capability to handle massive amount of content to index. # Fumadocs MDX (the built-in content source): Collections URL: /docs/mdx/collections Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/collections.mdx Collection of content data for your app ## Define Collections [#define-collections] Define a collection to parse a certain set of files. ```ts import { defineCollections } from 'fumadocs-mdx/config'; import { z } from 'zod'; export const blog = defineCollections({ type: 'doc', dir: './content/blog', schema: z.object({ // schema }), // other options }); ``` ### `type` [#type] The accepted type of collection. ```ts import { defineCollections } from 'fumadocs-mdx/config'; // only scan for json/yaml files export const metaFiles = defineCollections({ type: 'meta', // options }); ``` - `type: meta` Accept JSON/YAML files, available options: ### MetaCollection | Prop | Type | Description | | --------- | -------- | ----------- | | `type` | `"meta"` | | | `schema?` | `union` | | | `dir` | `string` | | | `files?` | `array` | | - `type: doc` Markdown/MDX documents, available options: ### DocCollection | Prop | Type | Description | | ----------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `"doc"` | | | `compiler?` | `union` | | | `mdxOptions?` | `union` | By defining a collection-level MDX options, **the default options & plugins will be removed**. | | `satteriOptions?` | `SatteriOptionsInput` | Sätteri compile options. When omitted, the global `satteriOptions` preset is used. | | `dir` | `string` | | | `files?` | `array` | | | `postprocess?` | `Partial` | | | `async?` | `union` | | | `dynamic?` | `union` | | | `schema?` | `union` | | | `lastModified?` | `union` | Expose the last modified date of each document. - `true`: obtained from Git. Requires `git` to be installed. If you are using Vercel, please set the `VERCEL_DEEP_CLONE` environment variable to `true`. - A function: return the last modified time for a given file path. | ### `schema` [#schema] The schema to validate file data (frontmatter on `doc` type, content on `meta` type). ```ts import { defineCollections } from 'fumadocs-mdx/config'; import { z } from 'zod'; export const blog = defineCollections({ type: 'doc', dir: './content/blog', schema: z.object({ name: z.string(), }), }); ``` > [Standard Schema](https://standardschema.dev) compatible libraries, including Zod, are supported. Note that the validation is done at build time, hence the output must be serializable. You can also pass a function that receives the transform context. ```ts import { defineCollections } from 'fumadocs-mdx/config'; import { z } from 'zod'; export const blog = defineCollections({ type: 'doc', dir: './content/blog', schema: (ctx) => { return z.object({ name: z.string(), testPath: z.string().default( // original file path ctx.path, ), }); }, }); ``` ### `mdxOptions` [#mdxoptions] Customize MDX options at the collection level. > This API is only available on `doc` type. ```ts title="source.config.ts" import { defineCollections } from 'fumadocs-mdx/config'; export const blog = defineCollections({ type: 'doc', mdxOptions: { // mdx options }, }); ``` By design, this will remove all default settings applied by your global config and Fumadocs MDX. You have full control over MDX options. You can use [`applyMdxPreset`](/docs/mdx/mdx) to apply the default MDX preset. ```ts title="source.config.ts" import { defineCollections } from 'fumadocs-mdx/config'; export const blog = defineCollections({ type: 'doc', mdxOptions: applyMdxPreset({ // extend the preset }), }); ``` ### `postprocess` [#postprocess] > This API is only available on `doc` type. You can pass build-time information to runtime using the `postprocess` API. - #### `includeProcessedMarkdown` [#includeprocessedmarkdown] Export the Markdown of documents, generated by [Remark LLMs](/docs/headless/mdx/remark-llms). Pass `true` or its options. ```ts tab="docs" import { defineDocs } from 'fumadocs-mdx/config'; export default defineDocs({ docs: { postprocess: { // [!code ++] includeProcessedMarkdown: true, }, }, }); ``` ```ts tab="doc" import { defineCollections } from 'fumadocs-mdx/config'; export const blog = defineCollections({ type: 'doc', postprocess: { // [!code ++] includeProcessedMarkdown: true, }, }); ``` Read it with `getText('processed')`, e.g. from `loader()`: ```ts import { source } from '@/lib/source'; const page = source.getPage('...'); console.log(await page.data.getText('processed')); ``` - #### `valueToExport` [#valuetoexport] Some remark plugins store their output in `vfile.data` (compile-time memory) which cannot be accessed from your code. You can specify the name of properties to include, they will get converted into ESM exports that you can access when importing the MDX file. ```ts tab="docs" import { defineDocs } from 'fumadocs-mdx/config'; export default defineDocs({ docs: { postprocess: { // [!code ++] valueToExport: ['dataName'], }, }, }); ``` ```ts tab="doc" import { defineCollections } from 'fumadocs-mdx/config'; export const blog = defineCollections({ type: 'doc', postprocess: { // [!code ++] valueToExport: ['dataName'], }, }); ``` ## Define Docs [#define-docs] Define a collection for Fumadocs. ```ts import { defineDocs } from 'fumadocs-mdx/config'; export const docs = defineDocs({ dir: '/my/content/dir', docs: { // optional, options of `doc` collection }, meta: { // optional, options of `meta` collection }, }); ``` ### `dir` [#dir] Instead of per collection, you should customize content directory from `defineDocs`: ```ts import { defineDocs } from 'fumadocs-mdx/config'; export const docs = defineDocs({ dir: 'content/guides', }); ``` ### `schema` [#schema-1] You can extend the default Zod 4 schema of `docs` and `meta`. ```ts import { defineDocs } from 'fumadocs-mdx/config'; import { metaSchema, pageSchema } from 'fumadocs-core/source/schema'; import { z } from 'zod'; export const docs = defineDocs({ docs: { schema: pageSchema.extend({ index: z.boolean().default(false), }), }, meta: { schema: metaSchema.extend({ // other props }), }, }); ``` # Fumadocs MDX (the built-in content source): Browser Entry URL: /docs/mdx/entry/browser Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/entry/browser.mdx Access collections from browser/client environment. Macro collections can be imported on the client directly, without a client loader. See [Macro API](/docs/mdx/macro#browser) for the equivalent usage. ## Usage [#usage] The generated outputs are optimized using async imports for browser environments. Only doc/docs collections are accessible on browser. ```tsx import browserCollections from 'collections/browser'; // unloaded entries console.log(browserCollections['collection name'].raw); // define client-side collection loader const clientLoader = browserCollections['collection name'].createClientLoader({ component( // access the compiled file { default: MDX }, // define props (optional) props: { myProp: string }, ) { return (
); }, }); // server (static generator) return a path async function serverLoader() { return source.getPage(slugs)!.path; } async function loader() { const path = await serverLoader(); // preload the path await clientLoader.preload(path); return { path }; } function Page() { const { path } = loader(); // render the content return clientLoader.useContent(path, { myProp: 'hello world', }); } ``` ## Examples [#examples] The examples below are for non-RSC usage. Prefer RSC when possible for better client performance. The example apps in our repository use the [Macro API](/docs/mdx/macro) for this instead, they are a good reference for a complete setup. ### Tanstack Start [#tanstack-start] ```tsx title="src/routes/docs/$.tsx" import { createFileRoute, notFound } from '@tanstack/react-router'; import { createServerFn } from '@tanstack/react-start'; import { source } from '@/lib/source'; import browserCollections from 'collections/browser'; export const Route = createFileRoute('/docs/$')({ component: Page, loader: async ({ params }) => { const data = await loader({ data: params._splat?.split('/') ?? [] }); await clientLoader.preload(data.path); return data; }, }); const loader = createServerFn({ method: 'GET', }) .validator((slugs: string[]) => slugs) .handler(async ({ data: slugs }) => { const page = source.getPage(slugs); if (!page) throw notFound(); return { path: page.path, }; }); const clientLoader = browserCollections.docs.createClientLoader({ component({ frontmatter, default: MDX }) { return (

{frontmatter.title}

); }, }); function Page() { const data = Route.useLoaderData(); return clientLoader.useContent(data.path); } ``` ### React Router [#react-router] ```tsx import type { Route } from './+types/docs'; import { source } from '@/lib/source'; import browserCollections from 'collections/browser'; export async function loader({ params }: Route.LoaderArgs) { const slugs = params['*'].split('/').filter((v) => v.length > 0); const page = source.getPage(slugs); if (!page) throw new Response('Not found', { status: 404 }); return { path: page.path, }; } const clientLoader = browserCollections.docs.createClientLoader({ component({ frontmatter, default: MDX }) { return (

{frontmatter.title}

); }, }); export default function Page(props: Route.ComponentProps) { const { path } = props.loaderData; return clientLoader.useContent(path); } ``` # Fumadocs MDX (the built-in content source): Dynamic Entry URL: /docs/mdx/entry/dynamic Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/entry/dynamic.mdx Accessing your collections with on-demand compilation. ## Usage [#usage] Enable [dynamic mode](/docs/mdx/async#dynamic-mode) on docs/doc collections, and import it from `dynamic` entry instead of `server`. It offers the same output as `server` entry, while compiled properties have to be loaded via `load()` method. ```ts tab="Docs (Meta + Doc)" // [!code highlight] import { docs } from 'collections/dynamic'; import { loader } from 'fumadocs-core/source'; export const source = loader({ baseUrl: '/docs', source: docs.toFumadocsSource(), }); ``` ```ts tab="Doc" // [!code highlight] import { blogPosts } from 'collections/dynamic'; import { toFumadocsSource } from 'fumadocs-mdx/runtime/server'; import { loader } from 'fumadocs-core/source'; export const blog = loader({ baseUrl: '/blog', source: toFumadocsSource(blogPosts, []), }); ``` # Fumadocs MDX (the built-in content source): Import MDX Files URL: /docs/mdx/entry/import Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/entry/import.mdx Importing MDX files directly. ## Introduction [#introduction] MDX files will be compiled into JavaScript with exports like: - `default`: the main component. - `frontmatter`: frontmatter data. - other properties generated by remark/rehype plugins. Hence, they can also be used as a page, or components that you can import. ### Using as Component [#using-as-component] You can import them as a component: ```tsx title="app/my-page.tsx" import MyPage from '@/content/page.mdx'; import { getMDXComponents } from '@/components/mdx'; export default function Page() { return (
{/* pass MDX components and render it */}
); } ``` ### Using as Page [#using-as-page] On Next.js, you can use `page.mdx` instead of `page.tsx` for creating a new page under the app directory. ```mdx tab="app/my-page/page.mdx" --- title: My Page --- export { default } from '@/components/layouts/page'; # Hello World This is my page. ``` ```tsx tab="components/layouts/page.tsx" import type { ComponentProps } from 'react'; export default function Content(props: ComponentProps<'main'>) { return (
{props.children}
); } ``` It doesn't have **MDX components** by default, you have to provide them: ```tsx tab="components/mdx.tsx" import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultMdxComponents, // for Fumadocs UI ...components, } satisfies MDXComponents; } // export a `useMDXComponents()` that returns MDX components export const useMDXComponents = getMDXComponents; // [!code ++] ``` ```ts tab="source.config.ts" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { // Path to import your `components/mdx.tsx` above. [!code ++] providerImportSource: '@/components/mdx', }, }); ``` Other React.js frameworks like Tanstack Start usually prefer explicit declaration of loaders, it's recommended to use them as components in your pages instead. # Fumadocs MDX (the built-in content source): Accessing Collections URL: /docs/mdx/entry Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/entry/index.mdx Access collection outputs from entry files. ## Overview [#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](/docs/mdx/macro). ### Entry Files [#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. # Fumadocs MDX (the built-in content source): Server Entry URL: /docs/mdx/entry/server Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/entry/server.mdx Accessing collection outputs from server. ## Usage [#usage] While you can access the collections directly from entry file, we suggest using it with [`loader()`](/docs/headless/source-api#output). ```ts tab="Docs (Meta + Doc)" import { docs } from 'collections/server'; import { loader } from 'fumadocs-core/source'; // raw collection console.log(docs); export const source = loader({ baseUrl: '/docs', source: docs.toFumadocsSource(), }); ``` ```ts tab="Doc" import { blogPosts } from 'collections/server'; import { toFumadocsSource } from 'fumadocs-mdx/runtime/server'; import { loader } from 'fumadocs-core/source'; // raw collection console.log(blogPosts); export const blog = loader({ baseUrl: '/blog', source: toFumadocsSource(blogPosts, []), }); ``` ## Examples [#examples] ```tsx import { source } from '@/lib/source'; const page = source.getPage(['slugs']); if (page) { // access page data [!code highlight] console.log(page.data); // frontmatter properties are also available [!code highlight] console.log(page.data.title); } ``` To render the page (on RSC), use `page.data.body` as a component. ```tsx import { getMDXComponents } from '@/components/mdx'; const MDX = page.data.body; // set your MDX components with `components` prop return ; ``` # Fumadocs MDX (the built-in content source): Global Options URL: /docs/mdx/global Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/global.mdx Customize Fumadocs MDX ## Global Options [#global-options] Shared options of Fumadocs MDX. ```ts title="source.config.ts" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ // global options }); ``` ### GlobalConfig | Prop | Type | Description | | ------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `plugins?` | `array` | | | `compiler?` | `union` | The compiler for files compiled without a collection (e.g. `page.mdx` routes). Collections choose their own compiler via the collection-level `compiler` option. Default: `'mdx'` | | `mdxOptions?` | `union` | Configure global MDX options, used by `doc` collections with the default MDX compiler. | | `satteriOptions?` | `SatteriOptionsInput` | Configure global Sätteri options, used by `doc` collections with `compiler: "satteri"`. | | `workspaces?` | `Record` | | | `experimentalBuildCache?` | `string` | | ### MDX Options [#mdx-options] Customize the [default MDX preset](/docs/mdx/mdx). ```ts title="source.config.ts" import { defineConfig } from 'fumadocs-mdx/config'; import rehypeKatex from 'rehype-katex'; import remarkMath from 'remark-math'; export default defineConfig({ mdxOptions: { remarkPlugins: [remarkMath], // When order matters rehypePlugins: (v) => [rehypeKatex, ...v], }, }); ``` Or using the minimal preset: ```ts title="source.config.ts" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { preset: 'minimal', // now it accepts only MDX processor options }, }); ``` # Fumadocs MDX (the built-in content source): Include URL: /docs/mdx/include Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/include.mdx Reuse content from other files. ## Usage [#usage] ### Content [#content] To display content from another Markdown/MDX file, specify its path relative to the file itself in the `` tag. ```mdx tab="MDX" ./another.mdx ``` ```md tab="Markdown" ::include[./another.mdx] ``` Markdown (`.md`) files do not support JSX syntax. To use ``, you need to configure `remark-directive`. ```ts title="source.config.ts" import { defineConfig } from 'fumadocs-mdx/config'; import remarkDirective from 'remark-directive'; export default defineConfig({ mdxOptions: { // [!code ++] remarkPlugins: [remarkDirective], }, }); ``` You may also need `rehype-raw` for HTML comments & content. ### CodeBlock [#codeblock] For other types of files, it will become a codeblock: ```mdx tab="MDX" ./script.ts page.md ``` ```md tab="Markdown" ::include[./script.ts] ::include[page.md]{lang=md meta='title="lib.md"'} ``` You can include only a region of content: ```ts title="code.ts" export function fn() { //#region a console.log('one'); //#endregion } ``` ```mdx tab="MDX" ./code.ts#a ``` ```md tab="Markdown" ::include[./code.ts#a] ``` ### `cwd` [#cwd] Resolve relative paths from cwd instead of the current file: ```mdx tab="MDX" ./script.ts ``` ```md tab="Markdown" ::include[./script.ts]{cwd lang=tsx meta='title="lib.ts"'} ``` ## Extraction [#extraction] When referencing content files, you can only include a certain part of the document. ### Section [#section] Encapsulate by a `
` tag: ```mdx tab="a.mdx" ## Hello World
This is included
This is not included. ``` ```mdx tab="b.mdx" Include the content from `a.mdx`: a.mdx#test ``` In Markdown: ```md tab="a.md" ## Hello World :::section{#test} This is included ::: This is not included. ``` ```md tab="b.md" Include the content from `a.mdx`: ::include[a.md#test] ``` ### Heading [#heading] Include contents under a certain heading. ```mdx tab="a.mdx" ## Included Section I'm here! ## Not Included Some random text. ``` ```mdx tab="b.mdx" Content under the heading: a.mdx#included-section ``` In Markdown: ```md tab="a.md" ## Included Section I'm here! ## Not Included Some random text. ``` ```md tab="b.md" Content under the heading: ::include[a.mdx#included-section] ``` # Fumadocs MDX (the built-in content source): Getting Started URL: /docs/mdx Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/index.mdx Introducing Fumadocs MDX, the official content source of Fumadocs. ## Introduction [#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? [#what-is-a-collection] **Collection** refers to a collection containing a certain type of files. You can define them with the [Macro API](/docs/mdx/macro) 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: 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', }); ``` 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', }); ``` 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 }, }); ``` 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](/docs/mdx/collections), which generates entry files under `.source`. ## Installation [#installation] Configure Fumadocs MDX on: Using Fumadocs MDX with Next.js. Using Fumadocs MDX with Vite. Using Fumadocs MDX with a JavaScript runtime. ## FAQ [#faq] ### Built-in Properties [#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 [#customize-frontmatter] Use the [`schema`](/docs/mdx/collections#schema-1) option to pass a validation schema to validate frontmatter and define its output properties. ### Customize MDX Compiler [#customize-mdx-compiler] Fumadocs MDX uses [MDX Compiler](https://mdxjs.com/packages/mdx) to compile MDX files into JavaScript files. The [default preset](/docs/mdx/mdx) includes a set of plugins and configurations out-of-the-box. You can customize it on [Global Config](/docs/mdx/global#mdx-options) or [Collection Config](/docs/mdx/collections#mdxoptions). # Fumadocs MDX (the built-in content source): Last Modified Time URL: /docs/mdx/last-modified Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/last-modified.mdx Output the last modified time of a document ## Usage [#usage] You can add the plugin from the config file. Please ensure you have Git installed on your machine, and **the repository is not shallow cloned**, as it relies on your local Git history. ```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()], }); ``` ### Access the Property [#access-the-property] After doing this, a `lastModified` property will be exported for each document (as `Date`). ```ts import { source } from '@/lib/source'; const page = source.getPage(['...']); console.log(page.data.lastModified); // or with lazy loading const { lastModified } = await page.data.load(); console.log(lastModified); ``` You can display it from UI like [``](/docs/ui/layouts/page#last-updated-time) # Fumadocs MDX (the built-in content source): Macro API URL: /docs/mdx/macro Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/macro.mdx Define collections in your app with fumadocs-mdx/macro. ## Overview [#overview] Declare collections directly in your application modules with `fumadocs-mdx/macro`. The bundler (or runtime loader) compiles `defineDocs` / `defineCollections` into imports of content files. - No codegen needed. - No `source.config.ts` needed. ## Usage [#usage] ```ts tab="lib/source.ts" import { defineDocs } from 'fumadocs-mdx/macro'; import { loader } from 'fumadocs-core/source'; export const docs = defineDocs({ dir: 'content/docs', }); export const source = loader({ baseUrl: '/docs', source: docs.toFumadocsSource(), }); ``` ```ts tab="lib/blog.ts" import { defineCollections } from 'fumadocs-mdx/macro'; import { loader } from 'fumadocs-core/source'; const blog = defineCollections({ type: 'doc', dir: 'content/blog', }); export const blogSource = loader({ baseUrl: '/blog', source: blog.toFumadocsSource(), }); ``` Collection options are largely the same as the [config API](/docs/mdx/collections). You can still keep a `source.config.ts` for [global options](/docs/mdx/global) and plugins while defining collections with macros. ### Browser [#browser] Macro collections work in the browser too. Import the collection in a client component and render an entry with `body`, no [client loader](/docs/mdx/entry/browser) needed. ```tsx import { docs } from '@/lib/source'; export default function Page({ path }: { path: string }) { const page = docs.getPage(path); if (!page) return
Not Found
; const MDX = page.body; return (

{page.title}

); } ``` The browser runs the whole module you import `docs` from. When your `loader()` has server-only sources, like `openapi.staticSource()`, define collections in another module (e.g. `lib/collections.ts`) and import them from there. #### Async collections [#async-collections] With [`async`](/docs/mdx/async) enabled, only the frontmatter is bundled upfront and the content is fetched on demand. `body` works the same way, except it suspends until the content is available. Two extra methods control that: - `preload()` loads the content ahead of rendering, so `body` renders without suspending. Call it in your route loader. - `load()` returns the loaded content, such as `toc`. Its promise is cached, so you can call it during render. ```tsx import { use } from 'react'; import { docs } from '@/lib/source'; // in your route loader (React Router, Tanstack Start) export async function loader({ path }: { path: string }) { await docs.getPage(path)?.preload(); return { path }; } export default function Page({ path }: { path: string }) { const page = docs.getPage(path); if (!page) return
Not Found
; const { toc } = use(page.load()); const MDX = page.body; return (

{page.title}

); } ``` Since the content is loaded lazily, wrap the page in a `` boundary unless you preloaded it. ## Configuration [#configuration] The Macro API is enabled by default. Pass `macro` to limit which modules may use it, or `false` to disable it. ```js tab="Next.js" title="next.config.mjs" import { createMDX } from 'fumadocs-mdx/next'; const withMDX = createMDX({ macro: { // glob patterns relative to the project root include: ['lib/**/*.ts', 'lib/**/*.tsx'], }, // or disable entirely: // macro: false, }); ``` ```ts tab="Vite" title="vite.config.ts" import { fumadocsMdx } from 'fumadocs-mdx/vite'; export default { plugins: [ fumadocsMdx({ macro: { include: ['src/**/*.ts', 'src/**/*.tsx'], }, }), ], }; ``` ```js tab="Node.js" import { register } from 'fumadocs-mdx/node'; register({ macro: { include: ['lib/**/*.ts'], }, }); ``` By default, all JS/TS modules are eligible (`node_modules` is always excluded). Narrow `include` if you want the transform to run only on specific paths. ### Constraints [#constraints] Macro calls are compiled statically. Keep these rules in mind: - Assign each call to a top-level `const` (the variable name identifies the collection): ```ts // ✓ const docs = defineDocs({ dir: 'content/docs' }); // ✗ not supported export function create() { return defineDocs({ dir: 'content/docs' }); } ``` - Options that shape how content is bundled (`dir`, `files`, `async`) must be statically analyzable (string / boolean literals). Put dynamic logic in options like `schema` or `mdxOptions` instead. - [`dynamic`](/docs/mdx/async#dynamic-mode) collections are not supported by the Macro API yet. Prefer [`async`](/docs/mdx/async) for lazy loading. - Re-exporting from `fumadocs-mdx/macro` is not supported. # Fumadocs MDX (the built-in content source): MDX Presets URL: /docs/mdx/mdx Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/mdx.mdx Customize the default configurations for MDX processor. Fumadocs MDX provides MDX presets to simplify configurations of features like **syntax highlighting**. ## Default Preset [#default-preset] A preset designed for documentation sites, it is enabled by default for global MDX options. To override the defaults, it accepts an interface inherited from [`ProcessorOptions`](https://mdxjs.com/packages/mdx/#processoroptions): ### Remark Plugins [#remark-plugins] These plugins are applied by default: - [Remark Image](/docs/headless/mdx/remark-image) - Handle images. - [Remark Heading](/docs/headless/mdx/headings) - Extract table of contents. - [Remark Structure](/docs/headless/mdx/structure) - Generate search indexes. Add other remark plugins with: ```ts tab="Global Config" import { defineConfig } from 'fumadocs-mdx/config'; import { myPlugin } from './remark-plugin'; export default defineConfig({ mdxOptions: { remarkPlugins: [myPlugin], // You can also pass a function to control the order of remark plugins. remarkPlugins: (v) => [myPlugin, ...v], }, }); ``` ```ts tab="Collection Config" import { defineCollections, applyMdxPreset } from 'fumadocs-mdx/config'; import { myPlugin } from './remark-plugin'; export const blog = defineCollections({ type: 'doc', mdxOptions: applyMdxPreset({ remarkPlugins: [myPlugin], // You can also pass a function to control the order of remark plugins. remarkPlugins: (v) => [myPlugin, ...v], }), }); ``` ### Rehype Plugins [#rehype-plugins] These plugins are applied by default: - [Rehype Code](/docs/headless/mdx/rehype-code) - Syntax highlighting. - [Rehype TOC](/docs/headless/mdx/headings#rehype-toc) - Export table of contents. Same as remark plugins, you can pass an array or a function to add other rehype plugins. ```ts tab="Global Config" import { defineConfig } from 'fumadocs-mdx/config'; import { myPlugin } from './rehype-plugin'; export default defineConfig({ mdxOptions: { rehypePlugins: (v) => [myPlugin, ...v], }, }); ``` ```ts tab="Collection Config" import { defineCollections, applyMdxPreset } from 'fumadocs-mdx/config'; import { myPlugin } from './rehype-plugin'; export const blog = defineCollections({ type: 'doc', mdxOptions: applyMdxPreset({ rehypePlugins: (v) => [myPlugin, ...v], }), }); ``` ### Customize Built-in Plugins [#customize-built-in-plugins] Customize the options of built-in plugins like: ```ts tab="Global Config" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { rehypeCodeOptions: { // options }, remarkImageOptions: { // set image placeholder to `blur` (only effective on Next.js) placeholder: 'blur', }, remarkHeadingOptions: { // options }, }, }); ``` ```ts tab="Collection Config" import { defineCollections, applyMdxPreset } from 'fumadocs-mdx/config'; export const blog = defineCollections({ type: 'doc', mdxOptions: applyMdxPreset({ rehypeCodeOptions: { // options }, remarkImageOptions: { // options }, remarkHeadingOptions: { // options }, }), }); ``` ## Source Map [#source-map] Source maps are disabled by default, since generating them slows down compilation. To enable, install `source-map` and pass its generator to MDX options: ```npm npm i -D source-map ``` ```ts tab="Global Config" title="source.config.ts" import { defineConfig } from 'fumadocs-mdx/config'; import { SourceMapGenerator } from 'source-map'; export default defineConfig({ mdxOptions: { SourceMapGenerator, }, }); ``` ```ts tab="Collection Config" import { defineCollections, applyMdxPreset } from 'fumadocs-mdx/config'; import { SourceMapGenerator } from 'source-map'; export const blog = defineCollections({ type: 'doc', mdxOptions: applyMdxPreset({ SourceMapGenerator, }), }); ``` Errors thrown from compiled content can now be traced back to the original Markdown file. # Fumadocs MDX (the built-in content source): Performance URL: /docs/mdx/performance Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/performance.mdx The performance of Fumadocs MDX ## Overview [#overview] Fumadocs MDX is a bundler plugin, in other words, it has a higher performance bottleneck depending on the underlying bundler. For typical bundlers like Turbopack and Vite, it is enough for large docs sites with nearly 500+ MDX files, which is sufficient for almost all use cases. Since Fumadocs MDX works with your bundler, you can import any files including client components in your MDX files. This allows high flexibility and ensures everything is optimized by default. ### Development Server [#development-server] There's two categories: - **Lazy**: Bundlers like Vite, and runtime-side loaders like Node.js & Bun, can compile MDX files on-demand when a module is requested (e.g. when `await import()` is called), the dev server performance will not be significantly affected by the number of MDX files. However, ensure to enable [Lazy Loading](/docs/mdx/async), so that lazy compilation can take effect. - **Precompiled**: Webpack & Turbopack do not support lazy compilation on dev mode, all MDX files must be precompiled ahead of time. The dev server might be slow depending of the number of MDX files to precompile. While this is a bundler-level limitation, we recommend [Local Markdown](/docs/integrations/content/local-md) as an alternative if the slowness is a concern. It skips the bundler layer, MDX files are loaded & compiled on runtime. See the linked introduction for details. ### Image Optimization [#image-optimization] Fumadocs MDX resolves images into static imports with [Remark Image](/docs/headless/mdx/remark-image). Therefore, your images will be optimized automatically for your React framework (e.g. Next.js Image API). ```mdx ![Hello](./hello.png) or in public folder ![Hello](/hello.png) ``` Yields: ```mdx import HelloImage from './hello.png'; Hello ``` ![Banner](/banner.png) ### Production Build [#production-build] In a production build, the most consuming parts are usually TypeScript docgens (the TypeScript Compiler is slow without tsgo), and the trivial work with all MDX files summed up. Without cache, a huge amount of MDX files can cause extremely high memory usage during build. This is because: - After MDX files are compiled into JavaScript, bundlers also have to bundle the output JavaScript files. - Even the compilation time of each MDX file is trivial, it can be slow when summed up. For example, ~2500 files, assuming 25ms per file, cold build will take up to 62.5 seconds. The main solution is to make the compilation on-demand, such that content is only loaded when it's being requested. #### Local Markdown [#local-markdown] As suggested in the development server section, [Local Markdown](/docs/integrations/content/local-md) is a good alternative that runs purely on runtime, without extra compilation work. #### Remote Source [#remote-source] Create your custom content source with remoting content loading. If your content source doesn't need to pre-compile content files, it can process them on-demand with SSG which can **improve your build speed.** However, you cannot leverage build time optimizations (e.g. bundling). See [Custom Source](/docs/integrations/content/custom) for implementing custom sources. # Fumadocs MDX (the built-in content source): Generate Types URL: /docs/mdx/typegen Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/typegen.mdx Generate types without running dev/build server ### Overview [#overview] You can run `fumadocs-mdx` to generate types & entry files (the `.source` folder). Optionally, you can run it in post install to ensure types are generated when initializing the project. ```json title="package.json" { "scripts": { "postinstall": "fumadocs-mdx" } } ``` If your project uses a custom path for configuration file or output directory, you can provide them as command arguments: ```npm npx fumadocs-mdx "config-path" "output-dir" ``` - `config-path`: Fumadocs MDX config file, default to `source.config.ts`. - `output-dir`: Directory of generated files, default to `.source`. # Fumadocs MDX (the built-in content source): Workspace URL: /docs/mdx/workspace Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/mdx/workspace.mdx Use Fumadocs MDX in multiple workspace. ## Overview [#overview] **Workspace** in Fumadocs MDX, refers to an independent project with its own config and content. Fumadocs MDX Workspace is not limited to the traditional meaning of workspace in package managers. They do not need to have its own `package.json`, only a config file is needed. To define a workspace, add: ```ts title="source.config.ts" import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ workspaces: { 'my-workspace': { dir: 'my-workspace', config: await import('./my-workspace/source.config.ts'), }, }, }); ``` When writing content in a workspace, note that: - `cwd` refers to the **current workspace directory**. - configs will not inherit, workspaces are always independent. By running dev or build server, you should see collection entries from all workspaces to be generated. ### Accessing Collections [#accessing-collections] You can access the generated files of a workspace at `.source/{workspace}/*`. For example: ```ts title="lib/source.ts" import { docs } from 'collections/my-workspace/server'; ``` > The output location of root workspace is not changed. To integrate [multi-source](/docs/headless/source-api/source#multiple-sources) in Fumadocs: ```ts title="lib/source.ts" import { loader } from 'fumadocs-core/source'; import { docs } from 'collections/server'; import * as MyWorkspace from 'collections/my-workspace/server'; export const source = loader( { root: docs.toFumadocsSource(), 'my-workspace': MyWorkspace.docs.toFumadocsSource(), }, { baseUrl: '/docs', }, ); ``` ## When to Use [#when-to-use] In some setups, you might have multiple Fumadocs MDX configs with their own content directory. With workspaces, you can integrate them into one Fumadocs MDX config, and access all collections of each workspace. This is crucial for use cases like storing content across multiple repos, a simplified setup would be: - let your main docs repo be $A$, and other repos be $T_n$ - for each repo $T_n$: - $T_n$ has $A$ as a git submodule. - $T_n$ defines its own config `source.config.ts` and work independently. - when a commit is made to the content in $T_n$, it triggers a GitHub action (or CI), which creates a new deployment on $A$. - when a deployment is triggered on $A$: - Fumadocs MDX handles each $T_n$ as a workspace. - each workspace generates its own collection entries, e.g. `collections/{repo}/server`. - Fumadocs `loader()` integrate multiple sources into one. # Fumadocs (Framework Mode): Scalar Example URL: /docs/openapi Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/openapi/index.mdx View the Scalar Galaxy example OpenAPI schema. # Fumadocs UI (the default theme of Fumadocs): Component Library URL: /docs/ui/component-library Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/component-library.mdx The underlying headless component library. ## Overview [#overview] Fumadocs UI maintains support for both [Base UI](https://base-ui.com) and [Radix UI](https://radix-ui.com/primitives), while it uses Base UI by default. The configuration may be different if you use Fumadocs CLI for customising components. ### Base UI [#base-ui] Base UI is the default component library. If you install manually, alias `fumadocs-ui` to the `@fumadocs/base-ui` package: ```json title="package.json" { "dependencies": { "fumadocs-ui": "npm:@fumadocs/base-ui@latest", // make sure Base UI is already installed "@base-ui/react": "..." } } ``` For Fumadocs CLI, this is the default `uiLibrary` value: ```json title="cli.json" { "uiLibrary": "base-ui" } ``` ### Radix UI [#radix-ui] You can opt-in to use Radix UI by installing the `fumadocs-ui` package directly: ```json title="package.json" { "dependencies": { "fumadocs-ui": "latest" } } ``` For Fumadocs CLI, configure it in your `cli.json` config: ```json title="cli.json" { "uiLibrary": "radix-ui" } ``` # Fumadocs UI (the default theme of Fumadocs): Accordion URL: /docs/ui/components/accordion Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/accordion.mdx Add Accordions to your documentation ## Usage [#usage] Based on [Base UI Accordion](https://base-ui.com/react/components/accordion), useful for FAQ sections. > For the Radix UI package, this is based on [Radix UI Accordion](https://www.radix-ui.com/primitives/docs/components/accordion) instead. Use it in MDX files or as a normal React component. ```mdx tab="MDX" --- title: Hello World --- import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; My Content ``` ```tsx tab="React.js" import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; export default function Page() { return ( My Content ); } ``` ### Accordions [#accordions] ### AccordionsProps | Prop | Type | Description | | ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `render?` | `union` | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. | | `disabled?` | `union` | Whether the component should ignore user interaction. Default: `false` | | `hiddenUntilFound?` | `union` | Allows the browser's built-in page search to find and expand the panel contents. Overrides the `keepMounted` prop and uses `hidden="until-found"` to hide the element without removing it from the DOM. Default: `false` | | `keepMounted?` | `union` | Whether to keep the element in the DOM while the panel is closed. This prop is ignored when `hiddenUntilFound` is used. Default: `false` | | `loopFocus?` | `union` | **Deprecated.** Deprecated following the [APG guidance update](https://github.com/w3c/aria-practices/pull/3434) to remove roving focus. This prop no longer affects keyboard focus behavior. | | `multiple?` | `union` | Whether multiple items can be open at the same time. Default: `false` | | `orientation?` | `union` | **Deprecated.** Deprecated following the [APG guidance update](https://github.com/w3c/aria-practices/pull/3434) to remove roving focus. This prop no longer affects keyboard focus behavior. Default: `'vertical'` | ### Accordion [#accordion] ### AccordionProps | Prop | Type | Description | | --------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `render?` | `union` | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. | | `disabled?` | `union` | Whether the component should ignore user interaction. Default: `false` | | `onOpenChange?` | `function` | Event handler called when the panel is opened or closed. | | `value?` | `string` | | ### Linking to Accordion [#linking-to-accordion] You can specify an `id` for accordion. The accordion will automatically open when the user is navigating to the page with the specified `id` in hash parameter. ```mdx My Content ``` > The value of accordion is same as title by default. When an id presents, it will be used as the value instead. # Fumadocs UI (the default theme of Fumadocs): Auto Type Table URL: /docs/ui/components/auto-type-table Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/auto-type-table.mdx Auto-generated type table ### AutoTypeTableExample | Prop | Type | Description | | ---------- | ----------------- | -------------------------------------------------------------------------------------------------------- | | `name` | `union` | Markdown syntax like links, `code` are supported. See https://fumadocs.dev/docs/ui/components/type-table | | `fn` | `function` | | | `options?` | `Partial` | We love Shiki. ```ts console.log("Hello World, powered by Shiki"); ``` Default: `{ a: "test" }` | You cannot use this in a client component, instead, try the [build-time MDX integration](/docs/integrations/typescript#mdx-integration) instead. It generates a table for your docs based on TypeScript definitions. ## Usage [#usage] ```npm npm i fumadocs-typescript ``` Initialize the TypeScript compiler and add it as a MDX component. ```tsx title="components/mdx.tsx" import defaultComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; import { createGenerator, createFileSystemGeneratorCache } from 'fumadocs-typescript'; import { AutoTypeTable, type AutoTypeTableProps } from 'fumadocs-typescript/ui'; const generator = createGenerator({ // set a cache, necessary for serverless platform like Vercel cache: createFileSystemGeneratorCache('.next/fumadocs-typescript'), }); export function getMDXComponents(components?: MDXComponents) { return { ...defaultComponents, // [!code ++] AutoTypeTable: (props: Partial) => ( ), ...components, } satisfies MDXComponents; } ``` You can now reference `` in your MDX content. See [TypeScript DocGen](/docs/integrations/typescript) for more usages. ### From File [#from-file] It accepts a `path` prop that points to a typescript file, and `name` for the exported type name. ```ts title="path/to/file.ts" export interface MyInterface { name: string; } ``` ```mdx title="content.mdx" ``` The path is relative to your project directory (`cwd`), because `AutoTypeTable` is a React Server Component, it cannot access build-time information like MDX file path. ### From Type [#from-type] You can specify the type to generate, without an actual TypeScript file. ```mdx title="content.mdx" ``` When a `path` is given, it shares the same context as the TypeScript file. ```ts title="file.ts" export type A = { hello: string }; ``` ```mdx title="content.mdx" ``` When `type` has multiple lines, the export statement and `name` prop are required. ```mdx title="content.mdx" ``` ### Functions [#functions] Notice that only object type is allowed. For functions, you should wrap them into an object instead. ```ts export interface MyInterface { myFn: (input: string) => void; } ``` ## TypeScript Compiler [#typescript-compiler] Under the hood, it uses the [Typescript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) to extract type information. Your `tsconfig.json` file in the current working directory will be loaded. You can change the compiler settings from [`createGenerator()`](/docs/integrations/typescript). ```ts import { createGenerator, createFileSystemGeneratorCache } from 'fumadocs-typescript'; const generator = createGenerator({ tsconfigPath: './tsconfig.json', // where to resolve relative paths (normally cwd) basePath: './', // other options... }); ``` ### File System [#file-system] It relies on the file system, hence, the page referencing this component must be built in **build time**. Rendering the component on serverless runtime may cause problems. ## References [#references] ### AutoTypeTableProps | Prop | Type | Description | | ----------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path?` | `string` | The path to source TypeScript file. | | `name?` | `string` | Exported type name to generate from. | | `type?` | `string` | Set the type to generate from. When used with `name`, it generates the type with `name` as export name. ```ts export const myName = MyType; ``` When `type` contains multiple lines, `export const` is not added. You need to export it manually, and specify the type name with `name`. ```tsx ``` | | `generator` | `object` | | | `shiki?` | `union` | Shiki configuration when using default `renderMarkdown` & `renderType` | | `options?` | `object` | | | `renderMarkdown?` | `function` | | | `renderType?` | `function` | | # Fumadocs UI (the default theme of Fumadocs): Banner URL: /docs/ui/components/banner Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/banner.mdx Add a banner to your site ## Usage [#usage] Put the element at the top of your root layout, you can use it for displaying announcements. ```tsx import { Banner } from 'fumadocs-ui/components/banner'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {/* [!code ++] */} Hello World {children} ); } ``` ### Variant [#variant] Change the default variant. ```tsx import { Banner } from 'fumadocs-ui/components/banner'; Hello World; // customize colors Hello World ; ``` ### Change Layout [#change-layout] By default, the banner uses a `style` tag to modify Fumadocs layouts (e.g. reduce the sidebar height). You can disable it with: ```tsx import { Banner } from 'fumadocs-ui/components/banner'; Hello World; ``` ### Close [#close] To allow users to close the banner, give the banner an ID. ```tsx import { Banner } from 'fumadocs-ui/components/banner'; Hello World; ``` The state will be automatically persisted. # Fumadocs UI (the default theme of Fumadocs): Code Block URL: /docs/ui/components/codeblock Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/codeblock.mdx Displaying Shiki highlighted code blocks
```js title="config.js" import createMDX from 'fumadocs-mdx/config'; const withMDX = createMDX(); // [!code word:config] /** @type {import('next').NextConfig} */ const config = { // [!code highlight] reactStrictMode: true, // [!code highlight] }; // [!code highlight] export default withMDX(config); ```
This is a **MDX component** meant to be used with [Rehype Code](/docs/headless/mdx/rehype-code) to display highlighted codeblocks. You can refer to [Markdown](/docs/markdown#codeblock) for the syntax of writing codeblocks. Supported features: - Copy button - Custom titles and icons > If you're looking for an equivalent with runtime syntax highlighting, see [Dynamic Code Block](/docs/ui/components/dynamic-codeblock). ## Usage [#usage] Wrap the pre element in ``, which acts as the wrapper of code block. ```tsx title="components/mdx.tsx" import defaultComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultComponents, // HTML `ref` attribute conflicts with `forwardRef` pre: ({ ref: _ref, ...props }) => (
{props.children}
{/* [!code highlight] */}
), ...components, } satisfies MDXComponents; } ``` ### Keep Background [#keep-background] Use the background color generated by Shiki. ```tsx import { Pre, CodeBlock } from 'fumadocs-ui/components/codeblock';
{props.children}
; ``` ### Icons [#icons] Specify a custom icon by passing an `icon` prop to `CodeBlock` component. By default, the icon will be injected by the custom Shiki transformer. ```js title="config.js" console.log('js'); ``` # Fumadocs UI (the default theme of Fumadocs): Code Block (Dynamic) URL: /docs/ui/components/dynamic-codeblock Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/dynamic-codeblock.mdx A codeblock that also highlights code ## Usage [#usage] ### Client Component [#client-component] ```tsx import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; export function MyComp() { const code = `console.log("Hello World")`; return ; } ``` Unlike the MDX [`CodeBlock`](/docs/ui/components/codeblock) component, this is a client component that can be used without MDX. It highlights the code with Shiki and use the default component to render it. Features: - using React.js 19 Suspense. - can be pre-rendered on server. - load languages and themes on browser lazily. #### Options [#options] ```tsx import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; ; ``` ### Server Component [#server-component] For a server component equivalent, you can use the RSC version: ```tsx import { ServerCodeBlock } from 'fumadocs-ui/components/codeblock.rsc'; ; ``` # Fumadocs UI (the default theme of Fumadocs): Files URL: /docs/ui/components/files Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/files.mdx Display file structure in your documentation ## Usage [#usage] Wrap file components in `Files`, you can use it in your MDX content, or as a normal React.js component. ```mdx title="content.mdx" import { File, Folder, Files } from 'fumadocs-ui/components/files'; ``` ### File [#file] ### FileProps | Prop | Type | Description | | ------- | ----------- | ----------- | | `name` | `string` | | | `icon?` | `ReactNode` | | ### Folder [#folder] ### FolderProps | Prop | Type | Description | | -------------- | -------- | --------------------------------------- | | `name` | `string` | | | `disabled?` | `union` | | | `defaultOpen?` | `union` | Open folder by default Default: `false` | ## Remark Plugin [#remark-plugin] You can enable [`remark-mdx-files`](/docs/headless/mdx/remark-mdx-files) for additional feature & syntax. ```tsx title="source.config.ts (Fumadocs MDX)" import { remarkMdxFiles } from 'fumadocs-core/mdx-plugins/remark-mdx-files'; import { defineConfig } from 'fumadocs-mdx/config'; export default defineConfig({ mdxOptions: { // [!code ++] remarkPlugins: [remarkMdxFiles], }, }); ``` ### CodeBlock Syntax [#codeblock-syntax] It will convert `files` codeblocks into `` component, like: ````md title="content.md" ```files project ├── src │ ├── index.js │ └── utils │ └── helper.js ├── package.json ``` ```` ### `` [#auto-files] Generate `` component from glob. ```mdx title="content.mdx" ``` # Fumadocs UI (the default theme of Fumadocs): GitHub Info URL: /docs/ui/components/github-info Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/github-info.mdx Display your GitHub repository information ## Usage [#usage] ```tsx import { GithubInfo } from 'fumadocs-ui/components/github-info'; export function MyComp() { return ( ); } ``` It's recommended to add it to your docs layout with `links` option: ```tsx title="app/docs/layout.tsx" import { DocsLayout, type DocsLayoutProps } from 'fumadocs-ui/layouts/notebook'; import { baseOptions } from '@/lib/layout.shared'; import { source } from '@/lib/source'; import { GithubInfo } from 'fumadocs-ui/components/github-info'; function docsOptions(): DocsLayoutProps { return { ...baseOptions(), tree: source.getPageTree(), links: [ { type: 'custom', children: , }, ], }; } export default function Layout({ children }: { children: React.ReactNode }) { return {children}; } ``` # Fumadocs UI (the default theme of Fumadocs): Graph View URL: /docs/ui/components/graph-view Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/graph-view.mdx A graph of all pages. ## Installation [#installation] You can install this from CLI: ```npm npx @fumadocs/cli add graph-view ``` Enable `extractLinkReferences` on Fumadocs MDX. ```ts title="source.config.ts" import { defineDocs } from 'fumadocs-mdx/config'; export const docs = defineDocs({ docs: { postprocess: { // [!code ++] extractLinkReferences: true, }, }, }); ``` ## Usage [#usage] You can use it in MDX files or the layout components (e.g. in `page.tsx`): ```tsx title="page.tsx" import { GraphView } from '@/components/graph-view'; import { buildGraph } from '@/lib/build-graph'; export function PageBody() { return (
{/* ... */}
); } ``` # Fumadocs UI (the default theme of Fumadocs): Zoomable Image URL: /docs/ui/components/image-zoom Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/image-zoom.mdx Allow zoom-in images in your documentation ## Usage [#usage] Replace `img` with `ImageZoom` in your MDX components. ```tsx title="components/mdx.tsx" import { ImageZoom } from 'fumadocs-ui/components/image-zoom'; import defaultComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultComponents, // [!code ++] img: (props) => , ...components, } satisfies MDXComponents; } ``` Now image zoom will be automatically enabled on all images. ```mdx ![Test](/banner.png) ``` ### Image Optimization [#image-optimization] On Next.js, a default [`sizes` property](https://nextjs.org/docs/app/api-reference/components/image#sizes) will be defined for `` component if not specified. # Fumadocs UI (the default theme of Fumadocs): Components URL: /docs/ui/components Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/index.mdx Additional components to improve your docs ## Overview [#overview] Additional components that you can use: ### MDX Components [#mdx-components] The default MDX components include Cards, Callouts, Code Blocks and Headings. ```ts import defaultMdxComponents from 'fumadocs-ui/mdx'; ``` ### Relative Link [#relative-link] 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 ( ); ``` ```mdx [My Link](./file.mdx) ``` Example: [`../../(integrations)/feedback.mdx`](<../../(integrations)/feedback.mdx>) # Fumadocs UI (the default theme of Fumadocs): Inline TOC URL: /docs/ui/components/inline-toc Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/inline-toc.mdx Add Inline TOC into your documentation ## Usage [#usage] You can use it in your MDX content: ```mdx title="content.mdx" import { InlineTOC } from 'fumadocs-ui/components/inline-toc'; Table of Contents ``` Or adding it to every page. ```tsx title="page.tsx" import { DocsPage } from 'fumadocs-ui/layouts/docs/page'; import { InlineTOC } from 'fumadocs-ui/components/inline-toc'; export default function Page() { // ... return ( {/* [!code ++] */} Table of Contents ); } ``` ## Reference [#reference] ### InlineTOCProps | Prop | Type | Description | | --------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `render?` | `union` | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. | | `open?` | `union` | Whether the collapsible panel is currently open. To render an uncontrolled collapsible, use the `defaultOpen` prop instead. | | `defaultOpen?` | `union` | Whether the collapsible panel is initially open. To render a controlled collapsible, use the `open` prop instead. Default: `false` | | `onOpenChange?` | `function` | Event handler called when the panel is opened or closed. | | `disabled?` | `union` | Whether the component should ignore user interaction. Default: `false` | | `items` | `array` | | # Fumadocs UI (the default theme of Fumadocs): Steps URL: /docs/ui/components/steps Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/steps.mdx Adding steps to your docs ## Usage [#usage] Put your steps into the `Steps` container. ```mdx title="content.mdx" import { Step, Steps } from 'fumadocs-ui/components/steps'; ### Hello World ### Hello World ``` ### Without imports [#without-imports] You can use the Tailwind CSS utilities without importing it. ```mdx
``` It supports adding step styles to only headings with arbitrary variants. ```mdx
### Hello World
```
### Hello World [#hello-world] Rendered without the `` component.
# Fumadocs UI (the default theme of Fumadocs): Tabs URL: /docs/ui/components/tabs Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/tabs.mdx A Tabs component built with Base UI, with additional features such as persistent and shared value. ## Usage [#usage] Add MDX components. ```tsx title="components/mdx.tsx" import defaultMdxComponents from 'fumadocs-ui/mdx'; import * as TabsComponents from 'fumadocs-ui/components/tabs'; import type { MDXComponents } from 'mdx/types'; export function getMDXComponents(components?: MDXComponents) { return { ...defaultMdxComponents, ...TabsComponents, // [!code ++] ...components, } satisfies MDXComponents; } ``` And use it in your MDX content: ```mdx title="content.mdx" Javascript is weird Rust is fast ``` Javascript is weird Rust is fast ### Without `value` [#without-value] Without a `value`, it detects from the children index. Note that it might cause errors on re-renders, it's not encouraged if the tabs might change. ```mdx import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; Javascript is weird Rust is fast ``` Javascript is weird Rust is fast ### Shared Value [#shared-value] By passing an `groupId` property, you can share a value across all tabs with the same id. ```mdx Javascript is weird Rust is fast ``` ### Persistent [#persistent] When `groupId` is specified, their value will be temporarily stored in `sessionStorage`. To persist the value, you can specify a `persist` prop. The value will be stored in `localStorage`, with its group ID as storage key. ```mdx Javascript is weird Rust is fast ``` ### Default Value [#default-value] Set a default value by passing `defaultIndex`. ```mdx Javascript is weird Rust is fast ``` ### Link to Tab [#link-to-tab] Use HTML `id` attribute to link to a specific tab. ```mdx Javascript is weird Rust is fast `Hello World` ``` You can add the hash `#tab-cpp` to your URL and reload, the C++ tab will be activated. Javascript is weird Rust is fast `Hello World` Additionally, the `updateAnchor` property can be set to `true` in the `Tabs` component to automatically update the URL hash whenever time a new tab is selected: ```mdx Javascript is weird Rust is fast `Hello World` ``` Hello! World! ## Advanced Usage [#advanced-usage] Use it in the primitive way: ```mdx npm Hello World ``` npm npm Hello World # Fumadocs UI (the default theme of Fumadocs): Type Table URL: /docs/ui/components/type-table Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/components/type-table.mdx A table for documenting types ## Usage [#usage] It accepts a `type` property. ```mdx import { TypeTable } from 'fumadocs-ui/components/type-table'; ``` ## References [#references] ### Type Table [#type-table] ### TypeTableProps | Prop | Type | Description | | ------ | ------------------------ | ----------- | | `type` | `Record` | | ### Object Type [#object-type] ### ObjectTypeProps | Prop | Type | Description | | ---------------------- | ----------- | ---------------------------------------------------- | | `description?` | `ReactNode` | Additional description of the field | | `type` | `ReactNode` | type signature (short) | | `typeDescription?` | `ReactNode` | type signature (full) | | `typeDescriptionLink?` | `string` | Optional `href` for the type | | `default?` | `ReactNode` | | | `required?` | `union` | | | `deprecated?` | `union` | | | `parameters?` | `array` | a list of parameters info if the type is a function. | | `returns?` | `ReactNode` | | # Fumadocs UI (the default theme of Fumadocs): Overview URL: /docs/ui Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/index.mdx The default theme of Fumadocs **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](/docs/cli) to install the components locally and have full control. ## Learn More [#learn-more] # Fumadocs UI (the default theme of Fumadocs): Docs Layout URL: /docs/ui/layouts/docs Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/layouts/docs.mdx The layout of documentation The layout of documentation pages, it includes a sidebar and **mobile-only** navbar/header. ![preview](./docs.png) ## Usage [#usage] Pass your page tree to the component. ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { baseOptions } from '@/lib/layout.shared'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` See detailed docs for [`links`](/docs/ui/layouts/links) and [`nav`](/docs/ui/layouts/nav) options. ### References [#references] ### $Fumadocs | Prop | Type | Description | | ----------------- | ----------------- | ----------------- | | `githubUrl?` | `string` | GitHub url | | `links?` | `array` | | | `nav?` | `object` | navigation config | | `themeSwitch?` | `object` | | | `searchToggle?` | `object` | | | `i18n?` | `union` | **Deprecated.** | | `tree` | `object` | | | `sidebar?` | `object` | | | `tabMode?` | `union` | | | `tabs?` | `union` | | | `containerProps?` | `object` | | | `aiChat?` | `object` | | | `slots?` | `Partial` | | ## Sidebar [#sidebar] ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/docs'; ; ``` ### Sidebar Items [#sidebar-items] Customize sidebar navigation links.
![Sidebar](/docs/sidebar.png)
Sidebar items are rendered from the page tree you passed to ``. For page tree from [`loader()`](/docs/headless/source-api), it generates the tree from your file structure, see [available patterns](/docs/page-conventions). ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { source } from '@/lib/source'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` You may hardcode it too: ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` ### Layout Tabs (Dropdown) [#layout-tabs] Layout Tabs are folders with tab-like behaviours, only the content of opened tab will be visible.
![Layout Tabs](/docs/sidebar-tabs.png)
By default, the tab trigger will be displayed as a **Dropdown** component (hidden unless one of its items is active). Tabs are generated from the page tree, you can mark folders as [Root Folders](/docs/page-conventions#root-folder) by creating a `meta.json` file in the folder: ```json title="content/docs/my-folder/meta.json" { "title": "Name of Folder", "description": "The description of root folder (optional)", "root": true } ``` Or specify them explicitly: ```tsx title="/app/docs/layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/docs'; ; ``` Set it to `false` to disable: ```tsx import { DocsLayout } from 'fumadocs-ui/layouts/docs'; ; ``` You can specify a `banner` to the [Docs Layout](/docs/ui/layouts/docs) component. ```tsx import { DocsLayout, type DocsLayoutProps } from 'fumadocs-ui/layouts/docs'; import type { ReactNode } from 'react'; import { baseOptions } from '@/lib/layout.shared'; import { source } from '@/lib/source'; export default function Layout({ children }: { children: ReactNode }) { return ( Hello World, }} > {children} ); } ``` #### Decoration [#decoration] Change the icon/styles of layout tabs. ```tsx import { DocsLayout } from 'fumadocs-ui/layouts/docs'; ({ ...option, icon: , }), }} />; ``` ### Tabs Groups [#tabs-groups] Tabs are grouped by [root type](/docs/page-conventions#root-type), with a dropdown for each root folder on the current page's path. On `v2/framework/quickstart.mdx`, the sidebar shows a dropdown for the version and another for the section:
![Tabs Groups](/docs/sidebar-tabs-groups.png)
Selecting an option navigates to the same page in that root folder, or its index page when the page doesn't exist there. See [Versioning](/docs/navigation#versioning) for a complete example. ### Sidebar Components [#sidebar-components] You can replace certain components for rendering page tree. ```tsx tab="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import { SidebarSeparator } from './layout.client'; ; ``` ```tsx tab="layout.client.tsx" 'use client'; import * as Base from 'fumadocs-ui/components/sidebar/base'; export function SidebarSeparator({ className, style, children, ...props }: ComponentProps<'p'>) { const depth = Base.useFolderDepth(); return ( {children} ); } ``` ### References [#references-1] ### SidebarProps | Prop | Type | Description | | ------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------- | | `defaultOpenLevel?` | `number` | Open folders by default if their level is lower or equal to a specific level (Starting from 1) Default: `0` | | `prefetch?` | `union` | Prefetch links, default behaviour depends on your React.js framework. | | `components?` | `Partial` | | | `banner?` | `ReactNode` | | | `footer?` | `ReactNode` | | | `collapsible?` | `union` | Support collapsing the sidebar on desktop mode Default: `true` | | `enabled?` | `union` | | | `component?` | `ReactNode` | **Deprecated.** | | `tabMode?` | `union` | **Deprecated.** | | `tabs?` | `union` | **Deprecated.** | ## AI Chat [#ai-chat] Pass your chat panel to `aiChat` with its open state, the layout places it beside the page on wide screens, and floats it over the page on smaller ones. ```tsx }} /> ``` See [Ask AI](/docs/integrations/llms#ask-ai) for a pre-built chat. ## Advanced [#advanced] ### Prefetching [#prefetching] Fumadocs use the `` component of your React framework, and keep their default prefetch behaviours. On Vercel, prefetch requests may cause a higher usage of serverless functions and Data Cache. It can also hit the limits of some other hosting platforms. You can disable prefetching to reduce the amount of prefetch requests, or enable explicitly: ```tsx import { DocsLayout } from 'fumadocs-ui/layouts/docs'; ; ``` ### The Layout System [#the-layout-system] Handling layout is challenging, Fumadocs UI needed an approach that is: - **Composable:** Layout components should manage their position and size effortlessly, ideally in place. - **Flexible:** The system should avoid reliance on fixed values or heights, enabling seamless integration of external components, such as AI chat interfaces. - **Cohesive:** Components should respond to changes in others, for instance, by animating sidebar collapses. - **Predictable:** Layout properties should remain centralized, allowing the final result to be readily anticipated from the source code. - **Compatible:** The solution should work on older browsers by leveraging only Baseline Widely Available CSS features. Fumadocs UI does this with a grid system: ```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)); --fd-sidebar-col: var(--fd-sidebar-width); --fd-page-col: calc( var(--fd-layout-width, 97rem) - var(--fd-sidebar-width) - var(--fd-toc-width) ); --fd-sidebar-width: 0px; --fd-toc-width: 0px; --fd-header-height: 0px; --fd-toc-popover-height: 0px; } ``` - The layout container uses grid layout, `grid-template` is set to produce predictable result. - `--fd-docs-row-*` define the top offset of each row, allowing elements with `position: sticky` to hook a correct top offset. - `--fd-*-width` and `--fd-*-height` are set by layout components using CSS, they are essential to maintain the grid structure, or calculating `--fd-docs-row-*`. - `--fd-*-col` are dynamic values, updated as state changes (e.g. `--fd-sidebar-col` becomes `0px` when the sidebar is collapsed). Both default and the notebook layout utilize this system. # Fumadocs UI (the default theme of Fumadocs): Flux Layout URL: /docs/ui/layouts/flux Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/layouts/flux.mdx An aggressively minimal layout for docs A docs layout that impractically minimal & clean, originally made for experimental purpose. ![Flux](./flux.png) ## Usage [#usage] Enable the Flux layout with `fumadocs-ui/layouts/flux`. ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/flux'; // [!code highlight] import { baseOptions } from '@/lib/layout.shared'; import { source } from '@/lib/source'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` Make sure to update your page import too: ```tsx title="page.tsx" import { ... } from 'fumadocs-ui/layouts/docs/page'; // [!code --] import { ... } from 'fumadocs-ui/layouts/flux/page'; // [!code ++] ``` - It is best to be paired with [static/local search](/docs/search/orama#static), the navigation is more efficient using document search. - It prioritizes subjective aesthetics over user experience. - It is opinionated, use Fumadocs CLI for customization instead. ## Configurations [#configurations] The options are inherited from [Docs Layout](/docs/ui/layouts/docs), with minor differences: - Flux layout is a client component, you cannot pass unserializable props from a server component. - A `renderNavigationPanel` prop to customize the bottom navigation panel. - There is no `tocPopover` options. # Fumadocs UI (the default theme of Fumadocs): Glass Layout URL: /docs/ui/layouts/glass Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/layouts/glass.mdx A docs layout with a floating, translucent sidebar and header A docs layout built around floating, translucent (glass-like) panels: the sidebar and header sit on top of the content as rounded, blurred surfaces rather than fixed columns. ![Glass](./glass.png) ## Usage [#usage] Enable the Glass layout with `fumadocs-ui/layouts/glass`. ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/glass'; // [!code highlight] import { baseOptions } from '@/lib/layout.shared'; import { source } from '@/lib/source'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` Make sure to update your page import too: ```tsx title="page.tsx" import { ... } from 'fumadocs-ui/layouts/docs/page'; // [!code --] import { ... } from 'fumadocs-ui/layouts/glass/page'; // [!code ++] ``` ### Styles [#styles] Unlike other layouts, Glass layout styles are **not** bundled into the CSS preset, so you don't pay for them unless you use the layout. Import them in your Tailwind CSS file: ```css title="Tailwind CSS" @import 'tailwindcss'; /* [!code ++] */ @import 'fumadocs-ui/css/generated/glass.css'; @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; ``` - Glass layout is a **client component**, you cannot pass unserializable props from a server component. - It is opinionated: the sidebar and header are more fixed than the default layout, use Fumadocs CLI or the `slots` option for customization. ## Configurations [#configurations] The options are inherited from [Docs Layout](/docs/ui/layouts/docs), with minor differences: - there is no `sidebar` option, Glass layout is more opinionated than the default one. - additional options (see below). ### Layout Tabs [#layout-tabs] Configure [Layout Tabs](/docs/ui/layouts/docs#layout-tabs) with the `tabs` prop, see the linked docs for how to add them. Tabs are shown as a dropdown in the sidebar. ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/glass'; import { baseOptions } from '@/lib/layout.shared'; import { source } from '@/lib/source'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` ### AI Chat [#ai-chat] The floating header can render an **Ask AI** button. Pass an `aiChat` object with the open state and a change handler to control your AI search dialog, the layout places your chat `panel` beside the page on wide screens, and floats it over the page on smaller ones. ```tsx title="layout.tsx" 'use client'; import { DocsLayout } from 'fumadocs-ui/layouts/glass'; import { useState } from 'react'; export function Layout({ children }) { const [open, setOpen] = useState(false); return ( }} // [!code ++] // ... > {children} ); } ``` ### Slots [#slots] Like other layouts, Glass layout exposes a `slots` option to override the built-in `header`, `sidebar`, and `sidebarDrawer` (mobile) components, in addition to the shared slots (`navTitle`, `themeSwitch`, `searchTrigger`, `languageSelect`). ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/glass'; ; ``` # Fumadocs UI (the default theme of Fumadocs): Home Layout URL: /docs/ui/layouts/home-layout Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/layouts/home-layout.mdx Shared layout for other pages A layout with only navbar. ![preview](./home.png) ## Usage [#usage] Add a navbar and search dialog across other pages. ```tsx 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 {children}; } ``` See detailed docs for [`links`](/docs/ui/layouts/links) and [`nav`](/docs/ui/layouts/nav) options. # Fumadocs UI (the default theme of Fumadocs): Layouts URL: /docs/ui/layouts Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/layouts/index.mdx A list of layout components. ## Overview [#overview] Fumadocs UI provides essential layouts to display content. ### Configurations [#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 ( {children} ); } ``` ```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 ( {children} ); } ``` See detailed docs for [`links`](/docs/ui/layouts/links) and [`nav`](/docs/ui/layouts/nav) options. ### BaseLayoutProps | Prop | Type | Description | | --------------- | ----------------- | ----------------- | | `githubUrl?` | `string` | GitHub url | | `links?` | `array` | | | `nav?` | `object` | navigation config | | `slots?` | `Partial` | | | `children?` | `ReactNode` | | | `themeSwitch?` | `object` | | | `searchToggle?` | `object` | | | `i18n?` | `union` | **Deprecated.** | # Fumadocs UI (the default theme of Fumadocs): Layout Links URL: /docs/ui/layouts/links Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/layouts/links.mdx Navigation configurations on all layouts. ## Overview [#overview] Fumadocs allows adding navigation links to your layouts with a `links` prop, like linking to your "showcase" page.
<>![Nav](/docs/nav-layout-home.png) <>![Nav](/docs/nav-layout-docs.png)
```tsx tab="All" title="lib/layout.shared.tsx" import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; export function baseOptions(): BaseLayoutProps { return { links: [], // [!code ++] }; } ``` ```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 ( {children} ); } ``` ```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 ( {children} ); } ``` You can see all supported items below: ### Link Item [#link-item] A link to navigate to a URL/href, can be external. ```tsx title="lib/layout.shared.tsx" import { BookIcon } from 'lucide-react'; import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; export function baseOptions(): BaseLayoutProps { return { links: [ { icon: , text: 'Blog', url: '/blog', // secondary items will be displayed differently on navbar secondary: false, }, ], }; } ``` #### Active Mode [#active-mode] The conditions to be marked as active. | Mode | Description | | ------------ | ----------------------------------------------------------- | | `url` | When browsing the specified url | | `nested-url` | When browsing the url and its child pages like `/blog/post` | | `none` | Never be active | ```tsx title="lib/layout.shared.tsx" import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; export function baseOptions(): BaseLayoutProps { return { links: [ { text: 'Blog', url: '/blog', active: 'nested-url', }, ], }; } ``` ### Icon Item [#icon-item] Same as link item, but is shown as an icon button. Icon items are secondary by default. ```tsx title="lib/layout.shared.tsx" import { BookIcon } from 'lucide-react'; import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; export function baseOptions(): BaseLayoutProps { return { links: [ { type: 'icon', label: 'Visit Blog', // `aria-label` icon: , text: 'Blog', url: '/blog', }, ], }; } ``` ### Custom Item [#custom-item] Display a custom component. ```tsx title="lib/layout.shared.tsx" import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; export function baseOptions(): BaseLayoutProps { return { links: [ { type: 'custom', children: , secondary: true, }, ], }; } ``` ### GitHub URL [#github-url] There's also a shortcut for adding GitHub repository link item. ```tsx twoslash title="lib/layout.shared.tsx" import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; export function baseOptions(): BaseLayoutProps { return { githubUrl: 'https://github.com', }; } ``` ### Normal Menu [#normal-menu] A menu containing multiple link items. ```tsx title="lib/layout.shared.tsx" import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; export function baseOptions(): BaseLayoutProps { return { links: [ { type: 'menu', text: 'Guide', items: [ { text: 'Getting Started', description: 'Learn to use Fumadocs', url: '/docs', }, ], }, ], }; } ``` ### Navigation Menu [#navigation-menu] In Home Layout, you can add navigation menu (fully animated) to the navbar. ![Nav](/docs/nav-layout-menu.png) ```tsx title="app/(home)/layout.tsx" import { baseOptions } from '@/lib/layout.shared'; import type { ReactNode } from 'react'; import { HomeLayout } from 'fumadocs-ui/layouts/home'; import { NavbarMenu, NavbarMenuContent, NavbarMenuLink, NavbarMenuTrigger, } from 'fumadocs-ui/layouts/home/navbar'; export default function Layout({ children }: { children: ReactNode }) { return ( Documentation Hello World ), }, // other items ]} > {children} ); } ``` # Fumadocs UI (the default theme of Fumadocs): Navbar URL: /docs/ui/layouts/nav Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/layouts/nav.mdx Navbar/header configurations. ## Configurations [#configurations] Options for navbar (header). ### $Fumadocs | Prop | Type | Description | | ------------------ | ----------- | ------------------------------------------ | | `enabled?` | `union` | | | `title?` | `union` | | | `url?` | `string` | Redirect url of title Default: `'/'` | | `transparentMode?` | `union` | Use transparent background Default: `none` | | `component?` | `ReactNode` | **Deprecated.** | ### Transparent Mode [#transparent-mode] To make the navbar background transparent, you can configure transparent mode. ```tsx tab="All" title="lib/layout.shared.tsx" import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; export function baseOptions(): BaseLayoutProps { return { nav: { title: 'My App', // [!code ++] transparentMode: 'top', }, }; } ``` ```tsx tab="Home Layout" import { baseOptions } from '@/lib/layout.shared'; import { HomeLayout } from 'fumadocs-ui/layouts/home'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { const base = baseOptions(); return ( {children} ); } ``` ```tsx tab="Docs Layout" import { baseOptions } from '@/lib/layout.shared'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { const base = baseOptions(); return ( {children} ); } ``` | Mode | Description | | -------- | ---------------------------------------- | | `always` | Always use a transparent background | | `top` | When at the top of page | | `none` | Disable transparent background (default) | ### Replace Navbar [#replace-navbar] To replace the navbar in different layouts, set `nav.component` to your own component. ```tsx tab="All" title="lib/layout.shared.tsx" import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; export function baseOptions(): BaseLayoutProps { return { nav: { // [!code ++] component: , }, }; } ``` ```tsx tab="Home Layout" import { baseOptions } from '@/lib/layout.shared'; import { HomeLayout } from 'fumadocs-ui/layouts/home'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { const base = baseOptions(); return ( , }} > {children} ); } ``` ```tsx tab="Docs Layout" import { baseOptions } from '@/lib/layout.shared'; import { DocsLayout } from 'fumadocs-ui/layouts/docs'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { const base = baseOptions(); return ( , }} > {children} ); } ``` Fumadocs uses **CSS Variables** to share the size of layout components, and fit each layout component into appropriate position. You need to override `--fd-nav-height` to the exact height of your custom navbar, this can be done with a CSS stylesheet (e.g. in `global.css`): ```css :root { --fd-nav-height: 80px !important; } ``` # Fumadocs UI (the default theme of Fumadocs): Notebook Layout URL: /docs/ui/layouts/notebook Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/layouts/notebook.mdx A compact version of Docs Layout A compact version of [``](/docs/ui/layouts/docs). ![Notebook](/docs/notebook.png) ## Usage [#usage] Enable the notebook layout with `fumadocs-ui/layouts/notebook`. ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/notebook'; // [!code highlight] import { baseOptions } from '@/lib/layout.shared'; import { source } from '@/lib/source'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` Make sure to update your page import too: ```tsx title="page.tsx" import { ... } from 'fumadocs-ui/layouts/docs/page'; // [!code --] import { ... } from 'fumadocs-ui/layouts/notebook/page'; // [!code ++] ``` ## Configurations [#configurations] The options are inherited from [Docs Layout](/docs/ui/layouts/docs), with minor differences: - sidebar/navbar cannot be replaced, Notebook layout is more opinionated than the default one. - additional options (see below). ### Tab Mode [#tab-mode] Configure the style of [Layout Tabs](/docs/ui/layouts/docs#layout-tabs), see the linked docs for how to add layout tabs. ![Notebook](/docs/notebook-tab-mode.png) ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/notebook'; import { baseOptions } from '@/lib/layout.shared'; import { source } from '@/lib/source'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` ### Nav Mode [#nav-mode] Configure the style of navbar. Below is an example using [tab mode](#tab-mode) `navbar` & nav mode `top`: ![Notebook](/docs/notebook-nav-mode.png) ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/notebook'; import { baseOptions } from '@/lib/layout.shared'; import { source } from '@/lib/source'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { const { nav, ...base } = baseOptions(); return ( {children} ); } ``` ### AI Chat [#ai-chat] Same as [Docs Layout](/docs/ui/layouts/docs#ai-chat). # Fumadocs UI (the default theme of Fumadocs): Docs Page URL: /docs/ui/layouts/page Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/layouts/page.mdx A page in your documentation Page is the base element of a documentation, it includes Table of contents, Footer, and Breadcrumb. ## Usage [#usage] Import it according to your layout. ```tsx title="page.tsx" tab="Docs Layout" import { DocsPage, DocsDescription, DocsTitle, DocsBody } from 'fumadocs-ui/layouts/docs/page'; title description

This heading looks good!

It applies the Typography styles, wrap your content here.
; ``` ```tsx title="page.tsx" tab="Notebook Layout" import { DocsPage, DocsDescription, DocsTitle, DocsBody } from 'fumadocs-ui/layouts/notebook/page'; title description

This heading looks good!

It applies the Typography styles, wrap your content here.
; ``` Instead of rendering the title with `DocsTitle` in `page.tsx`, you can put the title into MDX file. This will render the title in the MDX body. ### Page Actions [#page-actions] Show GitHub link, and shortcut links for AIs. ```tsx import { DocsPage, ViewOptionsPopover } from 'fumadocs-ui/layouts//page'; const githubUrl = `https://github.com/fuma-nama/fumadocs/blob/main/content/docs/${page.path}` ; ``` ### Last Updated Time [#last-updated-time] Display last updated time of the page. ```tsx import { DocsPage, PageLastUpdate } from 'fumadocs-ui/layouts//page'; const lastModifiedTime: Date | undefined; {/* Other content */} {lastModifiedTime && } ; ``` For `lastModifiedTime`, you can possibly use different version controls like Github or a CMS. You can enable [`lastModified`](/docs/mdx/last-modified). ```tsx import { source } from '@/lib/source'; const page = source.getPage(['...']); const lastModifiedTime = page.data.lastModified; ``` For Github hosted documents, you can use the [`getGithubLastEdit`](/docs/headless/utils/git-last-edit) utility. ```tsx import { getGithubLastEdit } from 'fumadocs-core/content/github'; const lastModifiedTime = await getGithubLastEdit({ owner: 'fuma-nama', repo: 'fumadocs', // file path in Git path: `content/docs/${page.path}`, }); ``` ## Configurations [#configurations] ### Full Mode [#full-mode] To extend the page to fill up all available space, pass `full` to the page component. This will force TOC to be shown as a popover. ```tsx import { DocsPage } from 'fumadocs-ui/layouts//page'; ...; ``` ### Table of Contents [#table-of-contents] An overview of all the headings in your article, it requires an array of headings. For Markdown and MDX documents, You can obtain it using the [TOC Utility](/docs/headless/utils/get-toc). Content sources like Fumadocs MDX offer this out-of-the-box. ```tsx import { DocsPage } from 'fumadocs-ui/layouts//page'; ...; ``` You can customize it with `tableOfContent`, or with `tableOfContentPopover` on smaller devices. ```tsx title="page.tsx" import { DocsPage } from 'fumadocs-ui/layouts//page'; return ( ... ); ``` ### $Fumadocs | Prop | Type | Description | | ------------ | ----------------------------------- | ---------------------------------------------------- | | `container?` | `DetailedHTMLProps` | | | `header?` | `ReactNode` | Custom content in TOC container, before the main TOC | | `footer?` | `ReactNode` | Custom content in TOC container, after the main TOC | | `style?` | `union` | | | `list?` | `union` | | #### Style [#style] Choose another style for the TOC: A track line with a thumb on active headings (default). A track line following heading depth, inspired by [Clerk](https://clerk.com). No track line, a block slides behind active headings. ```tsx import { DocsPage } from 'fumadocs-ui/layouts//page'; ... ; ``` #### Replace TOC [#replace-toc] To replace the component: ```tsx tab="page.tsx" import { TOCProvider, TOC, TOCPopover } from './my-toc'; return ( ... ); ``` ```tsx tab="my-toc.tsx" 'use client'; import type { TOCProps, TOCPopoverProps, TOCProviderProps, } from 'fumadocs-ui/layouts//page/slots/toc'; export function TOCProvider({ toc, children }: TOCProviderProps) { // receive TOC items, you can pass it down via React contexts console.log(toc); return children; } export function TOC(props: TOCProps) { return
Hello World
; } export function TOCPopover(props: TOCPopoverProps) { return
Popover
; } ``` You can start from the default implementation with Fumadocs CLI: ```bash tab="Docs Layout" npx @fumadocs/cli add slots/docs/page/toc ``` ```bash tab="Notebook Layout" npx @fumadocs/cli add slots/notebook/page/toc ``` ```bash tab="Flux Layout" npx @fumadocs/cli add slots/flux/page/toc ``` ### Footer [#footer] Footer is a navigation element that has two buttons to jump to the next and previous pages. When not specified, it shows the neighbour pages found from page tree. Customize the footer with the `footer` prop: ```tsx import { DocsPage } from 'fumadocs-ui/layouts//page'; ...; ``` ### $Fumadocs | Prop | Type | Description | | -------- | -------- | ---------------------------------------------------------- | | `items?` | `object` | Items including information for the next and previous page | #### Replace Footer [#replace-footer] To replace the component: ```tsx tab="page.tsx" import { DocsPage } from 'fumadocs-ui/layouts//page'; import { Footer } from './my-footer'; ; ``` ```tsx tab="my-footer.tsx" 'use client'; import type { FooterProps } from 'fumadocs-ui/layouts//page/slots/footer'; export function Footer(props: FooterProps) { return
Hello World
; } ``` You can start from the default implementation with Fumadocs CLI: ```bash tab="Docs Layout" npx @fumadocs/cli add slots/docs/page/footer ``` ```bash tab="Notebook Layout" npx @fumadocs/cli add slots/notebook/page/footer ``` ```bash tab="Flux Layout" npx @fumadocs/cli add slots/flux/page/footer ``` ### Breadcrumb [#breadcrumb] A navigation element, shown only when user is navigating in folders. Customize the breadcrumb with the `breadcrumb` option: ```tsx import { DocsPage } from 'fumadocs-ui/layouts//page'; ...; ``` ### $Fumadocs | Prop | Type | Description | | ------------------- | ------- | ------------------------------------------------------------------------ | | `includeRoot?` | `union` | Include the root folders in the breadcrumb items array. Default: `false` | | `includePage?` | `union` | Include the page itself in the breadcrumb items array Default: `false` | | `includeSeparator?` | `union` | Count separator as an item Default: `false` | #### Replace Breadcrumb [#replace-breadcrumb] To replace the component: ```tsx tab="page.tsx" import { DocsPage } from 'fumadocs-ui/layouts//page'; import { Breadcrumb } from './my-breadcrumb'; ; ``` ```tsx tab="my-breadcrumb.tsx" 'use client'; import type { BreadcrumbProps } from 'fumadocs-ui/layouts//page/slots/breadcrumb'; export function Breadcrumb(props: BreadcrumbProps) { return
Hello World
; } ``` You can start from the default implementation with Fumadocs CLI: ```bash tab="Docs Layout" npx @fumadocs/cli add slots/docs/page/breadcrumb ``` ```bash tab="Notebook Layout" npx @fumadocs/cli add slots/notebook/page/breadcrumb ``` ```bash tab="Flux Layout" npx @fumadocs/cli add slots/flux/page/breadcrumb ``` ### Container [#container] Container controls the article wrapper around breadcrumb, body, and footer. To replace the page container: ```tsx tab="page.tsx" import { DocsPage } from 'fumadocs-ui/layouts//page'; import { Container } from './my-container'; ; ``` ```tsx tab="my-container.tsx" 'use client'; export function Container(props: React.ComponentProps<'article'>) { return
; } ``` You can start from the default implementation with Fumadocs CLI: ```bash tab="Docs Layout" npx @fumadocs/cli add slots/docs/page/container ``` ```bash tab="Notebook Layout" npx @fumadocs/cli add slots/notebook/page/container ``` ```bash tab="Flux Layout" npx @fumadocs/cli add slots/flux/page/container ``` # Fumadocs UI (the default theme of Fumadocs): Root Provider URL: /docs/ui/layouts/root-provider Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/layouts/root-provider.mdx The context provider of Fumadocs UI. The context provider of all the components, including `next-themes` and [``](/docs/headless#installation). It should be located at the root layout. ## Usage [#usage] Import it according to your React.js framework: ```jsx tab="Next.js" import { RootProvider } from 'fumadocs-ui/provider/next'; export default function Layout({ children }) { return ( {children} ); } ``` ```jsx tab="React Router" import { RootProvider } from 'fumadocs-ui/provider/react-router'; export function Layout({ children }) { return ( {children} ); } ``` ```jsx tab="Tanstack" import { HeadContent, Scripts, } from '@tanstack/react-router'; import { RootProvider } from 'fumadocs-ui/provider/tanstack'; function RootDocument({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ```jsx tab="Waku" import { RootProvider } from 'fumadocs-ui/provider/waku'; export default function Layout({ children }) { return ( {children} ); } ``` ```jsx tab="Astro" import { navigate } from 'astro:transitions/client'; import { RootProvider } from 'fumadocs-ui/provider/astro'; export function DocsIsland({ children, pathname, params }) { return ( {children} ); } ``` ### Search Dialog [#search-dialog] Customize or disable the search dialog with `search` option. ```jsx {children} ``` Learn more from [Search](/docs/ui/search). ### Theme Provider [#theme-provider] Fumadocs supports light/dark modes with [`next-themes`](https://github.com/pacocoursey/next-themes). Customize or disable it with `theme` option. ```jsx {children} ``` You can also install & reference `next-themes` in your code for managing themes. #### Hotkey [#hotkey] Pressing D toggles between light and dark mode. It is ignored while you are typing in an editable element (such as ``), or when a dialog like the search dialog is opened. Pass another key to `theme.hotKey`, or `false` to disable it. ```jsx {children} ``` A function is also accepted for full control over the matched key combination. ```jsx e.shiftKey && e.key === 'D', }} > {children} ``` # Fumadocs UI (the default theme of Fumadocs): Spacious Layout URL: /docs/ui/layouts/spacious Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/layouts/spacious.mdx A less compact version of Docs Layout A less compact version of [``](/docs/ui/layouts/docs), the page sits in an inset panel beside the sidebar, with page-level actions at the top of the panel. ![Spacious](./spacious.png) Spacious layout is only available in the Base UI version of Fumadocs UI (`@fumadocs/base-ui`). ## Usage [#usage] Enable the Spacious layout with `fumadocs-ui/layouts/spacious`. ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/spacious'; // [!code highlight] import { baseOptions } from '@/lib/layout.shared'; import { source } from '@/lib/source'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` Make sure to update your page import too: ```tsx title="page.tsx" import { ... } from 'fumadocs-ui/layouts/docs/page'; // [!code --] import { ... } from 'fumadocs-ui/layouts/spacious/page'; // [!code ++] ``` ### Styles [#styles] Like Glass layout, Spacious layout styles are **not** bundled into the CSS preset, so you don't pay for them unless you use the layout. Import them in your Tailwind CSS file: ```css title="Tailwind CSS" @import 'tailwindcss'; /* [!code ++] */ @import 'fumadocs-ui/css/generated/spacious.css'; @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; ``` - Spacious layout is a **client component**, you cannot pass unserializable props from a server component. - On desktop, the page scrolls inside the panel instead of the window. ## Configurations [#configurations] The options are inherited from [Docs Layout](/docs/ui/layouts/docs), with minor differences: - the `sidebar` option only accepts `defaultOpenLevel` and `prefetch`, the sidebar is always collapsible. - additional options (see below). ### Layout Tabs [#layout-tabs] Configure [Layout Tabs](/docs/ui/layouts/docs#layout-tabs) with the `tabs` prop, see the linked docs for how to add them. Tabs are shown as a dropdown at the top of sidebar. ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/spacious'; import { baseOptions } from '@/lib/layout.shared'; import { source } from '@/lib/source'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` ### AI Chat [#ai-chat] Pass an `aiChat` object with the open state and a change handler to show **Ask AI** in the layout. With the `panel` option, the layout renders your chat panel for you: it's docked beside the page on wide screens (the table of contents moves into the page header), and floats over the page on smaller screens. ```tsx title="layout.tsx" 'use client'; import { DocsLayout } from 'fumadocs-ui/layouts/spacious'; import { useState } from 'react'; export function Layout({ children }) { const [open, setOpen] = useState(false); return ( , }} // ... > {children} ); } ``` ### Slots [#slots] Spacious layout exposes a `slots` option to override its parts, in addition to the shared slots (`navTitle`, `themeSwitch`, `searchTrigger`, `languageSelect`). | Slot | Description | | --------- | ---------------------------------------------------------------------------------- | | `header` | The navbar on mobile. | | `sidebar` | The sidebar system: `provider`, `main` (desktop) and `drawer` (mobile). | | `actions` | The actions at the top right of page, like the language switcher and options menu. | ```tsx title="layout.tsx" import { DocsLayout } from 'fumadocs-ui/layouts/spacious'; ; ``` ## Docs Page [#docs-page] The page component is similar to [Docs Page](/docs/ui/layouts/page), with a few differences: - The table of contents uses the `block` style by default. - `tableOfContentPopover` configures the table of contents dropdown in page header, and the table of contents bar on mobile. - `slots.toc` accepts a `dropdown` component for the dropdown in page header. - There is no `container` slot. # Fumadocs UI (the default theme of Fumadocs): Search UI URL: /docs/ui/search Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/search.mdx The UI for document search ## Overview [#overview] You can customize Search UI from [``](/docs/ui/layouts/root-provider). ```tsx title="app/layout.tsx" import { RootProvider } from 'fumadocs-ui/provider/'; import type { ReactNode } from 'react'; import { SearchDialog } from '@/components/my-search-dialog'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` ### Hot Keys [#hot-keys] Customize the hot keys to trigger search dialog, by default it's ⌘ K or Ctrl K. ```tsx import { RootProvider } from 'fumadocs-ui/provider/'; {children} ; ``` ### References [#references] A full list of options. ### SearchProps | Prop | Type | Description | | --------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `preload?` | `union` | **Deprecated.** Preload search dialog before opening it Default: ``true`` | | `links?` | `array` | Custom links to be displayed if search is empty | | `hotKey?` | `array` | Hotkeys for triggering search dialog Default: `Meta/Ctrl + K` | | `SearchDialog?` | `union` | Replace default search dialog, allowing you to use other solutions such as Algolia Search It receives the `open` and `onOpenChange` prop, can be lazy loaded with `React.lazy()` | | `options?` | `Partial` | Additional props to the dialog | | `enabled?` | `union` | Enable search functionality Default: ``true`` | ## Custom UI [#custom-ui] Fumadocs UI also exposes a lower level `` component for advanced use, you can create your own search dialog to replace the default one. ```tsx twoslash 'use client'; import React from 'react'; import { useFetchSearch } from 'fumadocs-core/search/client'; import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogHeader, SearchDialogFooter, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, type SharedProps, } from 'fumadocs-ui/components/dialog/search'; import { useI18n } from 'fumadocs-ui/contexts/i18n'; export default function DefaultSearchDialog(props: SharedProps) { const { locale } = useI18n(); // (optional) for i18n const search = useFetchSearch({ locale, }); return ( {/* footer items */} ); } ``` ### Content Renderer [#content-renderer] To render the Markdown content of search items on your own instead, pass `renderMarkdown`. ```tsx title="components/search.tsx" ( { // ... }} /> )} /> ``` ### Tables [#tables] Customize how tables in results are rendered. A custom `Table` renders the rows on its own: ```tsx title="components/search.tsx" table.rows.map((row) => ( {Item({ item: row, onClick: () => onSelect(row) })} )) } /> ``` ### Highlights [#highlights] Matches of the search query are highlighted with the [CSS Custom Highlight API](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API), style them with `::highlight(fd-search)`. ```css title="global.css" ::highlight(fd-search) { color: var(--color-fd-primary-foreground); background-color: var(--color-fd-primary); } ``` `` highlights the query of `data` from ``, or the search input without it. When rendering results on your own, highlight them with `useHighlightQuery()`. ```tsx title="components/search-result.tsx" 'use client'; import { useHighlightQuery } from 'fumadocs-core/search/client'; import type { ReactNode } from 'react'; export function SearchResult({ query, children }: { query: string; children: ReactNode }) { const ref = useHighlightQuery(query); return
{children}
; } ``` Text inside elements with `data-highlight-ignore` is not highlighted, like the badges of components. # Fumadocs UI (the default theme of Fumadocs): Themes URL: /docs/ui/theme Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/theme.mdx Add Theme to Fumadocs UI ## Overview [#overview] Fumadocs UI adds its own colors, animations, and utilities with Tailwind CSS preset. ### Setup [#setup] Only Tailwind CSS v4 is supported, the preset will also include source to Fumadocs UI itself: ```css title="Tailwind CSS" @import 'tailwindcss'; @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; ``` For **Shadcn UI**, you can use the `shadcn` preset instead. Fumadocs UI will adopt colors from your Shadcn UI theme. ```css @import 'tailwindcss'; @import 'fumadocs-ui/css/shadcn.css'; @import 'fumadocs-ui/css/preset.css'; ``` See [Colors](#colors) for details. ### Preflight Changes [#preflight-changes] By using the Tailwind CSS plugin, or the pre-built stylesheet, your default border, text and background colors will be changed. ### Light/Dark Modes [#lightdark-modes] Fumadocs supports light/dark modes with [`next-themes`](https://github.com/pacocoursey/next-themes), it is included in Root Provider. See [Root Provider](/docs/ui/layouts/root-provider#theme-provider) to learn more. ### RTL Layout [#rtl-layout] RTL (Right-to-left) layout is supported. To enable RTL, set the `dir` prop to `rtl` in body and root provider. ```tsx import { RootProvider } from 'fumadocs-ui/provider/'; import type { ReactNode } from 'react'; export default function RootLayout({ children }: { children: ReactNode }) { return ( {children} ); } ``` ### Layout Width [#layout-width] Customize the max width of docs layout with CSS Variables. ```css :root { --fd-layout-width: 1400px; } ``` ### Colors [#colors] It comes with many presets out-of-the-box, you can pick one you prefer. ```css @import 'fumadocs-ui/css/.css'; @import 'fumadocs-ui/css/preset.css'; ``` ![Neutral](/themes/neutral.png) ![Black](/themes/black.png) ![Vitepress](/themes/vitepress.png) ![Dusk](/themes/dusk.png) ![Catppuccin](/themes/catppuccin.png) ![Ocean](/themes/ocean.png) ![Purple](/themes/purple.png) ![Solar](/themes/solar.png) ![Emerald](/themes/emerald.png) ![Ruby](/themes/ruby.png) ![Aspen](/themes/aspen.png) The design system was inspired by [Shadcn UI](https://ui.shadcn.com), you can also customize the colors using [CSS/Theme variables](https://tailwindcss.com/docs/theme). ```css title="global.css" @theme { --color-fd-background: hsl(0, 0%, 96%); --color-fd-foreground: hsl(0, 0%, 3.9%); --color-fd-muted: hsl(0, 0%, 96.1%); --color-fd-muted-foreground: hsl(0, 0%, 45.1%); --color-fd-popover: hsl(0, 0%, 98%); --color-fd-popover-foreground: hsl(0, 0%, 15.1%); --color-fd-card: hsl(0, 0%, 94.7%); --color-fd-card-foreground: hsl(0, 0%, 3.9%); --color-fd-border: hsla(0, 0%, 80%, 50%); --color-fd-primary: hsl(0, 0%, 9%); --color-fd-primary-foreground: hsl(0, 0%, 98%); --color-fd-secondary: hsl(0, 0%, 93.1%); --color-fd-secondary-foreground: hsl(0, 0%, 9%); --color-fd-accent: hsla(0, 0%, 82%, 50%); --color-fd-accent-foreground: hsl(0, 0%, 9%); --color-fd-ring: hsl(0, 0%, 63.9%); } .dark { --color-fd-background: hsl(0, 0%, 7.04%); --color-fd-foreground: hsl(0, 0%, 92%); --color-fd-muted: hsl(0, 0%, 12.9%); --color-fd-muted-foreground: hsla(0, 0%, 70%, 0.8); --color-fd-popover: hsl(0, 0%, 11.6%); --color-fd-popover-foreground: hsl(0, 0%, 86.9%); --color-fd-card: hsl(0, 0%, 9.8%); --color-fd-card-foreground: hsl(0, 0%, 98%); --color-fd-border: hsla(0, 0%, 40%, 20%); --color-fd-primary: hsl(0, 0%, 98%); --color-fd-primary-foreground: hsl(0, 0%, 9%); --color-fd-secondary: hsl(0, 0%, 12.9%); --color-fd-secondary-foreground: hsl(0, 0%, 92%); --color-fd-accent: hsla(0, 0%, 40.9%, 30%); --color-fd-accent-foreground: hsl(0, 0%, 90%); --color-fd-ring: hsl(0, 0%, 54.9%); } ``` ### Typography [#typography] We have a built-in plugin forked from [Tailwind CSS Typography](https://tailwindcss.com/docs/typography-plugin). The plugin adds a `prose` class and variants to customize it. ```tsx

Good Heading

``` Use `prose-sm` alongside `prose` for smaller typography with an optically adjusted font weight: ```tsx
...
``` > The plugin is supposed to work with Fumadocs UI's MDX components, it may conflict with `@tailwindcss/typography`. > > If you need to use `@tailwindcss/typography` over the default plugin, [set a `className` option](https://github.com/tailwindlabs/tailwindcss-typography/blob/main/README.md#changing-the-default-class-name) to avoid conflicts. > > ```sass > @import 'tailwindcss'; > @plugin "@tailwindcss/typography" { > className: wysiwyg; > } > ``` # Fumadocs UI (the default theme of Fumadocs): Translations URL: /docs/ui/translations Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/ui/translations.mdx Adding Translations to UI ## Overview [#overview] All official Fumadocs packages use English by default, you can add translations for other languages. > This page mainly covers details for adding UI translations, you may be interested in the full [Internationalization](/docs/internationalization) guide instead. ### Singular [#singular] **Singular** refers to adding translations for only one language: ```ts title="lib/layout.shared.tsx" import { defineTranslations } from 'fumadocs-core/i18n'; import { uiTranslations } from 'fumadocs-ui/i18n'; export const translations = defineTranslations().extend(uiTranslations()).add({ // [label]: [translation] 'Search(search trigger)': '搜尋文檔', }); ``` > `.add()` should only be called **after** all `.extend()` calls. Pass the translations to ``. ```tsx import { RootProvider } from 'fumadocs-ui/provider/'; import { i18nProvider } from 'fumadocs-ui/i18n'; import { translations } from '@/lib/layout.shared'; export function Layout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ### Multilingual [#multilingual] Define translations for multiple languages, this requires a standard [i18n](/docs/internationalization) setup. ```ts title="lib/layout.shared.tsx" import { defineTranslations } from 'fumadocs-core/i18n'; import { uiTranslations } from 'fumadocs-ui/i18n'; import { i18n } from '@/lib/i18n'; export const translations = i18n .translations() .extend(uiTranslations()) .add({ // [locale code]: [translations] cn: { // [label]: [translation] 'Search(search trigger)': '搜尋文檔', }, }); ``` Pass the translations to ``. ```tsx import { RootProvider } from 'fumadocs-ui/provider/'; import { i18nProvider } from 'fumadocs-ui/i18n'; import { translations } from '@/lib/layout.shared'; export function Layout({ locale, children }: { locale: string; children: React.ReactNode }) { return ( {children} ); } ``` ## Language Packs [#language-packs] Language packs provide translations for `fumadocs-ui` and several other integrations like Fumadocs OpenAPI. ```npm npm i @fumadocs/language ``` > If your desired language is missing in the official language packs, we welcome contributions to [our repository](https://github.com/fuma-nama/fumadocs). Use `preset()` to consume language packs: ```ts tab="Singular" import { defineTranslations } from 'fumadocs-core/i18n'; import { openapiTranslations } from 'fumadocs-openapi/i18n'; import { uiTranslations } from 'fumadocs-ui/i18n'; import { zhTW } from '@fumadocs/language/zh-tw'; export const translations = defineTranslations() .extend(uiTranslations()) // add extensions according to the integrations you configured, e.g. Fumadocs OpenAPI: .extend(openapiTranslations()) // Traditional Chinese .preset(zhTW()); ``` ```ts tab="Multilingual" import { defineTranslations } from 'fumadocs-core/i18n'; import { openapiTranslations } from 'fumadocs-openapi/i18n'; import { uiTranslations } from 'fumadocs-ui/i18n'; import { zhTW } from '@fumadocs/language/zh-tw'; import { i18n } from '@/lib/i18n'; export const translations = i18n .translations() .extend(uiTranslations()) // add extensions according to the integrations you configured, e.g. Fumadocs OpenAPI: .extend(openapiTranslations()) // add Traditional Chinese translations to the "cn" locale code .preset('cn', zhTW()); ``` ### Custom Packs [#custom-packs] You can also create & publish your own language packs. For example, the structure of `zh-TW` language pack looks like: ```ts twoslash import type { TranslationPreset } from 'fumadocs-core/i18n'; import * as OpenAPI from 'fumadocs-openapi/i18n'; import * as UI from 'fumadocs-ui/i18n'; import * as Story from '@fumadocs/story/i18n'; const translations = { displayName: '繁體中文', 'Server URL(playground server select)': '伺服器 URL', 'No Variant(story controls)': '沒有變體', // ... } satisfies Partial; export function zhTW(): TranslationPreset { return { name: 'zh-TW', value: translations, }; } ``` ## Extensions [#extensions] Extensions declare the available labels for translations, only registered labels will be sent to client payload. ### Custom Extension [#custom-extension] If you are building community packages for Fumadocs, you can create extensions to register your own translations. To begin, install the translations toolkit Fumadocs use: ```npm npm i @fuma-translate/react ``` ```ts tab="i18n.ts" twoslash import type { TranslationExtension } from 'fumadocs-core/i18n'; const keys = [ // key format: "your label(additional context)" 'Hello {user}(welcome screen)', ] as const; export function myTranslations(): TranslationExtension<(typeof keys)[number]> { return { keys }; } ``` ```tsx tab="Reference" 'use client'; import { useTranslations } from '@fuma-translate/react'; export function Comp() { const t = useTranslations(); return (
{t('Hello {user}', { note: 'welcome screen', variables: { user: '...', }, })}
); } ``` > While skipped for simplicity, you can see the docs of [Fuma Translate](https://fuma-translate.vercel.app) to learn how to compile translation keys & types, rather than hardcoding it. Consumers can use your extension like: ```ts import { defineTranslations } from 'fumadocs-core/i18n'; import { myTranslations } from 'my-package/i18n'; export const translations = defineTranslations() // [!code ++] .extend(myTranslations()) .add({ 'Hello {user}(welcome screen)': '你好 {user}', }); ```