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()],
});

Integrate with Fumadocs

To integrate with Fumadocs, make a lib/source.ts file:

app/lib/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',
});

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

Done

The configuration is now finished.

Examples

Rendering Content

As React Router doesn't support RSC at the moment, use toClientRenderer() to lazy load MDX content as a component on browser.

For example:

import type { Route } from './+types/page';
import { source } from '@/lib/source';
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,
  };
}

const renderer = toClientRenderer(docs.doc, ({ default: Mdx, frontmatter }) => {
  return (
    <div className="prose">
      <h1>{frontmatter.title}</h1>
      <Mdx />
    </div>
  );
});

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

  return <Content />;
}

Note that you can import the source.generated.ts file directly, it's useful to access compiled content without loader().

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

How is this guide?

Last updated on