Fumadocs

Without RSC

Setup

Install the required packages.

npm i fumadocs-openapi shiki

Generate Styles

Add the following line:

Tailwind CSS
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import 'fumadocs-openapi/css/preset.css';

Configure Plugin

Create the OpenAPI server instance & <APIPage /> component.

import { createOpenAPI } from 'fumadocs-openapi/server';

export const openapi = createOpenAPI({
  // the OpenAPI schema, you can also give it an external URL.
  input: ['./openapi.json'],
});

See createOpenAPI() & <APIPage /> for available options.

Generate Pages

You can generate pages dynamically by integrating into Loader API.

lib/source.ts
import { loader, multiple } from 'fumadocs-core/source';
import { openapiPlugin, openapiSource } from 'fumadocs-openapi/server';
import { docs } from 'collections/server';
import { openapi } from '@/lib/openapi';

export const source = loader(
  multiple({
    docs: docs.toFumadocsSource(),
    openapi: await openapiSource(openapi, {
      baseDir: 'openapi',
    }),
  }),
  {
    baseUrl: '/docs',
    plugins: [openapiPlugin()],
    // ...
  },
);

openapiSource() is a server-side API that generates pages directly to your loader(), hence allowing dynamic generation (e.g. different page tree as schema changes).

It shares a different type from your original source, explicit handling of OpenAPI pages is necessary (e.g. where you return text for LLM).

import { source } from '@/lib/source';
import type { InferPageType } from 'fumadocs-core/source';

export async function getLLMText(page: InferPageType<typeof source>) {
  if (page.data.type === 'openapi') {
    // e.g. return the stringified OpenAPI schema
    return JSON.stringify(page.data.getSchema().bundled, null, 2);
  }

  // your original flow below...
}

Render Page

Pass a client payload from server, then render the page using the <ClientAPIPage /> component you created above.

For example, in Tanstack Start:

routes/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 browserCollections from 'collections/browser';
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 { ClientAPIPage } 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
    if (data.type === 'docs') {
      await clientLoader.preload(data.path);
    }
    return data;
  },
});

const serverLoader = createServerFn({
  method: 'GET',
})
  .inputValidator((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
    if (page.data.type === 'openapi') {
      return {
        type: 'openapi',
        title: page.data.title,
        description: page.data.description,
        pageTree,
        props: await page.data.getClientAPIPageProps(),
      };
    }

    return {
      type: 'docs',
      path: page.path,
      markdownUrl: getPageMarkdownUrl(page).url,
      pageTree,
    };
  });

const clientLoader = browserCollections.docs.createClientLoader({
  component(pageData, props) {
    // ...
  },
});

function Page() {
  const page = useFumadocsLoader(Route.useLoaderData());
  let content: ReactNode;

  // render OpenAPI page content
  if (page.type === 'openapi') {
    content = (
      <DocsPage full>
        <DocsTitle>{page.title}</DocsTitle>
        <DocsDescription>{page.description}</DocsDescription>
        <DocsBody>
          {/* pass the payload data */}
          <ClientAPIPage {...page.props} />
        </DocsBody>
      </DocsPage>
    );
  } else {
    content = <Suspense>{clientLoader.useContent(page.path, page)}</Suspense>;
  }

  return <DocsLayout tree={page.pageTree}>{content}</DocsLayout>;
}

You can see the full Tanstack Start example.

After configurating Fumadocs OpenAPI, you should be able to view the generated API pages after starting your app.

How is this guide?

Last updated on

On this page