Fumadocs
Vite

Tanstack Start

Use Fumadocs MDX with Tanstack Start & 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 react from '@vitejs/plugin-react';
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import { defineConfig } from 'vite';
import tsConfigPaths from 'vite-tsconfig-paths';
import tailwindcss from '@tailwindcss/vite';
import mdx from 'fumadocs-mdx/vite';

export default defineConfig({
  server: {
    port: 3000,
  },
  plugins: [
    mdx(await import('./source.config')),
    tailwindcss(),
    tsConfigPaths({
      projects: ['./tsconfig.json'],
    }),
    tanstackStart({
      customViteReactPlugin: true,
      prerender: {
        enabled: true,
      },
    }),
    react(),
  ],
});

Integrate with Fumadocs

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

src/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 Tanstack Start doesn't support RSC at the moment, use createClientLoader() to lazy load MDX content as a component on browser.

For example:

src/routes/docs/$.tsx
import { createFileRoute, notFound } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import { source } from '@/lib/source';
import { docs } from '../../../source.generated';
import { createClientLoader } from 'fumadocs-mdx/runtime/vite';

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 = createClientLoader(docs.doc, {
  id: 'docs',
  component({ frontmatter, default: MDX }) {
    return (
      <div className="prose">
        <h1>{frontmatter.title}</h1>
        <MDX />
      </div>
    );
  },
});

function Page() {
  const data = Route.useLoaderData();
  const Content = clientLoader.getComponent(data.path);

  return <Content />;
}

As you see, the source.generated.ts file can be imported directly:

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

How is this guide?

Last updated on