---
title: "Page Tree Generation"
description: "Page tree generation constructs and manages the hierarchical navigation models that organize documentation content across the Fumadocs framework ecosystem. By parsing content files, resolving direc..."
last_updated: "2026-10-12T07:53:59.827853+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/page-tree-generation"
---

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

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

- [packages/core/src/source/page-tree/builder.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/builder.ts)
- [packages/radix-ui/src/components/sidebar/page-tree.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/sidebar/page-tree.tsx)
- [packages/base-ui/src/components/sidebar/page-tree.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/sidebar/page-tree.tsx)
- [packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/notebook/slots/sidebar.tsx)
- [packages/base-ui/src/layouts/notebook/slots/sidebar.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/notebook/slots/sidebar.tsx)
- [packages/asyncapi/src/server/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/server/index.tsx)
- [packages/base-ui/src/components/sidebar/tabs/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/sidebar/tabs/index.tsx)
- [packages/radix-ui/src/components/sidebar/tabs/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/sidebar/tabs/index.tsx)
- [packages/core/src/source/loader.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/loader.ts)
- [packages/core/src/source/plugins/status-badges.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/plugins/status-badges.tsx)
- [packages/radix-ui/src/utils/use-footer-items.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/utils/use-footer-items.ts)
- [packages/base-ui/src/layouts/flux/slots/sidebar.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/flux/slots/sidebar.tsx)
- [packages/radix-ui/src/layouts/flux/slots/sidebar.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/flux/slots/sidebar.tsx)
- [packages/core/src/source/llms.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/llms.ts)
- [packages/cli/src/commands/file-tree.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/file-tree.ts)
- [packages/base-ui/src/utils/use-footer-items.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/utils/use-footer-items.ts)
- [packages/radix-ui/src/contexts/tree.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/tree.tsx)
- [packages/core/src/source/client/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx)
- [packages/openapi/src/server/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/server/index.tsx)
- [packages/base-ui/src/contexts/tree.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/contexts/tree.tsx)
- [packages/core/src/source/page-tree/transformer-fallback.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/transformer-fallback.ts)
- [packages/core/src/page-tree/utils.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/page-tree/utils.ts)
- [packages/openapi/src/generate-file.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/generate-file.ts)
- [packages/core/src/breadcrumb.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/breadcrumb.tsx)
- [packages/epub/src/toc-builder.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/epub/src/toc-builder.ts)
- [packages/openapi/src/utils/pages/builder.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/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/base-ui/src/components/sidebar/link-item.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/sidebar/link-item.tsx)
- [packages/radix-ui/src/components/sidebar/link-item.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/sidebar/link-item.tsx)
- [packages/radix-ui/src/layouts/shared/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/shared/index.tsx)
</details>

## Overview

Page tree generation constructs and manages the hierarchical navigation models that organize documentation content across the Fumadocs framework ecosystem. By parsing content files, resolving directory structures, and processing metadata, the builder turns raw file storage into structured node primitives including roots, folders, items, and separators.

Sources: [packages/core/src/source/page-tree/builder.ts:72-87](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/builder.ts#L72-L87), [packages/core/src/source/loader.ts:1-15](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/loader.ts#L1-L15)

## Page Tree Data Structures

### Node Types and Primitives

The core page tree architecture represents documentation structures through a set of primitive node schemas and symbols. Internal builder structures attach metadata such as `SymbolUnfinished`, `SymbolName`, and `SymbolOwner` to track ownership priority and resolve references when multiple folders claim the same node.

Sources: [packages/core/src/source/page-tree/builder.ts:74-82](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/builder.ts#L74-L82)

The builder interface exposes path resolution and root generation capabilities through the `PageTreeBuilder` contract.

```typescript
export interface PageTreeBuilder {
  resolveFlattenPath(name: string, format: string): string;
  root(id?: string, path?: string): PageTree.Root;
}
```
Sources: [packages/core/src/source/page-tree/builder.ts:84-87](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/builder.ts#L84-L87)

Tree traversal utilities operate on nodes and roots, flattening hierarchical structures into ordered arrays of page items, locating neighboring nodes for pagination, and discovering parent containers via depth-first searches.

Sources: [packages/core/src/page-tree/utils.ts:6-19](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/page-tree/utils.ts#L6-L19), [packages/core/src/page-tree/utils.ts:24-33](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/page-tree/utils.ts#L24-L33), [packages/core/src/page-tree/utils.ts:105-119](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/page-tree/utils.ts#L105-L119)

## Hierarchical Tree Builder

### File Scanning and Path Resolution

The builder initializes by scanning storage instances via `createPageTreeBuilder`, mapping every file and its content format into a flattened path lookup table. The `excludePrefix` constant (`!`) identifies excluded items during tree construction, while internal symbols track state: `SymbolUnfinished` marks incomplete nodes, `SymbolName` stores display identifiers, and `SymbolOwner` records ownership objects consisting of an owner path and priority.

```typescript
const excludePrefix = '!';

const SymbolUnfinished = Symbol('unfinished');
const SymbolName = Symbol('name');
const SymbolOwner = Symbol('owner');

interface Attached {
  [SymbolName]?: string;
  [SymbolUnfinished]?: boolean;
  [SymbolOwner]?: { owner: string; priority: number };
}
```
Sources: [packages/core/src/source/page-tree/builder.ts:72-82](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/builder.ts#L72-L82)

Content storages are iterated to register file paths by stripping their extensions and concatenating format extensions into `flattenPathToFullPath` mapping entries. The `resolveFlattenPath` method retrieves the full file path from a given name and format or falls back to the original name.

```typescript
  for (const file of storage.getFiles()) {
    const content = storage.read(file)!;
    const flattenPath = file.substring(0, file.length - extname(file).length);

    flattenPathToFullPath.set(flattenPath + '.' + content.format, file);
  }

  function resolveFlattenPath(name: string, format: string) {
    return flattenPathToFullPath.get(name + '.' + format) ?? name;
  }
```
Sources: [packages/core/src/source/page-tree/builder.ts:140-149](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/builder.ts#L140-L149)

### Ownership Tracking and Transfer

When multiple folders reference the same node, ownership tracking determines container assignment based on priority values. The `own` function checks if a node is unfinished, evaluates existing ownership, updates matching ownership priorities, or revokes ownership from lower-priority folders by splicing nodes out of their children lists and clearing folder index references.

```typescript
  function own(ownerPath: string, node: PageTree.Node & Attached, priority: number): boolean {
    if (node[SymbolUnfinished]) return false;
    const existing = node[SymbolOwner];
    if (!existing) {
      node[SymbolOwner] = { owner: ownerPath, priority };
      return true;
    }
    if (existing.owner === ownerPath) {
      existing.priority = Math.max(existing.priority, priority);
      return true;
    }
    if (existing.priority >= priority) return false;

    // return ownership
    const folder = pathToNode.get(existing.owner);
    if (folder && folder.type === 'folder') {
      if (folder.index === node) {
        delete folder.index;
      } else {
        const idx = folder.children.indexOf(node);
        if (idx !== -1) folder.children.splice(idx, 1);
      }
    }
    existing.owner = ownerPath;
    existing.priority = priority;
    return true;
  }

  function transferOwner(ownerPath: string, node: PageTree.Node & Attached) {
    const existing = node[SymbolOwner];
    if (existing) existing.owner = ownerPath;
  }
```
Sources: [packages/core/src/source/page-tree/builder.ts:158-190](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/builder.ts#L158-L190)

> [!NOTE]
> Lower priority owners attempting to claim a node already owned by a higher priority parent will have their claim rejected, returning `false` without modifying existing ownership boundaries.

Sources: [packages/core/src/source/page-tree/builder.ts:169-169](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/builder.ts#L169-L169)

### Fallback Transformation

The fallback mechanism implemented in `transformerFallback` captures unreferenced files and directories that were not visited during standard tree building. It isolates remaining storage items and generates a secondary fallback page tree prefixed with `fallback`.

```typescript
export function transformerFallback(): PageTreeTransformer {
  const addedFiles = new Set<string>();
  function shouldIgnore(context: PageTreeBuilderContext) {
    return context.custom?._fallback === true;
  }

  return {
    root(root) {
      if (shouldIgnore(this)) return root;
      const isolatedStorage = new FileSystem() as ContentStorage;
      if (addedFiles.size === this.storage.files.size) return root;

      for (const file of this.storage.getFiles()) {
        if (addedFiles.has(file)) continue;

        isolatedStorage.write(file, this.storage.read(file)!);
      }

      root.fallback = createPageTreeBuilder(isolatedStorage, {
        ...this.options,
        idPrefix: this.options.idPrefix ? `fallback:${this.options.idPrefix}` : 'fallback',
        generateFallback: false,
        context: { ...this.custom, _fallback: true },
      }).root();

      addedFiles.clear();
      return root;
    },
    file(node, file) {
      if (shouldIgnore(this)) return node;
      if (file) addedFiles.add(file);

      return node;
    },
    folder(node, _dir, metaPath) {
      if (shouldIgnore(this)) return node;
      if (metaPath) addedFiles.add(metaPath);

      return node;
    },
  };
}
```
Sources: [packages/core/src/source/page-tree/transformer-fallback.ts:5-45](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/transformer-fallback.ts#L5-L45)

## Tree Navigation and Traversal Utilities

### Depth-First Tree Visitation and Mutation

The `visit` function executes a depth-first search across a page tree or node collection, invoking a visitor callback for each encountered node. The visitor callback can return `'skip'` to bypass children of the current node, `'break'` to terminate the search entirely via a thrown `VisitBreak` symbol caught internally, or a modified node object to perform in-place tree transformations.

```typescript
export function visit<Root extends PageTree.Node | PageTree.Root>(
  root: Root,
  visitor: <T extends PageTree.Node | PageTree.Root>(
    node: T,
    parent?: PageTree.Root | PageTree.Folder,
  ) => 'skip' | 'break' | T | void,
): Root {
  function onNode<T extends PageTree.Node | PageTree.Root>(
    node: T,
    parent?: PageTree.Root | PageTree.Folder,
  ): T {
    const result = visitor(node, parent);
    switch (result) {
      case 'skip':
        return node;
      case 'break':
        throw VisitBreak;
      default:
        if (result) node = result;
    }

    if ('index' in node && node.index) {
      node.index = onNode(node.index, node);
    }

    if ('fallback' in node && node.fallback) {
      node.fallback = onNode(node.fallback, node);
    }

    if ('children' in node) {
      for (let i = 0; i < node.children.length; i++) {
        node.children[i] = onNode(node.children[i], node);
      }
    }

    return node;
  }

  try {
    return onNode(root);
  } catch (e) {
    if (e === VisitBreak) return root;
    throw e;
  }
}
```
Sources: [packages/core/src/page-tree/utils.ts:176-220](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/page-tree/utils.ts#L176-L220)

> [!WARNING]
> Returning `'break'` from a visitor callback throws an internal `VisitBreak` symbol that is intercepted by `visit` to cleanly return the unmodified root reference, avoiding unhandled exceptions during traversal aborts.

Sources: [packages/core/src/page-tree/utils.ts:168-175](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/page-tree/utils.ts#L168-L175), [packages/core/src/page-tree/utils.ts:214-219](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/page-tree/utils.ts#L214-L219)

### Path Lookup and Neighbor Resolution

Path lookup and list flattening utilities transform hierarchical tree structures into linear arrays or specific ancestor trails. The `flattenTree` function iterates over nodes, extracting folder indices and nested children recursively into a flat list of page items.

```typescript
export function flattenTree(nodes: PageTree.Node[]): PageTree.Item[] {
  const out: PageTree.Item[] = [];

  for (const node of nodes) {
    if (node.type === 'folder') {
      if (node.index) out.push(node.index);
      out.push(...flattenTree(node.children));
    } else if (node.type === 'page') {
      out.push(node);
    }
  }

  return out;
}
```
Sources: [packages/core/src/page-tree/utils.ts:6-19](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/page-tree/utils.ts#L6-L19)

The `findPath` function searches for a node matching a predicate, optionally including separators in the returned path array. Breadcrumb generation leverages `searchPath` to resolve the current URL against normalized tree paths.

```typescript
export function findPath(
  nodes: PageTree.Node[],
  matcher: (node: PageTree.Node) => boolean,
  options: {
    includeSeparator?: boolean;
  } = {},
): PageTree.Node[] | null {
  const { includeSeparator = true } = options;

  function run(nodes: PageTree.Node[]): PageTree.Node[] | undefined {
    let separator: PageTree.Separator | undefined;

    for (const node of nodes) {
      if (matcher(node)) {
        const items: PageTree.Node[] = [];
        if (separator) items.push(separator);
        items.push(node);

        return items;
      }

      if (node.type === 'separator' && includeSeparator) {
        separator = node;
        continue;
      }

      if (node.type === 'folder') {
        const items = node.index && matcher(node.index) ? [node.index] : run(node.children);

        if (items) {
          items.unshift(node);
          if (separator) items.unshift(separator);

          return items;
        }
      }
    }
  }

  return run(nodes) ?? null;
}
```
Sources: [packages/core/src/page-tree/utils.ts:126-166](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/page-tree/utils.ts#L126-L166)

| Utility Function | Parameters | Return Type | Description |
| :--- | :--- | :--- | :--- |
| `flattenTree` | `nodes: PageTree.Node[]` | `PageTree.Item[]` | Flattens folders, index nodes, and pages into a single sequential list. |
| `findNeighbour` | `tree: PageTree.Root`, `url: string`, `options?: { separateRoot?: boolean }` | `{ previous?: PageTree.Item, next?: PageTree.Item }` | Resolves adjacent previous and next page items for pagination controls. |
| `getPageTreeRoots` | `pageTree: PageTree.Root \| PageTree.Folder` | `(PageTree.Root \| PageTree.Folder)[]` | Collects all root nodes and nested root folders. |
| `getPageTreePeers` | `treeOrTrees: PageTree.Root \| Record<string, PageTree.Root>`, `url: string` | `PageTree.Item[]` | Retrieves peer page nodes residing under the same parent container. |
| `findSiblings` | `treeOrTrees: PageTree.Root \| Record<string, PageTree.Root>`, `url: string` | `PageTree.Node[]` | Retrieves all sibling tree nodes under the same parent. |
| `findParent` | `from: PageTree.Root \| PageTree.Folder`, `url: string` | `PageTree.Root \| PageTree.Folder \| undefined` | Locates the parent container of a page matched by its URL. |
| `findPath` | `nodes: PageTree.Node[]`, `matcher: (node: PageTree.Node) => boolean`, `options?: { includeSeparator?: boolean }` | `PageTree.Node[] \| null` | Finds the array of nodes forming the path to a matched node. |

Sources: [packages/core/src/page-tree/utils.ts:6-166](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/page-tree/utils.ts#L6-L166)

## Status Badges and Tree Transformation

### Status Badges Plugin Architecture

The `statusBadgesPlugin` function returns a loader plugin named `fumadocs:status-badges` that decorates navigation items with status indicators by inspecting document frontmatter. When processing a file node, the transformer checks whether the file format is `'page'` and whether a string-typed `status` property exists within `file.data`.

```tsx
export function statusBadgesPlugin(
  options: {
    renderBadge?: (status: string) => ReactNode;
  } = {},
): LoaderPlugin {
  const { renderBadge = (status) => <span data-status={status}>{status}</span> } = options;

  return {
    name: 'fumadocs:status-badges',
    transformPageTree: {
      file(node, filePath) {
        if (!filePath) return node;

        const file = this.storage.read(filePath);
        if (
          file?.format === 'page' &&
          'status' in file.data &&
          typeof file.data.status === 'string'
        ) {
          const status = file.data.status;

          node.name = (
            <>
              {node.name}
              {renderBadge(status)}
            </>
          );
          (node as Item).status = status;
        }

        return node;
      },
    },
  };
```
Sources: [packages/core/src/source/plugins/status-badges.tsx:50-83](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/plugins/status-badges.tsx#L50-L83)

If a valid status string is located, the plugin modifies `node.name` by appending a rendered badge element alongside the original node name, and assigns the raw status string directly to `(node as Item).status`. Consumers can customize badge rendering by passing a `renderBadge` callback option; if omitted, the plugin defaults to rendering a `<span>` element equipped with a `data-status` attribute.

```tsx
const { renderBadge = (status) => <span data-status={status}>{status}</span> } = options;

return {
  name: 'fumadocs:status-badges',
  transformPageTree: {
    file(node, filePath) {
      if (!filePath) return node;

      const file = this.storage.read(filePath);
      if (
        file?.format === 'page' &&
        'status' in file.data &&
        typeof file.data.status === 'string'
      ) {
        const status = file.data.status;

        node.name = (
          <>
            {node.name}
            {renderBadge(status)}
          </>
        );
        (node as Item).status = status;
      }
```
Sources: [packages/core/src/source/plugins/status-badges.tsx:55-78](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/plugins/status-badges.tsx#L55-L78)

> [!NOTE]
> The `statusBadgesPlugin` strictly requires `filePath` to be defined on the node and relies on `this.storage.read(filePath)` to access document frontmatter during tree transformation.

Sources: [packages/core/src/source/plugins/status-badges.tsx:60-63](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/plugins/status-badges.tsx#L60-L63)

### Fallback Tree Transformation

The `transformerFallback` function constructs a secondary fallback page tree builder by intercepting root, file, and folder transformation hooks. It tracks processed files in an internal `addedFiles` set and inspects the builder context via `shouldIgnore` to verify whether `context.custom?._fallback` is enabled, preventing recursive fallback generation loops.

```typescript
export function transformerFallback(): PageTreeTransformer {
  const addedFiles = new Set<string>();
  function shouldIgnore(context: PageTreeBuilderContext) {
    return context.custom?._fallback === true;
  }

  return {
    root(root) {
      if (shouldIgnore(this)) return root;
      const isolatedStorage = new FileSystem() as ContentStorage;
      if (addedFiles.size === this.storage.files.size) return root;

      for (const file of this.storage.getFiles()) {
        if (addedFiles.has(file)) continue;

        isolatedStorage.write(file, this.storage.read(file)!);
      }

      root.fallback = createPageTreeBuilder(isolatedStorage, {
        ...this.options,
        idPrefix: this.options.idPrefix ? `fallback:${this.options.idPrefix}` : 'fallback',
        generateFallback: false,
        context: { ...this.custom, _fallback: true },
      }).root();

      addedFiles.clear();
      return root;
    },
    file(node, file) {
      if (shouldIgnore(this)) return node;
      if (file) addedFiles.add(file);

      return node;
    },
    folder(node, _dir, metaPath) {
      if (shouldIgnore(this)) return node;
      if (metaPath) addedFiles.add(metaPath);

      return node;
    },
  };
```
Sources: [packages/core/src/source/page-tree/transformer-fallback.ts:5-45](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/transformer-fallback.ts#L5-L45)

When the root transformer executes, it initializes an isolated file system storage instance, copies unindexed files that are absent from `addedFiles`, and instantiates a nested page tree builder with a modified `idPrefix` and a restricted context where `_fallback` is set to `true`.

```typescript
root(root) {
  if (shouldIgnore(this)) return root;
  const isolatedStorage = new FileSystem() as ContentStorage;
  if (addedFiles.size === this.storage.files.size) return root;

  for (const file of this.storage.getFiles()) {
    if (addedFiles.has(file)) continue;

    isolatedStorage.write(file, this.storage.read(file)!);
  }

  root.fallback = createPageTreeBuilder(isolatedStorage, {
    ...this.options,
    idPrefix: this.options.idPrefix ? `fallback:${this.options.idPrefix}` : 'fallback',
    generateFallback: false,
    context: { ...this.custom, _fallback: true },
  }).root();

```
Sources: [packages/core/src/source/page-tree/transformer-fallback.ts:12-29](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/transformer-fallback.ts#L12-L29)

> [!WARNING]
> The fallback transformer clears `addedFiles` immediately after constructing the fallback tree root, ensuring subsequent transformation passes evaluate file additions correctly.

Sources: [packages/core/src/source/page-tree/transformer-fallback.ts:30-31](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/page-tree/transformer-fallback.ts#L30-L31)

## Client Serialization and State Contexts

### Serialization and Hydration

Page trees generated by server-side loaders are serialized into a standard format before transmission across client-server boundaries. The `SerializedPageTree` interface wraps data using the `$fumadocs_loader` discriminator set to `'page-tree'`.

```tsx
export interface SerializedPageTree {
  $fumadocs_loader: 'page-tree';
  data: object;
}
```
Sources: [packages/core/src/source/client/index.tsx:5-8](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L5-L8)

The `useFumadocsLoader` hook processes serialized loader data, identifying serialized page trees and passing them to `deserializePageTree` for hydration.

```tsx
export function useFumadocsLoader<V>(serialized: V): Serialized<V> {
  return useMemo(() => {
    const out: Record<string, unknown> = {};
    for (const k in serialized) {
      const v = serialized[k];
      if (isSerializedPageTree(v)) {
        out[k] = deserializePageTree(v);
      } else {
        out[k] = v;
      }
    }
    return out as Serialized<V>;
  }, [serialized]);
}
```
Sources: [packages/core/src/source/client/index.tsx:47-60](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L47-L60)

During deserialization, `visit` traverses the page tree nodes to parse and inject HTML strings for `icon` and `name` properties via `dangerouslySetInnerHTML`.

```tsx
export function deserializePageTree(serialized: SerializedPageTree): PageTree.Root {
  const root = serialized.data as PageTree.Root;
  visit(root, (item) => {
    if ('icon' in item && typeof item.icon === 'string') {
      item.icon = (
        <span
          dangerouslySetInnerHTML={{
            __html: item.icon,
          }}
        />
      );
    }
    if (typeof item.name === 'string') {
      item.name = (
        <span
          className="fd-page-tree-item-name"
          dangerouslySetInnerHTML={{
            __html: item.name,
          }}
        />
      );
    }
  });

  return root;
}
```
Sources: [packages/core/src/source/client/index.tsx:14-39](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L14-L39)

> [!NOTE]
> `deserializePageTree` inspects every visited node and converts string-based icons and names into React element structures containing `dangerouslySetInnerHTML` wrappers.

Sources: [packages/core/src/source/client/index.tsx:16-35](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L16-L35)

### Tree Contexts and Path Tracking

The `TreeContextProvider` component manages active tree state, path breadcrumbs, and unique root identifiers across client navigation. It computes the active breadcrumb path by searching the tree and its fallback children against the current pathname.

Sources: [packages/radix-ui/src/contexts/tree.tsx:17-47](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/tree.tsx#L17-L47)

| Context/Hook | Return Type | Purpose |
| --- | --- | --- |
| `TreeContext` | `TreeContextType` | Holds the active root node (`root`) and the complete page tree (`full`). |
| `PathContext` | `PageTree.Node[]` | Tracks the hierarchical node path leading to the current active pathname. |
| `useTreePath()` | `PageTree.Node[]` | Retrieves the current path node array from `PathContext`. |
| `useTreeContext()` | `TreeContextType` | Retrieves the active tree context, throwing an error if uninitialized. |

Sources: [packages/radix-ui/src/contexts/tree.tsx:9-12](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/tree.tsx#L9-L12), [packages/radix-ui/src/contexts/tree.tsx:15-15](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/tree.tsx#L15-L15), [packages/radix-ui/src/contexts/tree.tsx:49-57](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/tree.tsx#L49-L57)

> [!WARNING]
> `useTreeContext` throws an explicit error if invoked outside of a component wrapped under `<DocsLayout />`.

Sources: [packages/radix-ui/src/contexts/tree.tsx:53-57](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/contexts/tree.tsx#L53-L57)

## Sidebar Node Rendering and Navigation

### Recursive Folder Hierarchies and Active States

The `createPageTreeRenderer` function generates React component trees from page tree structures by recursively traversing nodes via `renderList` and `PageTreeNode`. It evaluates each node type—separator, folder, or page item—and dispatches rendering to either custom user-supplied components or default UI primitives.

```tsx
export function createPageTreeRenderer({
  SidebarFolder,
  SidebarFolderContent,
  SidebarFolderLink,
  SidebarFolderTrigger,
  SidebarSeparator,
  SidebarItem,
}: InternalComponents) {
  function renderList(nodes: PageTree.Node[]) {
    return nodes.map((node, i) => <PageTreeNode key={i} node={node} />);
  }

  function PageTreeNode({ node }: { node: PageTree.Node }) {
    const { Separator, Item, Folder, pathname } = use(RendererContext)!;

    if (node.type === 'separator') {
      if (Separator) return <Separator item={node} />;
      return (
        <SidebarSeparator>
          {node.icon}
          {node.name}
        </SidebarSeparator>
      );
    }

    if (node.type === 'folder') {
      // eslint-disable-next-line react-hooks/rules-of-hooks -- assume node type unchanged
      const path = useTreePath();
      if (Folder) return <Folder item={node}>{renderList(node.children)}</Folder>;

      return (
        <SidebarFolder
          collapsible={node.collapsible}
          active={path.includes(node)}
          defaultOpen={node.defaultOpen}
        >
          {node.index ? (
            <SidebarFolderLink
              href={node.index.url}
              active={isActive(node.index.url, pathname)}
              external={node.index.external}
            >
              {node.icon}
              {node.name}
            </SidebarFolderLink>
          ) : (
            <SidebarFolderTrigger>
              {node.icon}
              {node.name}
            </SidebarFolderTrigger>
          )}
          <SidebarFolderContent>{renderList(node.children)}</SidebarFolderContent>
        </SidebarFolder>
      );
    }

    if (Item) return <Item item={node} />;
    return (
      <SidebarItem
        href={node.url}
        external={node.external}
        active={isActive(node.url, pathname)}
        icon={node.icon}
      >
        {node.name}
      </SidebarItem>
    );
  }
```
Sources: [packages/radix-ui/src/components/sidebar/page-tree.tsx:31-98](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/sidebar/page-tree.tsx#L31-L98)

Active states for individual items and index links are determined by comparing their target URLs against the current router pathname using the `isActive` utility. For folders, the `useTreePath` hook checks whether the folder is included in the current branch path to set expansion and highlight states.

```tsx
if (node.type === 'folder') {
  // eslint-disable-next-line react-hooks/rules-of-hooks -- assume node type unchanged
  const path = useTreePath();
  if (Folder) return <Folder item={node}>{renderList(node.children)}</Folder>;

  return (
    <SidebarFolder
      collapsible={node.collapsible}
      active={path.includes(node)}
      defaultOpen={node.defaultOpen}
    >
      {node.index ? (
        <SidebarFolderLink
          href={node.index.url}
          active={isActive(node.index.url, pathname)}
          external={node.index.external}
        >
          {node.icon}
          {node.name}
        </SidebarFolderLink>
      ) : (
        <SidebarFolderTrigger>
          {node.icon}
          {node.name}
        </SidebarFolderTrigger>
      )}
      <SidebarFolderContent>{renderList(node.children)}</SidebarFolderContent>
    </SidebarFolder>
  );
}

if (Item) return <Item item={node} />;
return (
  <SidebarItem
    href={node.url}
    external={node.external}
    active={isActive(node.url, pathname)}
    icon={node.icon}
  >
    {node.name}
  </SidebarItem>
);
```
Sources: [packages/radix-ui/src/components/sidebar/page-tree.tsx:56-97](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/sidebar/page-tree.tsx#L56-L97)

> [!NOTE]
> `RendererContext` caches the active pathname alongside custom component overrides using `useMemo`, ensuring minimal re-renders during client-side navigation.

Sources: [packages/radix-ui/src/components/sidebar/page-tree.tsx:14-19](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/sidebar/page-tree.tsx#L14-L19), [packages/radix-ui/src/components/sidebar/page-tree.tsx:109-114](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/sidebar/page-tree.tsx#L109-L114)

### Tab Partitioning and Folder Scanning

The `getSidebarTabs` utility extracts layout tabs from root page trees by scanning folders recursively. It identifies root-level sections and builds a collection of valid navigation endpoints.

```tsx
export function getSidebarTabs(
  tree: PageTree.Root,
  { transform = defaultTransform }: GetSidebarTabsOptions = {},
): SidebarTab[] {
  const results: SidebarTab[] = [];

  function scanOptions(node: PageTree.Root | PageTree.Folder, unlisted?: boolean) {
    if ('root' in node && node.root) {
      const urls = getFolderUrls(node);

      if (urls.size > 0) {
        const option: SidebarTab = {
          url: urls.values().next().value ?? '',
          title: node.name,
          icon: node.icon,
          unlisted,
          description: node.description,
          urls,
        };

        const mapped = transform ? transform(option, node) : option;
        if (mapped) results.push(mapped);
      }
    }

    for (const child of node.children) {
      if (child.type === 'folder') scanOptions(child, unlisted);
    }
  }

  scanOptions(tree);
  if (tree.fallback) scanOptions(tree.fallback, true);

  return results;
}
```
Sources: [packages/base-ui/src/components/sidebar/tabs/index.tsx:23-57](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/sidebar/tabs/index.tsx#L23-L57)

| Helper Function | Input Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `getSidebarTabs` | `tree: PageTree.Root, options?: GetSidebarTabsOptions` | `SidebarTab[]` | Scans root nodes and fallback trees to construct partition tabs. |
| `getFolderUrls` | `folder: PageTree.Folder, output?: Set<string>` | `Set<string>` | Collects all non-external page URLs and folder index links within a branch. |

Sources: [packages/base-ui/src/components/sidebar/tabs/index.tsx:23-26](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/sidebar/tabs/index.tsx#L23-L26), [packages/base-ui/src/components/sidebar/tabs/index.tsx:59-67](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/sidebar/tabs/index.tsx#L59-L67)

> [!WARNING]
> If a scanned folder contains no resolvable child pages or index URLs, it is omitted from the resulting sidebar tabs array.

Sources: [packages/base-ui/src/components/sidebar/tabs/index.tsx:30-45](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/sidebar/tabs/index.tsx#L30-L45)

### Sequential Footer Item Navigation

The `useFooterItems` hook produces a flattened, linear array of page tree items suitable for generating footer pagination and sequential next/previous navigation links.

```typescript
export function useFooterItems(): PageTree.Item[] {
  const { root } = useTreeContext();
  const cached = footerCache.get(root);
  if (cached) return cached;

  const list: PageTree.Item[] = [];
  function onNode(node: PageTree.Node) {
    if (node.type === 'folder') {
      if (node.index) onNode(node.index);
      for (const child of node.children) onNode(child);
    } else if (node.type === 'page' && !node.external) {
      list.push(node);
    }
  }

  for (const child of root.children) onNode(child);
  footerCache.set(root, list);
  return list;
```
Sources: [packages/radix-ui/src/utils/use-footer-items.ts:10-27](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/utils/use-footer-items.ts#L10-L27)

During traversal, `onNode` processes folders by recursively inspecting folder index nodes and child elements, while filtering out external links and non-page entries before pushing items onto the linear list.

```typescript
function onNode(node: PageTree.Node) {
  if (node.type === 'folder') {
    if (node.index) onNode(node.index);
    for (const child of node.children) onNode(child);
  } else if (node.type === 'page' && !node.external) {
    list.push(node);
  }
}
```
Sources: [packages/radix-ui/src/utils/use-footer-items.ts:16-23](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/utils/use-footer-items.ts#L16-L23)

> [!TIP]
> `useFooterItems` caches generated item lists per root reference using a `WeakMap`, avoiding redundant tree traversals across repeated hook calls.

Sources: [packages/radix-ui/src/utils/use-footer-items.ts:5-5](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/utils/use-footer-items.ts#L5-L5), [packages/radix-ui/src/utils/use-footer-items.ts:12-13](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/utils/use-footer-items.ts#L12-L13), [packages/radix-ui/src/utils/use-footer-items.ts:26-26](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/utils/use-footer-items.ts#L26-L26)

## Related

- [Source Loader](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/source-loader)
- [Shared Layout Architecture](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/shared-layout-architecture)


## Sitemap

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