---
title: "AsyncAPI Document Processing"
description: "The AsyncAPI document processing pipeline ingest, bundles, and transforms AsyncAPI specifications into structured Fumadocs virtual files, navigation hierarchies, and standalone documentation pages...."
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/asyncapi-document-processing"
---

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

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

- [packages/asyncapi/src/utils/document/dereference.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/document/dereference.ts)
- [packages/asyncapi/src/server/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/server/index.tsx)
- [packages/asyncapi/src/utils/document/load.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/document/load.ts)
- [packages/asyncapi/src/ui/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/ui/index.tsx)
- [packages/openapi/src/server/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/server/index.tsx)
- [packages/openapi/src/utils/document/load.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/document/load.ts)
- [packages/openapi/src/utils/document/dereference.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/document/dereference.ts)
- [packages/asyncapi/src/generate-file.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/generate-file.ts)
- [packages/asyncapi/src/utils/schema.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/schema.ts)
- [packages/asyncapi/src/utils/pages/builder.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/builder.ts)
- [packages/asyncapi/src/utils/pages/preset-auto.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/preset-auto.ts)
- [packages/python/src/convert.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/src/convert.ts)
- [packages/api-docs/src/components/schema/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/components/schema/index.tsx)
- [packages/asyncapi/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/index.ts)
- [packages/asyncapi/src/utils/pages/to-text.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/to-text.ts)
- [packages/openapi/src/ui/base.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/ui/base.tsx)
- [packages/mdx/src/runtime/server.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/runtime/server.ts)
- [packages/api-docs/src/schema/dereference.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/schema/dereference.ts)
- [packages/openapi/src/generate-file.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/generate-file.ts)
- [packages/preview/src/pages/...slugs.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx)
- [packages/content-collections/src/configuration.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content-collections/src/configuration.ts)
- [packages/openapi/src/utils/pages/builder.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/builder.ts)
- [packages/mdx/src/core.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/core.ts)
- [packages/api-docs/src/schema/bundle.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/schema/bundle.ts)
- [packages/core/src/source/loader.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/loader.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/asyncapi/package.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/package.json)
- [packages/python/fumapy/mksource/document_module.py](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/fumapy/mksource/document_module.py)
- [packages/doc-gen/src/remark-docgen.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/doc-gen/src/remark-docgen.ts)
- [packages/asyncapi/src/utils/traits.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/traits.ts)
</details>

## Overview

The AsyncAPI document processing pipeline ingest, bundles, and transforms AsyncAPI specifications into structured Fumadocs virtual files, navigation hierarchies, and standalone documentation pages. By combining reference bundling, shallow and deep schema dereferencing, automated page generation presets, and trait merging utilities, the framework standardizes event-driven API documentation for message channels and operations.

Sources: [packages/asyncapi/src/utils/document/load.ts:7-16](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/document/load.ts#L7-L16), [packages/asyncapi/src/server/index.tsx:110-203](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/server/index.tsx#L110-L203), [packages/asyncapi/src/utils/pages/preset-auto.ts:56-279](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/preset-auto.ts#L56-L279)

## Document Loading and Bundling

### Ingestion and Bundling Pipeline

AsyncAPI documents are ingested and prepared for processing via `loadDocument`, which accepts either a file path, a URL, or an inline `AsyncAPIObject` specification. The ingestion utility invokes `bundle` from `@apidevtools/json-schema-ref-parser` to resolve external and internal `$ref` pointers into a single unified JSON schema structure.

```typescript
export async function loadDocument(input: string | AsyncAPIObject): Promise<{
  bundled: AsyncAPIObject;
}> {
  try {
    const bundled = await bundle<AsyncAPIObject>(input);
    if (!bundled.asyncapi) {
      throw new Error('Missing required `asyncapi` field.');
    }

    return { bundled };
  } catch (e) {
    throw new Error(`[AsyncAPI] Failed to resolve input: ${input}`, {
      cause: e,
    });
  }
```
Sources: [packages/asyncapi/src/utils/document/load.ts:7-21](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/document/load.ts#L7-L21), [packages/api-docs/src/schema/bundle.ts:1-1](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/schema/bundle.ts#L1-L1)

> [!WARNING]
> If a loaded specification lacks a root `asyncapi` field after bundling, `loadDocument` throws an explicit error indicating a missing required property.
> Sources: [packages/asyncapi/src/utils/document/load.ts:12-14](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/document/load.ts#L12-L14)

### Server Options and Document Caching

The `createAsyncAPI` server manages inputs supplied either as an array of path strings or as a `SchemaRecord` mapping identifiers to static objects or async loader functions. Caching behavior can be controlled using the `disableCache` option.

```tsx
type SchemaRecord = Record<
  string,
  string | AsyncAPIObject | (() => Awaitable<string | AsyncAPIObject>)
>;
interface LoadedDocument {
  bundled: AsyncAPIObject;
}

export interface AsyncAPIOptions {
  input?: string[] | SchemaRecord;
  disableCache?: boolean;
}
```
Sources: [packages/asyncapi/src/server/index.tsx:28-39](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/server/index.tsx#L28-L39)

When caching is active, loaded document promises are stored in a local `schemaMap` to prevent redundant disk reads or network requests across schema queries.

```tsx
export function createAsyncAPI(options: AsyncAPIOptions = {}): AsyncAPIServer {
  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 AsyncAPI] the document "${schemaId}" is not listed in the input array, this may be unexpected and won't be cached properly.`,
      );
      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;
  }
```
Sources: [packages/asyncapi/src/server/index.tsx:73-101](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/server/index.tsx#L73-L101)

> [!NOTE]
> If a requested `schemaId` is not pre-registered in `resolvedInput`, `getSchema` emits a console warning that the document will not be cached properly before falling back to direct execution of `loadDocument(schemaId)`.
> Sources: [packages/asyncapi/src/server/index.tsx:85-90](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/server/index.tsx#L85-L90)

## Document Dereferencing Architecture

### Synchronized and Shallow Dereferencing Mechanisms

The framework provides specialized routines to resolve `$ref` pointers within bundled AsyncAPI and JSON schema documents. The `dereferenceBundledDocument` utility wraps `dereferenceSync` from [API Docs Primitives](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/api-documentation/api-docs-primitives), constructing a `DereferencedDocument` container that holds the original bundled document, the fully dereferenced specification tree, and a `getRawRef` lookup function that retrieves original reference pointers for objects.

```typescript
export function dereferenceBundledDocument(bundled: AsyncAPIObject): DereferencedDocument {
  const dereferenceMap = new Map<object, string>();

  return {
    bundled,
    dereferenced: dereferenceSync(bundled as JSONSchema, {
      setOriginalRef(schema, ref) {
        dereferenceMap.set(schema as object, ref);
      },
    }) as NoReference<AsyncAPIObject>,
    getRawRef(obj: object) {
      return dereferenceMap.get(obj);
    },
  };
```
Sources: [packages/asyncapi/src/utils/document/dereference.ts:20-33](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/document/dereference.ts#L20-L33)

> [!NOTE]
> The `dereferenceBundledDocument` function registers a `setOriginalRef` callback during synchronous traversal, mapping both resolved target schemas and modified parent objects back to their original `$ref` strings in an internal `Map`.
> Sources: [packages/asyncapi/src/utils/document/dereference.ts:25-32](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/document/dereference.ts#L25-L32)

### Dereference Options and Resolution Flow

The core synchronization engine operates via `dereferenceSync`, which clones the input schema structure using `structuredClone` and traverses nodes recursively with pointer path tracking via `appendInternalRefPath`. It supports options to control preservation and original reference tracking during traversal.

| Option | Type | Default | Purpose |
| :--- | :--- | :--- | :--- |
| `preserveRef` | `(pointer: string) => boolean` | `undefined` | Returns `true` to keep the Reference Object at a specific pointer without inlining. |
| `setOriginalRef` | `(schema: JSONSchema, ref: string) => void` | `() => {}` | Callback invoked with the resolved schema and its original reference pointer. |

```typescript
export interface DereferenceOptions {
  /**
   * Return `true` to keep the Reference Object at `pointer` without inlining.
   */
  preserveRef?: (pointer: string) => boolean;
  setOriginalRef?: (schema: JSONSchema, ref: string) => void;
}
```
Sources: [packages/api-docs/src/schema/dereference.ts:7-13](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/schema/dereference.ts#L7-L13)

For non-recursive or single-level resolution, `dereferenceShallow` inspects individual plain objects, extracts local properties while removing `$ref`, and resolves referenced definitions against a full document root using `resolveRefSync`.

```typescript
export function dereferenceShallow<T>(schema: T, full: unknown): NoReferenceShallow<T> {
  if (isPlainObject(schema)) {
    if (typeof schema.$ref !== 'string') return schema as never;

    const { $ref, ...rest } = schema;
    const resolved = dereferenceShallow(resolveRefSync($ref, full), full);

    if (typeof resolved !== 'object') throw new Error(`invalid schema referenced via "${$ref}"`);
    return { ...resolved, ...rest } as never;
  }

  return schema as never;
```
Sources: [packages/api-docs/src/schema/dereference.ts:67-78](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/schema/dereference.ts#L67-L78)

> [!CAUTION]
> If a referenced target resolves to a boolean instead of an object during deep synchronization, `dereferenceSync` throws an explicit `'invalid schema'` error.
> Sources: [packages/api-docs/src/schema/dereference.ts:48-48](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/schema/dereference.ts#L48-L48)

## Virtual File and Route Generation

### Virtual File Construction Pipeline

The transformation of loaded AsyncAPI specifications into Fumadocs virtual files is driven by a precise execution sequence. The framework coordinates file generation starting from server initialization, traversing schemas, processing entries, and constructing page properties.

1. `createAsyncAPI` — Initializes server configurations, schema maps, and resolves input sources.
2. `getVirtualFiles` — Asynchronously retrieves all bundled schemas and initializes builder configurations via `createAutoPreset`.
3. `onEntries` — Recursively evaluates generated output entries, handling directory groups and meta configuration files when `meta` options are enabled.
4. `onEntry` — Processes individual page and operation entries, generating page props and assigning static [table of contents](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/table-of-contents) and structured data.
5. `getPageProps` — Extracts generated metadata, document identifiers, and operations associated with the specific entry item.

```tsx
async function getVirtualFiles(server: AsyncAPIServer, options: AsyncAPISourceOptions) {
  const { baseDir = '', meta = false } = options;
  const { createAutoPreset } = await import('@/utils/pages/preset-auto');
  const { fromSchema } = await import('@/utils/pages/builder');
  const files: VirtualFile<{
    pageData: AsyncAPIPageData;
    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) {
      const props = getPageProps(entry);

      files.push({
        type: 'page',
        path: `${baseDir}/${entry.path}`,
        data: {
          ...entry.info,
          getAsyncAPIPageProps() {
            return {
              payload: {
                bundled: schema.bundled,
              },
              ...props,
            };
          },
          getSchema() {
            return {
              id,
              bundled: schema.bundled,
            };
          },
          ...toStaticData(props, schema.bundled),
          _asyncapi: {
            action: entry.type === 'operation' ? entry.item.action : undefined,
            deprecated: entry.info.deprecated,
          },
        },
      });
    }
```
Sources: [packages/asyncapi/src/server/index.tsx:110-156](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/server/index.tsx#L110-L156)

```mermaid
sequenceDiagram
    participant Server as createAsyncAPI
    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)
```

> [!NOTE]
> During `onEntries` execution, path normalization uses `PathUtils.slash` to compute relative paths between parent groups and child entries.
> Sources: [packages/asyncapi/src/server/index.tsx:174-177](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/server/index.tsx#L174-L177)

### Schema Builders and Static Data Mapping

The `fromSchema` builder function accepts a schema identifier, a bundled `AsyncAPIObject`, and a configuration object containing a `toPages` callback. It exposes a `PagesBuilder` interface that executes custom page generation logic and provides a `routePathToFilePath` utility function to clean route paths into filesystem-compatible slugs.

| Property / Method | Type | Purpose |
| :--- | :--- | :--- |
| `id` | `string` | The unique schema identifier. |
| `document` | `AsyncAPIObject` | The bundled AsyncAPI specification object. |
| `dereferenceShallow` | `<T>(schema: T) => NoReferenceSwallow<T>` | Resolves shallow references against the document root. |
| `create` | `(entry: OutputEntry) => void` | Appends a generated entry to the output list. |
| `routePathToFilePath` | `(path: string) => string` | Sanitizes route paths into filesystem paths. |

```typescript
export interface PagesBuilder {
  id: string;
  document: AsyncAPIObject;
  dereferenceShallow: <T>(schema: T) => NoReferenceSwallow<T>;
  create: (entry: OutputEntry) => void;
  routePathToFilePath: (path: string) => string;
}

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

  toPages({
    id: schemaId,
    document: bundled,
    dereferenceShallow: (s) => dereferenceShallow(s, bundled),
    create(entry) {
      files.push(entry);
    },
    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/asyncapi/src/utils/pages/builder.ts:49-84](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/builder.ts#L49-L84)

> [!CAUTION]
> The `routePathToFilePath` utility converts paths to lowercase, replaces periods (`.`) with hyphens (`-`), strips curly brace parameters like `{id}` to their inner values, and filters out empty path segments.
> Sources: [packages/asyncapi/src/utils/pages/builder.ts:72-82](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/builder.ts#L72-L82)

Once entries are mapped, `toStaticData` iterates over page operations, generating slugged table of contents items and structured data headings using `github-slugger`.

```typescript
export function toStaticData(
  page: GeneratedPageProps,
  doc: AsyncAPIObject,
): {
  toc: TOCItemType[];
  structuredData: StructuredData;
} {
  const slugger = new Slugger();
  const toc: TOCItemType[] = [];
  const structuredData: StructuredData = { headings: [], contents: [] };

  for (const item of page.operations ?? []) {
    const operation = dereferenceShallow(doc.operations?.[item.id], doc);
    if (!operation) continue;

    if (page.showTitle) {
      const title = getOperationDisplayName(item.id, operation);
      const id = slugger.slug(title);

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

    if (operation.description) {
      structuredData.contents.push({
        content: operation.description,
        heading: structuredData.headings.at(-1)?.id,
      });
    }
  }

  return { toc, structuredData };
```
Sources: [packages/asyncapi/src/utils/pages/to-static-data.ts:9-47](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/to-static-data.ts#L9-L47)

## Automated Page Structuring Presets

### Configuration Options and Preset Types

The automatic page structuring pipeline is configured via `createAutoPreset`, which accepts `SchemaToPagesOptions` to determine how AsyncAPI documents are parsed into pages, tags, files, or custom groupings. By default, slugification replaces whitespace characters with hyphens and converts strings to lowercase.

| Option | Type | Default | Purpose |
| :--- | :--- | :--- | :--- |
| `per` | `'file' \| 'tag' \| 'operation' \| 'custom'` | `'operation'` | Determines the partitioning granularity for generated documentation entries. |
| `groupBy` | `'tag' \| 'channel' \| 'none' \| function` | `'none'` | Groups operation entries by tag, channel address, or a custom mapper function. |
| `name` | `NameFn \| NameFnOptions` | `{ algorithm: 'v2' }` | Customizes file naming using algorithm version `'v1'` or `'v2'`, or a custom name function. |
| `slugify` | `(name: string) => string` | `s => s.replace(/\s+/g, '-').toLowerCase()` | Converts titles or identifiers into URL-safe slugs. |

```typescript
interface OperationConfig extends BaseConfig {
  per?: 'operation';
  groupBy?:
    | 'tag'
    | 'channel'
    | 'none'
    | ((entry: DistributiveOmit<OperationOutput, 'path'>) => string);
  name?: NameFn<OperationOutput> | NameFnOptions;
}

interface TagConfig extends BaseConfig {
  per: 'tag';
  name?: NameFn<PageOutput> | NameFnOptions;
}

interface SchemaConfig extends BaseConfig {
  per: 'file';
  name?: NameFn<PageOutput> | NameFnOptions;
}

export type SchemaToPagesOptions =
  | SchemaConfig
  | TagConfig
  | OperationConfig
  | ({
      per: 'custom';
    } & PagesBuilderConfig);

type NameFn<Entry extends OperationOutput | PageOutput = OperationOutput | PageOutput> = (
  this: PagesBuilder,
  output: DistributiveOmit<Entry, 'path'>,
) => string;

interface NameFnOptions {
  algorithm?: 'v2' | 'v1';
}

interface BaseConfig {
  slugify?: (name: string) => string;
}
```
Sources: [packages/asyncapi/src/utils/pages/preset-auto.ts:15-54](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/preset-auto.ts#L15-L54)

> [!NOTE]
> When `per` is set to `'custom'`, `createAutoPreset` bypasses internal generation logic and returns the user-provided `PagesBuilderConfig` object directly.
> Sources: [packages/asyncapi/src/utils/pages/preset-auto.ts:56-57](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/preset-auto.ts#L56-L57)

### Generation Algorithms and Grouping Logic

When processing documents under an operation-level preset, the `group` function iterates through operations and organizes them based on the `groupBy` parameter. If `groupBy` is set to `'channel'`, operations resolve their associated channel address to construct group titles and file paths. If set to `'tag'`, operations inspect their `tags` array; if an operation lacks tags, it logs a warning and assigns an `'unknown'` tag fallback before registering entries under corresponding tag directories.

```typescript
function group(
  builder: PagesBuilder,
  entries: DistributiveOmit<OperationOutput, 'path'>[],
): OutputEntry[] {
  const groups = new Map<string, OutputGroup>();
  const rest: OutputEntry[] = [];
  const doc = builder.document;
  const { groupBy = 'none' } = options as OperationConfig;

  for (const entry of entries) {
    switch (groupBy) {
      case 'channel': {
        const operation = dereferenceShallow(doc.operations?.[entry.item.id], doc);
        const channel = operation ? dereferenceShallow(operation.channel, doc) : undefined;
        const groupName = slugify(channel?.address || entry.item.id);

        let group = groups.get(groupName);
        if (!group) {
          group = {
            type: 'group',
            info: {
              title: channel?.title || channel?.address || groupName,
              description: channel?.description,
            },
            entries: [],
            schemaId: builder.id,
            path: groupName,
          };
          groups.set(groupName, group);
        }

        group.entries.push({
          ...entry,
          path: path.join(groupName, `${entry.item.action}.mdx`),
        });
        break;
      }
      case 'tag': {
        const operation = dereferenceShallow(doc.operations?.[entry.item.id], doc)!;
        let tags = operation.tags?.map((t) => dereferenceShallow(t, doc));

        if (!tags || tags.length === 0) {
          console.warn(
            'When `groupBy` is set to `tag`, make sure a `tags` is defined for every operation schema.',
          );
          tags = [{ name: 'unknown' }];
        }

        for (const tag of tags) {
          const groupId = slugify(tag.name);
          let group = groups.get(groupId);
          if (!group) {
            group = {
              type: 'group',
              info: { title: getTagDisplayName(tag), description: tag.description },
              tag,
              entries: [],
              schemaId: builder.id,
              path: groupId,
            };
            groups.set(groupId, group);
          }

          group.entries.push({
            ...entry,
            path: path.join(groupId, `${nameFn.call(builder, entry)}.mdx`),
          });
        }

        break;
      }
```
Sources: [packages/asyncapi/src/utils/pages/preset-auto.ts:88-158](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/preset-auto.ts#L88-L158)

> [!WARNING]
> When `groupBy` is configured as `'tag'`, ensure that every operation schema defines a `tags` array; otherwise, operations fall back to an `'unknown'` group with a console warning.
> Sources: [packages/asyncapi/src/utils/pages/preset-auto.ts:129-134](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/preset-auto.ts#L129-L134)

Name resolution algorithms distinguish between file-level and operation-level outputs. Page outputs default to their tag name or extract the base name of the schema file, whereas operation outputs use algorithm `'v2'` to return the operation identifier directly, or algorithm `'v1'` to join the sanitized route path with the operation action (`send` or `receive`).

```typescript
let nameFn: NameFn;

if (typeof options.name === 'function') {
  nameFn = options.name as NameFn;
} else {
  const { algorithm = 'v2' } = options.name ?? {};

  nameFn = function (result) {
    if (result.type === 'page') {
      if (result.tag) return slugify(result.tag.name);
      const schemaId = result.schemaId;

      return schemaId.startsWith('http://') || schemaId.startsWith('https://')
        ? 'index'
        : path.basename(schemaId, path.extname(schemaId));
    }

    if (algorithm === 'v2') {
      return result.item.id;
    }

    return path.join(this.routePathToFilePath(result.item.id), result.item.action);
  };
}
```
Sources: [packages/asyncapi/src/utils/pages/preset-auto.ts:63-86](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/preset-auto.ts#L63-L86)

## Static File and MDX Output

### Execution Flow and Disk Generation Walkthrough

The disk generation pipeline transforms generated page structures into serialized MDX files on disk, supporting optional file watching, metadata generation, and custom pre-write hooks. The process begins by calling `generateFiles`, which delegates the core generation work and handles asynchronous file writing or sets up a file watcher via `chokidar`.

```mermaid
sequenceDiagram
    participant generate-file.ts as generate-file.ts
    participant to-text.ts as to-text.ts
    generate-file.ts->>generate-file.ts: generateFilesOnly
    generate-file.ts->>generate-file.ts: writeIndexFiles
    generate-file.ts->>to-text.ts: fileContent
```
Sources: [packages/asyncapi/src/generate-file.ts:56-134](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/generate-file.ts#L56-L134)

1. `generateFilesOnly` extracts input schemas, invokes `createAutoPreset`, and iterates through entries using `fromSchema` to produce virtual files and metadata objects.
Sources: [packages/asyncapi/src/generate-file.ts:83-117](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/generate-file.ts#L83-L117)
2. `writeIndexFiles` constructs index or overview files if an index configuration is specified, mapping relative paths and checking available schema keys.
Sources: [packages/asyncapi/src/generate-file.ts:183-209](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/generate-file.ts#L183-L209)
3. `fileContent` builds the card layout containing hyperlinks and descriptions for output entries before passing the accumulated content to `generateDocument`.
Sources: [packages/asyncapi/src/generate-file.ts:210-252](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/generate-file.ts#L210-L252)

> [!NOTE]
> When `options.watch` is enabled, `generateFiles` recursively invokes itself with `watch: false` and registers a chokidar watcher on `input._getWatchPaths()` to trigger regeneration on file changes.
> Sources: [packages/asyncapi/src/generate-file.ts:57-68](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/generate-file.ts#L57-L68)

### YAML Frontmatter Serialization and MDX Generation

Document serialization is handled by `toText` and `generateDocument`, which serialize metadata into YAML frontmatter and format the resulting MDX document. Frontmatter properties are processed using `js-yaml`'s `dump` function alongside `removeUndefined`, while optional comments warn consumers against manual file modification.

| Option Key | Type | Default | Purpose |
| :--- | :--- | :--- | :--- |
| `imports` | `Array<{ names: string[]; from: string }>` | `undefined` | Custom import statements prepended to the generated MDX document. |
| `frontmatter` | `(title: string, description: string \| undefined, context: DocumentContext) => Record<string, unknown>` | `undefined` | Extends or customizes generated YAML frontmatter attributes. |
| `includeDescription` | `boolean` | `false` | Controls whether descriptions appear in frontmatter or within page props. |
| `addGeneratedComment` | `boolean \| string` | `true` | Appends an automated generation warning comment or a custom string to the file banner. |

```typescript
export interface PagesToTextOptions {
  imports?: {
    names: string[];
    from: string;
  }[];
  frontmatter?: (
    title: string,
    description: string | undefined,
    context: DocumentContext,
  ) => Record<string, unknown>;
  includeDescription?: boolean;
  addGeneratedComment?: boolean | string;
}
```
Sources: [packages/asyncapi/src/utils/pages/to-text.ts:15-27](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/to-text.ts#L15-L27)

```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/asyncapi/src/utils/pages/to-text.ts:76-108](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/to-text.ts#L76-L108)

> [!WARNING]
> Generated comment strings automatically replace all forward slashes with escaped backslashes (`replaceAll('/', '\\/')`) to preserve valid comment syntax in MDX outputs.
> Sources: [packages/asyncapi/src/utils/pages/to-text.ts:94-94](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/to-text.ts#L94-L94)

## Operation Traits and Schema Merging

AsyncAPI definitions often use reusable traits to share common attributes, metadata, and configurations across operations and messages. The trait merging implementation processes these reusable layers according to the AsyncAPI specification, flattening trait arrays into base objects while omitting the `traits` property itself from the final merged output.

The merging engine is driven by `mergeTraits` and `mergeTraitLayer`, which iterate through an array of trait dictionaries and shallow-copy or deep-merge plain objects property by property. When an operation or message object contains a `traits` array, `applyOperationTraits` and `applyMessageTraits` extract the base properties by destructuring away the `traits` field and passing the remaining properties along with the trait layers into `mergeTraits`.

| Function Name | Input Parameters | Return Type | Description |
| :--- | :--- | :--- | :--- |
| `mergeTraits` | `target: T`, `traits: Record<string, unknown>[]` | `T` | Sequentially folds multiple trait object layers into a target record. |
| `mergeTraitLayer` | `target: Record<string, unknown>`, `trait: Record<string, unknown>` | `Record<string, unknown>` | Recursively merges a single trait layer into a target record, ignoring the `traits` key. |
| `applyOperationTraits` | `operation: NoReference<OperationObject>` | `NoReference<OperationObject>` | Strips the `traits` property from an operation object and merges any declared traits. |
| `applyMessageTraits` | `message: NoReference<MessageObject>` | `NoReference<MessageObject>` | Strips the `traits` property from a message object and merges any declared traits. |

```typescript
export function mergeTraits<T extends Record<string, unknown>>(
  target: T,
  traits: Record<string, unknown>[],
): T {
  let result: Record<string, unknown> = { ...target };
  for (const trait of traits) {
    result = mergeTraitLayer(result, trait);
  }
  return result as T;
}

function mergeTraitLayer(
  target: Record<string, unknown>,
  trait: Record<string, unknown>,
): Record<string, unknown> {
  const result: Record<string, unknown> = { ...target };

  for (const [key, traitValue] of Object.entries(trait)) {
    if (key === 'traits') continue;

    const targetValue = result[key];
    if (targetValue === undefined) {
      result[key] = traitValue;
    } else if (isPlainObject(targetValue) && isPlainObject(traitValue)) {
      result[key] = mergeTraitLayer(targetValue, traitValue);
    }
  }

  return result;
}
```
Sources: [packages/asyncapi/src/utils/traits.ts:9-38](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/traits.ts#L9-L38)

```typescript
export function applyOperationTraits(
  operation: NoReference<OperationObject>,
): NoReference<OperationObject> {
  const traits = operation.traits;
  if (!traits || traits.length === 0) return operation;

  const { traits: _, ...base } = operation;
  return mergeTraits(base, traits);
}

export function applyMessageTraits(
  message: NoReference<MessageObject>,
): NoReference<MessageObject> {
  const traits = message.traits;
  if (!traits || traits.length === 0) return message;

  const { traits: _, ...base } = message;
  return mergeTraits(base, traits);
}
```
Sources: [packages/asyncapi/src/utils/traits.ts:40-58](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/traits.ts#L40-L58)

> [!NOTE]
> During trait layer merging, the algorithm explicitly checks `if (key === 'traits') continue;` to prevent nested or recursive trait declarations from re-triggering trait application loops.
> Sources: [packages/asyncapi/src/utils/traits.ts:26-28](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/traits.ts#L26-L28)

> [!WARNING]
> If an operation or message object defines no traits or an empty traits array (`traits.length === 0`), `applyOperationTraits` and `applyMessageTraits` return the original object instance without modification.
> Sources: [packages/asyncapi/src/utils/traits.ts:43-44](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/traits.ts#L43-L44), [packages/asyncapi/src/utils/traits.ts:53-54](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/traits.ts#L53-L54)

## Related

- [AsyncAPI Protocol Bindings](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/api-documentation/asyncapi-protocol-bindings)
- [API Docs Primitives](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/api-documentation/api-docs-primitives)


## Sitemap

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