Fumadocs
Vite

React Router

Use Fumadocs MDX with React Router

Setup

npm i fumadocs-mdx fumadocs-core @types/mdx

Create the configuration file:

source.config.ts
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';

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

export default defineConfig();

Add the Vite plugin:

vite.config.ts
import { reactRouter } from '@react-router/dev/vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import mdx from 'fumadocs-mdx/vite';
import * as MdxConfig from './source.config';

export default defineConfig({
  plugins: [mdx(MdxConfig), tailwindcss(), reactRouter(), tsconfigPaths()],
});

A source.generated.ts file will be generated when you run development server or production build.

Accessing Content

You can import the source.generated.ts file directly.

import { docs } from './source.generated';
console.log(docs);

To integrate with Fumadocs, create a docs collection and use:

app/source.ts
import { loader } from 'fumadocs-core/source';
import { create, docs } from '../source.generated';

export const source = loader({
  source: await create.sourceAsync(docs.doc, docs.meta),
  baseUrl: '/docs',
});

Rendering Content

Rendering page content is different because React Router doesn't support RSC at the moment. Instead, use toClientRenderer() to lazy load MDX content as a component on browser.

For example:

app/docs/page.tsx
import type { Route } from './+types/page';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import {
  DocsBody,
  DocsDescription,
  DocsPage,
  DocsTitle,
} from 'fumadocs-ui/page';
import { source } from '@/source';
import { type PageTree } from 'fumadocs-core/server';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { docs } from '../../source.generated';
import { toClientRenderer } from 'fumadocs-mdx/runtime/vite';

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,
    tree: source.pageTree,
  };
}

const renderer = toClientRenderer(
  docs.doc,
  ({ toc, default: Mdx, frontmatter }) => {
    return (
      <DocsPage toc={toc}>
        <title>{frontmatter.title}</title>
        <meta name="description" content={frontmatter.description} />
        <DocsTitle>{frontmatter.title}</DocsTitle>
        <DocsDescription>{frontmatter.description}</DocsDescription>
        <DocsBody>
          <Mdx components={{ ...defaultMdxComponents }} />
        </DocsBody>
      </DocsPage>
    );
  },
);

export default function Page(props: Route.ComponentProps) {
  const { tree, path } = props.loaderData;
  const Content = renderer[path];

  return (
    <DocsLayout
      nav={{
        title: 'React Router',
      }}
      tree={tree as PageTree.Root}
    >
      <Content />
    </DocsLayout>
  );
}

How is this guide?

Last updated on