Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Page Tree Generation

Last updated Oct 12, 2026100% grounded
Relevant source files

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

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, packages/core/src/source/loader.ts:1-15

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

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

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

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, packages/core/src/page-tree/utils.ts:24-33, packages/core/src/page-tree/utils.ts:105-119

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.

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

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.

  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

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.

  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

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

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.

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

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.

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

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, packages/core/src/page-tree/utils.ts:214-219

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.

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

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.

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

Utility FunctionParametersReturn TypeDescription
flattenTreenodes: PageTree.Node[]PageTree.Item[]Flattens folders, index nodes, and pages into a single sequential list.
findNeighbourtree: PageTree.Root, url: string, options?: { separateRoot?: boolean }{ previous?: PageTree.Item, next?: PageTree.Item }Resolves adjacent previous and next page items for pagination controls.
getPageTreeRootspageTree: PageTree.Root | PageTree.Folder(PageTree.Root | PageTree.Folder)[]Collects all root nodes and nested root folders.
getPageTreePeerstreeOrTrees: PageTree.Root | Record<string, PageTree.Root>, url: stringPageTree.Item[]Retrieves peer page nodes residing under the same parent container.
findSiblingstreeOrTrees: PageTree.Root | Record<string, PageTree.Root>, url: stringPageTree.Node[]Retrieves all sibling tree nodes under the same parent.
findParentfrom: PageTree.Root | PageTree.Folder, url: stringPageTree.Root | PageTree.Folder | undefinedLocates the parent container of a page matched by its URL.
findPathnodes: PageTree.Node[], matcher: (node: PageTree.Node) => boolean, options?: { includeSeparator?: boolean }PageTree.Node[] | nullFinds the array of nodes forming the path to a matched node.

Sources: packages/core/src/page-tree/utils.ts:6-166

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.

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

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.

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

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

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.

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

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.

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

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

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

export interface SerializedPageTree {
  $fumadocs_loader: 'page-tree';
  data: object;
}

Sources: packages/core/src/source/client/index.tsx:5-8

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

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

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

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

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

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

Context/HookReturn TypePurpose
TreeContextTreeContextTypeHolds the active root node (root) and the complete page tree (full).
PathContextPageTree.Node[]Tracks the hierarchical node path leading to the current active pathname.
useTreePath()PageTree.Node[]Retrieves the current path node array from PathContext.
useTreeContext()TreeContextTypeRetrieves the active tree context, throwing an error if uninitialized.

Sources: packages/radix-ui/src/contexts/tree.tsx:9-12, packages/radix-ui/src/contexts/tree.tsx:15-15, packages/radix-ui/src/contexts/tree.tsx:49-57

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

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.

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

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.

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

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, packages/radix-ui/src/components/sidebar/page-tree.tsx:109-114

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.

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

Helper FunctionInput ParametersReturn TypeDescription
getSidebarTabstree: PageTree.Root, options?: GetSidebarTabsOptionsSidebarTab[]Scans root nodes and fallback trees to construct partition tabs.
getFolderUrlsfolder: 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, packages/base-ui/src/components/sidebar/tabs/index.tsx:59-67

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

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

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

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.

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

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, packages/radix-ui/src/utils/use-footer-items.ts:12-13, packages/radix-ui/src/utils/use-footer-items.ts:26-26

Was this page helpful?