Fumadocs

Without RSC

Setup guide for non-RSC environments.

Setup

Install the required packages.

npm i fumadocs-openapi shiki

shiki must be installed, otherwise Vite will have bundling problem with Shiki's WASM regex engine.

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 & <ClientAPIPage /> 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()],
    // ...
  },
);

Update References to source

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).

But it will change the type of your pages, explicit handling of OpenAPI pages is necessary.

import { source } from '@/lib/source';

const page = source.getPage(['...']);

if (page.data.type === 'openapi') {
  // page data for generated OpenAPI pages
  console.log(page.data);
} else {
  // original flow...
}

Update all references to the pages of source, for example:

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

/**
 * return page content for LLMs
 */
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...
}

Ensure the migration is complete!

Run a type check to verify before continuing, e.g.

npm run types:check

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 = clientLoader.useContent(page.path, page);
  }

  return (
    <DocsLayout tree={page.pageTree}>
      <Suspense>{content}</Suspense>
    </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