---
title: "OpenAPI Document Generation"
description: "OpenAPI Document Generation automates the transformation of OpenAPI specifications into structured, type-safe MDX documentation pages for OpenAPI Interactive UI. By combining document loading, bund..."
last_updated: "2026-10-12T07:53:59.986642+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/api-documentation/openapi-document-generation"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/openapi/src/utils/pages/builder.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/builder.ts)
- [packages/openapi/src/server/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/server/index.tsx)
- [packages/openapi/src/utils/pages/to-text.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-text.ts)
- [packages/openapi/src/utils/pages/preset-auto.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/preset-auto.ts)
- [packages/asyncapi/src/server/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/server/index.tsx)
- [packages/asyncapi/src/utils/pages/to-text.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/to-text.ts)
- [packages/asyncapi/src/ui/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/ui/index.tsx)
- [packages/openapi/src/ui/base.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/ui/base.tsx)
- [packages/asyncapi/src/utils/pages/preset-auto.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/preset-auto.ts)
- [packages/openapi/src/generate-file.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/generate-file.ts)
- [packages/asyncapi/src/generate-file.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/generate-file.ts)
- [packages/openapi/src/utils/pages/to-static-data.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-static-data.ts)
- [packages/python/src/convert.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/src/convert.ts)
- [packages/asyncapi/src/utils/pages/to-static-data.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/to-static-data.ts)
- [packages/mdx/src/runtime/dynamic.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/runtime/dynamic.ts)
- [packages/asyncapi/src/utils/pages/builder.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/builder.ts)
- [packages/preview/src/pages/...slugs.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx)
- [packages/mdx/src/runtime/server.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/runtime/server.ts)
- [packages/mdx/src/next/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/next/index.ts)
- [packages/content-collections/src/configuration.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content-collections/src/configuration.ts)
- [packages/sanity/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/index.ts)
- [packages/openapi/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/index.ts)
- [packages/doc-gen/src/remark-docgen.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/doc-gen/src/remark-docgen.ts)
- [packages/openapi/src/ui/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/ui/index.tsx)
- [packages/api-docs/src/components/schema/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/components/schema/index.tsx)
- [packages/mdx/src/plugins/index-file.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/plugins/index-file.ts)
- [packages/openapi/src/utils/document/load.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/document/load.ts)
- [packages/mdx/src/core.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/core.ts)
- [packages/asyncapi/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/index.ts)
- [packages/obsidian/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/index.ts)
</details>

## Overview

OpenAPI Document Generation automates the transformation of OpenAPI specifications into structured, type-safe MDX documentation pages for [OpenAPI Interactive UI](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/api-documentation/openapi-interactive-ui). By combining document loading, bundling, and schema upgrading through `@scalar/openapi-upgrader` and `@fumadocs/api-docs/schema/bundle`, the package processes input definitions into intermediate entry trees that organize paths, operations, tags, and webhooks.

Sources: [packages/openapi/src/utils/document/load.ts:1-19](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/document/load.ts#L1-L19), [packages/openapi/src/utils/pages/builder.ts:1-61](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/builder.ts#L1-L61)

It integrates with server loaders and file generation workflows to serialize static metadata, build table-of-contents trees, and output `.mdx` files alongside navigation manifests.

Sources: [packages/openapi/src/server/index.tsx:149-206](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/server/index.tsx#L149-L206), [packages/openapi/src/generate-file.ts:122-173](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/generate-file.ts#L122-L173)

## OpenAPI Specification Ingestion and Upgrading

### Document Loading and Upgrading Pipeline

The public API surface for OpenAPI document loading and upgrading processes raw specification inputs through bundling and normalization pipelines. Exported types expose specification versions (`OpenAPIV2`, `OpenAPIV3`, `OpenAPIV3_1`, `OpenAPIV3_2`, and the union `OpenAPI`) alongside media adapters and builder configurations.

```typescript
export * from './generate-file';
export type { MediaAdapter } from '@/requests/media/adapter';
export type { RenderContext } from './types';
export type {
  OperationOutput,
  OutputEntry,
  OutputGroup,
  PageOutput,
  PagesBuilder,
  PagesBuilderConfig,
  WebhookOutput,
  OperationItem,
  WebhookItem,
  GeneratedPageProps,
} from './utils/pages/builder';
export type { SchemaToPagesOptions } from './utils/pages/preset-auto';
export type { OpenAPIV3_2, OpenAPIV3_1, OpenAPIV3, OpenAPIV2, OpenAPI } from '@/types/openapi';
```
Sources: [packages/openapi/src/index.ts:1-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/index.ts#L1-L17)

The asynchronous `loadDocument` function accepts either a file path string or a pre-parsed document object. It resolves and bundles external references via `bundle` from `@fumadocs/api-docs/schema/bundle`, then normalizes the resulting structure to target version `3.2` using `upgrade` from `@scalar/openapi-upgrader`. Any resolution or upgrade failure catches the error and throws a descriptive `[OpenAPI] Failed to resolve input` error containing the original cause.

```typescript
export async function loadDocument(input: string | Document): Promise<{
  bundled: Document;
}> {
  try {
    let bundled = await bundle<Document>(input);
    bundled = upgrade(bundled, '3.2') as Document;
    return { bundled };
  } catch (e) {
    throw new Error(`[OpenAPI] Failed to resolve input: ${input}`, {
      cause: e,
    });
  }
}
```
Sources: [packages/openapi/src/utils/document/load.ts:8-20](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/document/load.ts#L8-L20)

> [!WARNING]
> If an OpenAPI specification contains circular references or unresolvable external `$ref` pointers, `bundle` throws during `loadDocument` execution, which aborts the loading process before the upgrade step can run.

Sources: [packages/openapi/src/utils/document/load.ts:11-19](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/document/load.ts#L11-L19)

## Document Parsing and Hierarchy Construction

### Intermediate Document Structures and Entry Types

The schema parsing engine decomposes bundled OpenAPI documents into structured intermediate outputs using the `fromSchema` function. It provides a `PagesBuilder` context that exposes helper methods like `extract`, `dereferenceShallow`, `fromExtractedOperation`, and `routePathToFilePath` to customize output generation.

```typescript
interface BaseEntry {
  path: string;
  schemaId: string;
  info: {
    title: string;
    description?: string;
    deprecated?: boolean;
  };
}

export interface OperationOutput extends BaseEntry {
  type: 'operation';
  item: OperationItem;
}

export interface WebhookOutput extends BaseEntry {
  type: 'webhook';
  item: WebhookItem;
}

export interface PageOutput extends BaseEntry {
  type: 'page';
  operations: OperationItem[];
  webhooks: WebhookItem[];
  /** tag info if the page is generated from a tag. */
  tag?: TagObject;
}

export interface OutputGroup extends BaseEntry {
  type: 'group';
  entries: OutputEntry[];
  /** tag info if the group is generated from a tag. */
  tag?: TagObject;
}

export interface WebhookItem {
  /**
   * webhook name in `webhooks`
   */
  name: string;
  method: HttpMethods;
}

export interface OperationItem {
  /**
   * the path of operation in `paths`
   */
  path: string;
  /**
   * the HTTP method of operation
   */
  method: HttpMethods;
}

export type OutputEntry = PageOutput | OperationOutput | WebhookOutput | OutputGroup;
```
Sources: [packages/openapi/src/utils/pages/builder.ts:7-61](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/builder.ts#L7-L61)

The parser iterates over `bundled.paths` and `bundled.webhooks` across all supported `methodKeys` to populate extracted operations and webhooks. The `routePathToFilePath` utility transforms route paths by converting characters to lowercase, replacing dots with hyphens, stripping surrounding curly braces from parameter segments, and joining segments with slashes.

```typescript
export function fromSchema(
  schemaId: string,
  bundled: Document,
  config: PagesBuilderConfig,
): OutputEntry[] {
  const files: OutputEntry[] = [];
  const { toPages } = config;

  toPages({
    id: schemaId,
    document: bundled,
    dereferenceShallow: (s) => dereferenceShallow(s, bundled),
    create(entry) {
      files.push(entry);
    },
    extract() {
      const result: ExtractedInfo = { webhooks: [], operations: [] };

      for (const [path, pathItem] of Object.entries(bundled.paths ?? {})) {
        if (!pathItem) continue;

        for (const methodKey of methodKeys) {
          if (!pathItem[methodKey]) continue;

          result.operations.push({
            method: methodKey,
            path,
            tags: pathItem[methodKey]?.tags,
          });
        }
      }

      for (const [name, _pathItem] of Object.entries(bundled.webhooks ?? {})) {
        const pathItem = dereferenceShallow(_pathItem, bundled);
        if (!pathItem) continue;

        for (const methodKey of methodKeys) {
          if (!pathItem[methodKey]) continue;

          result.webhooks.push({
            method: methodKey,
            name,
            tags: pathItem[methodKey]?.tags,
          });
        }
      }

      return result;
    },
    routePathToFilePath(path) {
      return path
        .toLowerCase()
        .replaceAll('.', '-')
        .split('/')
        .flatMap((v) => {
          if (v.startsWith('{') && v.endsWith('}')) return v.slice(1, -1);
          if (v.length === 0) return [];
          return v;
        })
        .join('/');
    },
```
Sources: [packages/openapi/src/utils/pages/builder.ts:121-181](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/builder.ts#L121-L181)

| Output Entry Type | Interface Name | Description |
| :--- | :--- | :--- |
| `operation` | `OperationOutput` | Represents a single API operation endpoint entry. |
| `webhook` | `WebhookOutput` | Represents an event-driven webhook endpoint entry. |
| `page` | `PageOutput` | Contains multiple operations and webhooks, with optional tag metadata. |
| `group` | `OutputGroup` | Holds a nested collection of child output entries and optional tag info. |

Sources: [packages/openapi/src/utils/pages/builder.ts:17-40](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/builder.ts#L17-L40)

> [!NOTE]
> When resolving display names for operations, `fromExtractedOperation` evaluates `operation.summary`, then `pathItem.summary`, and falls back to a title-cased `operation.operationId` or the raw path string if neither summary is present.

Sources: [packages/openapi/src/utils/pages/builder.ts:195-211](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/builder.ts#L195-L211)

### Execution Walkthrough and Sequence Diagram

The hierarchy construction and virtual file translation pipeline processes output entries through a multi-stage call chain. 

1. `createOpenAPI` initializes the server state and triggers schema loading.
2. `getVirtualFiles` calls `fromSchema` to generate the raw entry list and invokes `onEntries`.
3. `onEntries` recursively iterates through entry collections or builds `meta.json` navigation lists when meta generation is enabled.
4. `onEntry` receives individual non-group entries and invokes `getPageProps` for each.
5. `getPageProps` constructs the final page property payloads for documentation rendering.

```tsx
export function createOpenAPI(options: OpenAPIOptions = {}): OpenAPIServer {
  const { disableCache = false } = options;
  const schemaMap = new Map<string, Promise<LoadedDocument>>();

  let resolvedInput: SchemaRecord = {};
  if (Array.isArray(options.input)) {
    for (const item of options.input) resolvedInput[item] = item;
  } else if (options.input) {
    resolvedInput = options.input;
  }

  function getSchema(schemaId: string): Promise<LoadedDocument> {
    if (!(schemaId in resolvedInput)) {
      console.warn(
        `[Fumadocs OpenAPI] the document "${schemaId}" is not listed in the input array, this may be unexpected and won't be cached properly.`,
      );
      // do not cache unlisted documents
      return loadDocument(schemaId);
    }

    if (!disableCache) {
      const cached = schemaMap.get(schemaId);
      if (cached) return cached;
    }

    const raw = resolvedInput[schemaId];
    const output = Promise.resolve(typeof raw === 'function' ? raw() : raw).then(loadDocument);
    if (!disableCache) schemaMap.set(schemaId, output);
    return output;
  }

  async function getSchemas(): Promise<Record<string, LoadedDocument>> {
    const entries = await Promise.all(
      Object.keys(resolvedInput).map(async (k) => [k, await getSchema(k)]),
    );
    return Object.fromEntries(entries);
  }

  async function getVirtualFiles(server: OpenAPIServer, options: OpenAPISourceOptions) {
    const { baseDir = '', meta = false } = options;
    const { createAutoPreset } = await import('@/utils/pages/preset-auto');
    const { fromSchema } = await import('@/utils/pages/builder');
    const files: VirtualFile<{
      pageData: OpenAPIPageData;
      metaData: MetaData;
    }>[] = [];

    const schemas = await server.getSchemas();
    const builderOptions = createAutoPreset(options);

    for (const [id, schema] of Object.entries(schemas)) {
      const list = fromSchema(id, schema.bundled, builderOptions);

      onEntries(list);

      function onEntry(entry: PageOutput | OperationOutput | WebhookOutput) {
        const props = getPageProps(entry);

        files.push({
          type: 'page',
          path: `${baseDir}/${entry.path}`,
          data: {
            ...entry.info,
            getAPIPageProps() {
              return this.getOpenAPIPageProps();
            },
            getClientAPIPageProps() {
              return this.getOpenAPIPageProps();
            },
            getOpenAPIPageProps() {
              return {
                payload: {
                  bundled: schema.bundled,
                  proxyUrl: server.options.proxyUrl,
                },
                ...props,
              };
            },
            getSchema() {
              return {
```
Sources: [packages/openapi/src/server/index.tsx:111-206](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/server/index.tsx#L111-L206)

```mermaid
sequenceDiagram
    participant Server as createOpenAPI
    participant Files as getVirtualFiles
    participant Entries as onEntries
    participant Entry as onEntry
    participant Props as getPageProps

    Server->>Files: getVirtualFiles(server, options)
    Files->>Entries: onEntries(list)
    Entries->>Entry: onEntry(entry)
    Entry->>Props: getPageProps(entry)
```

Sources: [packages/openapi/src/server/index.tsx:111-206](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/server/index.tsx#L111-L206)

> [!CAUTION]
> If `meta` is configured with folder styles, `onEntries` computes relative paths using `path.relative` between parent and child entries; omitting valid parent relationships causes malformed `meta.json` page arrays.

Sources: [packages/openapi/src/server/index.tsx:221-252](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/server/index.tsx#L221-L252)

## Static Data and Table Extraction

### Slugged Metadata Generation

The `toStaticData` utility function builds [table of contents](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/table-of-contents) items (`TOCItemType[]`) and structured metadata (`StructuredData`) for generated documentation pages. It accepts a `GeneratedPageProps` page configuration and an OpenAPI `Document`, iterating through both configured operations and webhooks.

```typescript
export function toStaticData(
  page: GeneratedPageProps,
  doc: Document,
): {
  toc: TOCItemType[];
  structuredData: StructuredData;
} {
```
Sources: [packages/openapi/src/utils/pages/to-static-data.ts:9-15](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-static-data.ts#L9-L15)

For each operation or webhook entry, `toStaticData` resolves the operation object by shallow dereferencing path or webhook maps against the root document. If `page.showTitle` is enabled and `item.operationId` exists, it calculates a heading title using `item.summary`, falls back to `idToTitle(item.operationId)`, or defaults to the path string. The `Slugger` utility from `github-slugger` generates URL-safe anchors (`#id`) for the table of contents and heading arrays.

```typescript
const slugger = new Slugger();
const toc: TOCItemType[] = [];
const structuredData: StructuredData = { headings: [], contents: [] };

function pathItem(item: OperationObject, defaultTitle: string) {
  if (page.showTitle && item.operationId) {
    const title = item.summary || (item.operationId ? idToTitle(item.operationId) : defaultTitle);
    const id = slugger.slug(title);

    toc.push({
      depth: 2,
      title,
      url: `#${id}`,
    });
    structuredData.headings.push({
      content: title,
      id,
    });
  }

  if (item.description)
    structuredData.contents.push({
      content: item.description,
      heading: structuredData.headings.at(-1)?.id,
    });
}
```
Sources: [packages/openapi/src/utils/pages/to-static-data.ts:16-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-static-data.ts#L16-L41)

> [!NOTE]
> If an operation defines a `description`, it is automatically appended to `structuredData.contents` linked to the immediately preceding heading identifier.

Sources: [packages/openapi/src/utils/pages/to-static-data.ts:36-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-static-data.ts#L36-L41)

### Operation and Webhook Iteration Pipeline

The function processes collection loops for both HTTP path operations and asynchronous webhooks, resolving references through `dereferenceShallow`.

```typescript
for (const item of page.operations ?? []) {
  const operation = dereferenceShallow(doc.paths?.[item.path], doc)?.[item.method];
  if (!operation) continue;

  pathItem(operation, item.path);
}

for (const item of page.webhooks ?? []) {
  const webhook = dereferenceShallow(doc.webhooks?.[item.name], doc)?.[item.method];
  if (!webhook) continue;

  pathItem(webhook, item.name);
}

return { toc, structuredData };
```
Sources: [packages/openapi/src/utils/pages/to-static-data.ts:43-57](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-static-data.ts#L43-L57)

| Collection Source | Lookup Target | Dereference Source | Fallback Title Key |
| :--- | :--- | :--- | :--- |
| `page.operations` | `doc.paths?.[item.path]` | `doc` | `item.path` |
| `page.webhooks` | `doc.webhooks?.[item.name]` | `doc` | `item.name` |

Sources: [packages/openapi/src/utils/pages/to-static-data.ts:43-55](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-static-data.ts#L43-L55)

> [!WARNING]
> If `dereferenceShallow` returns a falsy operation or webhook object, the iteration skips the entry entirely without adding entries to the table of contents or structured metadata.

Sources: [packages/openapi/src/utils/pages/to-static-data.ts:44-45](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-static-data.ts#L44-L45), [packages/openapi/src/utils/pages/to-static-data.ts:51-52](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-static-data.ts#L51-L52)

## Virtual File Routing and Presets

### Preset Algorithms and Path Generation

The automatic page generation preset configures how endpoint schemas translate into virtual routes. The `createAutoPreset` function inspects `options.name` to determine naming behavior, supporting either custom naming functions or configuration objects with an `algorithm` property set to either `'v1'` or `'v2'`.

```typescript
  /**
   * The version of algorithm used to generate file paths.
   *
   * v1: Fumadocs OpenAPI v8
   * v2: Fumadocs OpenAPI v9
   *
   * @defaultValue v2
   */
  algorithm?: 'v2' | 'v1';
}
```
Sources: [packages/openapi/src/utils/pages/preset-auto.ts:81-90](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/preset-auto.ts#L81-L90)

When `result.type` is an operation, algorithm `'v2'` uses `operation.operationId` when available. If `algorithm` is `'v1'` or `operationId` is omitted, it falls back to joining `this.routePathToFilePath(result.item.path)` with `result.item.method.toLowerCase()`. For webhooks, algorithm `'v2'` similarly inspects `hook.operationId` via `dereferenceShallow`, falling back to `slugify(result.item.name)`.

```typescript
  if (result.type === 'operation') {
    const operation = this.document.paths![result.item.path]![result.item.method]!;

    if (algorithm === 'v2' && operation.operationId) {
      return operation.operationId;
    }

    return path.join(
      this.routePathToFilePath(result.item.path),
      result.item.method.toLowerCase(),
    );
  }

  const hook = dereferenceShallow(this.document.webhooks![result.item.name], this.document)[
    result.item.method
  ]!;

  if (algorithm === 'v2' && hook.operationId) {
    return hook.operationId;
  }

  return slugify(result.item.name);
};
```
Sources: [packages/openapi/src/utils/pages/preset-auto.ts:125-147](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/preset-auto.ts#L125-L147)

> [!WARNING]
> Relying on algorithm `'v2'` without explicit `operationId` definitions on operations or webhooks causes path generation to fall back directly to route path joining or slugified names.

Sources: [packages/openapi/src/utils/pages/preset-auto.ts:128-135](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/preset-auto.ts#L128-L135), [packages/openapi/src/utils/pages/preset-auto.ts:142-146](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/preset-auto.ts#L142-L146)

### Virtual File Resolution Workflow

The virtual file generation workflow converts loaded schema documents into structured file representations for server integrations. The execution walkthrough follows this exact sequence: `createOpenAPI` instantiates the server instance, which invokes `getVirtualFiles` to iterate over schemas, and passes output lists to `onEntries`.

```mermaid
sequenceDiagram
    participant Server as createOpenAPI
    participant Files as getVirtualFiles
    participant Entries as onEntries
    participant Entry as onEntry

    Server->>Files: createOpenAPI -> getVirtualFiles
    Files->>Entries: getVirtualFiles -> onEntries
    Entries->>Entry: onEntries -> onEntry
```

Sources: [packages/openapi/src/server/index.tsx:111-216](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/server/index.tsx#L111-L216)

1. `createOpenAPI` — Initializes `schemaMap` caches and returns the server object containing `getVirtualFiles`.
2. `getVirtualFiles` — Retrieves bundled schemas via `server.getSchemas()`, builds builder options using `createAutoPreset`, and invokes `fromSchema`.
3. `onEntries` — Recursively flattens or structures output entries, respecting `meta` options and `folderStyle` configurations.
4. `onEntry` — Maps each leaf entry into a virtual file object carrying `OpenAPIPageData` props, metadata, and static data handlers.

Sources: [packages/openapi/src/server/index.tsx:111-206](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/server/index.tsx#L111-L206)

> [!NOTE]
> If `meta` is enabled as an object with `folderStyle: 'separator'`, group entries push title separators (`---title---`) alongside path references (`...relativePath`) into the emitted `meta.json` pages array.

Sources: [packages/openapi/src/server/index.tsx:221-242](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/server/index.tsx#L221-L242)

## MDX Document Serialization

### String Template Conversion and Component Layouts

The `toText` function converts page outputs, operations, and webhooks into serialized MDX documents by orchestrating frontmatter formatting, metadata insertion, and component layout templates. It accepts an `entry`, a `doc` document object, and `options` of type `PagesToTextOptions`.

```typescript
export function toText(
  entry: PageOutput | OperationOutput | WebhookOutput,
  doc: Document,
  options: PagesToTextOptions = {},
) {
  const { frontmatter, includeDescription = false } = options;
  const extend = frontmatter?.(
    entry.info.title,
    entry.info.description,
    entry.type === 'page'
      ? entry.tag
        ? { type: 'tag', tag: entry.tag }
        : { type: 'file' }
      : { type: 'operation' },
  );
  const pageProps = getPageProps(entry);
  if (!includeDescription) {
    pageProps.showDescription = false;
  }

  let meta: InternalOpenAPIMeta = {
    preload: [entry.schemaId],
  };
  if (entry.type === 'operation' || entry.type === 'webhook') {
    const operation = entry.item;

    meta.method = operation.method.toUpperCase();
    meta.webhook = entry.type === 'webhook';
    meta.deprecated = entry.info.deprecated;
  }

  const data = toStaticData(pageProps, doc);

  return generateDocument(
    {
      title: entry.info.title,
      description: !includeDescription ? entry.info.description : undefined,
      full: true,
      ...extend,
      _openapi: {
        ...meta,
        ...data,
        ...(extend?._openapi as object | undefined),
      },
    },
    pageContent(pageProps),
    options,
  );
}
```
Sources: [packages/openapi/src/utils/pages/to-text.ts:57-105](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-text.ts#L57-L105)

The `pageContent` function generates a React layout component string that exposes `APIPage` and `OpenAPIPage` components for backward compatibility, forwarding `document`, `webhooks`, and `operations` props.

```typescript
function pageContent({
  showTitle,
  showDescription,
  document,
  webhooks,
  operations,
}: GeneratedPageProps): string {
  const propStrs: string[] = [`document=${doubleQuote(document)}`];

  // filter extra properties in props
  if (webhooks) {
    propStrs.push(
      `webhooks={${JSON.stringify(
        webhooks.map(
          (item) =>
            ({
              name: item.name,
              method: item.method,
            }) satisfies WebhookItem,
        ),
      )}}`,
    );
  }
  if (operations) {
    propStrs.push(
      `operations={${JSON.stringify(
        operations.map(
          (item) =>
            ({
              path: item.path,
              method: item.method,
            }) satisfies OperationItem,
        ),
      )}}`,
    );
  }
  if (showTitle) {
    propStrs.push(`showTitle`);
  }
  if (showDescription) {
    propStrs.push(`showDescription`);
  }

  return `export default function Layout(props) {
  const { APIPage, OpenAPIPage } = props.components ?? {};
  // "APIPage" is the old name from v10, this allows both for backward compatibility
  const Comp = OpenAPIPage ?? APIPage;
  return (
    <>
      {props.children}
      <Comp ${propStrs.join(' ')} />
    </>
  );
}`;
}
```
Sources: [packages/openapi/src/utils/pages/to-text.ts:153-207](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-text.ts#L153-L207)

> [!NOTE]
> The `APIPage` component name is preserved from v10 for backward compatibility; if `props.components.OpenAPIPage` is undefined, the layout falls back to `APIPage`.

Sources: [packages/openapi/src/utils/pages/to-text.ts:197-199](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-text.ts#L197-L199)

### Frontmatter Formatting and Options

The `generateDocument` utility serializes YAML frontmatter using the `js-yaml` `dump` function after stripping undefined properties via `removeUndefined`. It appends auto-generated comments and user-defined MDX component imports.

```typescript
export function generateDocument(
  frontmatter: unknown,
  content: string,
  options: PagesToTextOptions,
): string {
  const { addGeneratedComment = true, imports } = options;
  const out: string[] = [];
  const banner = dump(removeUndefined(frontmatter as object)).trimEnd();
  if (banner.length > 0) out.push(`---\n${banner}\n---`);

  if (addGeneratedComment) {
    let commentContent =
      'This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again.';

    if (typeof addGeneratedComment === 'string') {
      commentContent = addGeneratedComment;
    }

    commentContent = commentContent.replaceAll('/', '\\/');
    out.push(`{/* ${commentContent} */}`);
  }

  if (imports) {
    out.push(
      ...imports
        .map((item) => `import { ${item.names.join(', ')} } from ${doubleQuote(item.from)};`)
        .join('\n'),
    );
  }

  out.push(content);
  return out.join('\n\n');
}
```
Sources: [packages/openapi/src/utils/pages/to-text.ts:107-139](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-text.ts#L107-L139)

| Option Key | Type | Default Value | Purpose |
| :--- | :--- | :--- | :--- |
| `imports` | `{ names: string[]; from: string; }[]` | `undefined` | Additional imports of custom MDX components for the generated page. |
| `frontmatter` | `(title, description, context) => Record<string, unknown>` | `undefined` | Customizes frontmatter fields; a `full: true` property is added by default. |
| `includeDescription` | `boolean` | `false` | Controls whether description text is added to the document body instead of frontmatter. |
| `addGeneratedComment` | `boolean \| string` | `true` | Adds a comment at the top of generated files indicating they are auto-generated. |

Sources: [packages/openapi/src/utils/pages/to-text.ts:17-55](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-text.ts#L17-L55)

> [!WARNING]
> `includeDescription` is disabled by default because certain OpenAPI schemas contain invalid descriptions that break Markdown syntax during rendering.

Sources: [packages/openapi/src/utils/pages/to-text.ts:38-44](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-text.ts#L38-L44)

## Static Page File Generation

### Disk Writing Workflow and Watch Mode

The `generateFiles` function manages the end-to-end process of writing generated documentation files to the local disk. It accepts a `Config` object, invokes `generateFilesOnly` to resolve output buffers, and recursively creates destination directories using `node:fs/promises` before writing each file.

```typescript
export async function generateFiles(options: Config): Promise<void> {
  if (options.watch) {
    const { watch } = await import('chokidar');
    const subOptions: Config = { ...options, watch: false };

    await generateFiles(subOptions);
    const targets = options.input._getWatchPaths();
    console.log(`[Fumadocs OpenAPI] watching ${targets.join(', ')}`);
    watch(targets, {
      ignoreInitial: true,
    }).on('all', () => generateFiles(subOptions));
    return;
  }

  const files = await generateFilesOnly(options);

  await Promise.all(
    files.map(async (file) => {
      const filePath = path.join(options.output, file.path);

      await mkdir(path.dirname(filePath), { recursive: true });
      await writeFile(filePath, file.content);
      console.log(`Generated: ${filePath}`);
    }),
  );
}
```
Sources: [packages/openapi/src/generate-file.ts:95-120](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/generate-file.ts#L95-L120)

When `options.watch` is enabled, `generateFiles` spawns a `chokidar` file watcher targeting paths retrieved from `options.input._getWatchPaths()`. Whenever a schema file changes, the watcher triggers a regeneration cycle while setting `watch: false` on suboptions to prevent recursive watcher instantiation.

```typescript
if (options.watch) {
  const { watch } = await import('chokidar');
  const subOptions: Config = { ...options, watch: false };

  await generateFiles(subOptions);
  const targets = options.input._getWatchPaths();
  console.log(`[Fumadocs OpenAPI] watching ${targets.join(', ')}`);
  watch(targets, {
    ignoreInitial: true,
  }).on('all', () => generateFiles(subOptions));
  return;
}
```
Sources: [packages/openapi/src/generate-file.ts:96-107](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/generate-file.ts#L96-L107)

> [!WARNING]
> The built-in `watch` flag in `generateFiles` is intended only for simple development cases; it ignores custom input functions and URLs, and it is recommended to configure `chokidar` manually for advanced setups.

Sources: [packages/openapi/src/generate-file.ts:83-88](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/generate-file.ts#L83-L88)

### Manifest Generation and File Collection

The `generateFilesOnly` function iterates over schemas fetched from the OpenAPI server instance, creates auto presets via `createAutoPreset`, and transforms schema entries into `OutputFile` objects. It aggregates generated entries into a `BeforeWriteContext` containing `generated` record maps and `generatedEntries` records.

```typescript
export async function generateFilesOnly(
  options: DistributiveOmit<Config, 'output'>,
): Promise<OutputFile[]> {
  const schemas = await options.input.getSchemas();

  const files: OutputFile[] = [];
  const generated: Record<string, OutputFile[]> = {};
  const generatedEntries: Record<string, OutputEntry[]> = {};

  const entries = Object.entries(schemas);
  if (entries.length === 0) {
    throw new Error('No input files found.');
  }
  const preset = createAutoPreset(options);
  for (const [id, schema] of entries) {
    const entries = fromSchema(id, schema.bundled, preset);
    const schemaFiles: OutputFile[] = [];

    generatedEntries[id] = entries;
    function scan(entry: OutputEntry) {
      if (entry.type === 'group') {
        for (const child of entry.entries) scan(child);
        return;
      }

      schemaFiles.push({
        path: entry.path,
        content: toText(entry, schema.bundled, options),
      });
    }

    for (const entry of entries) scan(entry);
    generated[id] = schemaFiles;
    files.push(...schemaFiles);
  }

  const context: BeforeWriteContext = {
    generated,
    generatedEntries,
  };

  if (options.index) {
    files.push(...writeIndexFiles(context, options));
  }

  if (options.meta) {
    files.push(...generateMeta(context, options.meta === true ? {} : options.meta));
  }

  await options.beforeWrite?.call(context, files);
  return files;
}
```
Sources: [packages/openapi/src/generate-file.ts:122-173](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/generate-file.ts#L122-L173)

If `options.meta` is configured, `generateMeta` emits `meta.json` manifests for content folders. The `folderStyle` option determines how nested groups are structured within the manifest's `pages` array.

```typescript
function generateMeta(context: BeforeWriteContext, options: MetaOptions): OutputFile[] {
  const files: OutputFile[] = [];
  const folderStyle = options.folderStyle ?? 'folder';

  function scan(entries: OutputEntry[], parent?: OutputGroup) {
    const pages: string[] = [];

    for (const entry of entries) {
      const relativePath = PathUtils.slash(
        parent ? path.relative(parent.path, entry.path) : entry.path,
      );

      if (entry.type === 'group') {
        scan(entry.entries, entry);

        if (folderStyle === 'folder') {
          pages.push(relativePath);
        } else {
          pages.push(`---${entry.info.title}---`, `...${relativePath}`);
        }
      } else {
        pages.push(relativePath.slice(0, -path.extname(entry.path).length));
      }
    }

    if (pages.length === 0) return;
    files.push({
      path: parent ? path.join(parent.path, 'meta.json') : 'meta.json',
      content: JSON.stringify(
        {
          title: parent?.info.title,
          description: parent?.info.description,
          pages,
        },
        null,
        2,
      ),
    });
  }

  for (const entries of Object.values(context.generatedEntries)) {
    scan(entries);
  }

  return files;
}
```
Sources: [packages/openapi/src/generate-file.ts:175-220](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/generate-file.ts#L175-L220)

| Option Key | Type | Default Value | Purpose |
| :--- | :--- | :--- | :--- |
| `input` | `OpenAPIServer` | Required | The OpenAPI server object providing schema definitions. |
| `output` | `string` | Required | Target output directory path on the file system. |
| `index` | `IndexConfig` | `undefined` | Configuration for generating index files with cards linking to generated pages. |
| `meta` | `boolean \| MetaOptions` | `undefined` | Enables or configures `meta.json` file generation for navigation structures. |
| `beforeWrite` | `(files: OutputFile[]) => void \| Promise<void>` | `undefined` | Lifecycle hook to add, change, or remove output files before disk persistence. |

Sources: [packages/openapi/src/generate-file.ts:47-74](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/generate-file.ts#L47-L74)

> [!NOTE]
> When `folderStyle` is set to `'separator'` in `MetaOptions`, group entries emit custom title headers and path references using the format `---title---` and `...relativePath` in the generated `meta.json` file.

Sources: [packages/openapi/src/generate-file.ts:177-195](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/generate-file.ts#L177-L195)

## Related

- [OpenAPI Interactive UI](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/api-documentation/openapi-interactive-ui)
- [Code Sample Generators](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/api-documentation/code-sample-generators)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/llms.txt) for all pages in this wiki.
