Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

BaseHub Integration

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

The @fumadocs/basehub package provides official integration tools to connect BaseHub content services with Fumadocs documentation projects. It exports table of contents parsers and utilities that bridge BaseHub data structures with core Fumadocs source loader conventions. Sources: packages/basehub/package.json:1-48, packages/basehub/src/toc.tsx:1-49

Package Architecture and Dependencies

Manifest Configuration

The @fumadocs/basehub package configures its build output as an ECMAScript module (type: "module") with public publishing access. Its exports map entry points standardizing consumer imports, exposing the table of contents module at ./dist/toc.mjs and the package manifest at ./package.json.

Export PathTarget FilePurpose
../dist/toc.mjsMain compiled entry point for table of contents parsing utilities
./package.json./package.jsonPackage metadata definition for module resolution

Sources: packages/basehub/package.json:1-23

Development and Build Scripts

The package uses tsdown driven by modern build pipelines and oxlint for linting. The build configuration leverages workspace dependencies for shared tooling and core Fumadocs packages.

Script NameCommandPurpose
buildtsdownCompiles source files into distribution artifacts
cleanrimraf distRemoves built distribution files
devtsdown --watch --clean falseRuns the build tool in continuous watch mode
lintoxlint .Executes linter checks across the package
types:checktsc --noEmitVerifies TypeScript type correctness without emitting outputs

Sources: packages/basehub/package.json:24-30

Dependencies and Peer Relationships

Dependencies are split between build-time development tools and runtime peer requirements. The package relies on basehub for upstream API definitions and fumadocs-core for structural table of contents types.

Dependency NameVersion SpecifierDependency TypePurpose
@types/node25.9.2devDependenciesTypeScript definitions for Node.js runtime APIs
@types/react^19.2.17 / *devDependencies / peerDependenciesTypeScript definitions and types for React components
basehub9.6.0 / 9.x.xdevDependencies / peerDependenciesUpstream BaseHub client and API transaction types
fuma-cli^0.1.1devDependenciesCLI toolset for Fuma workspace utilities
fumadocs-coreworkspace:* / ^16.8.0devDependencies / peerDependenciesCore Fumadocs framework utilities and TOC types
fumadocs-uiworkspace:*devDependenciesUI component library for Fumadocs themes
react^19.2.7 / ^19.2.0devDependencies / peerDependenciesReact library runtime dependency
tsconfigworkspace:*devDependenciesShared workspace TypeScript configurations
tsdown0.22.2devDependenciesBundling tool built on esbuild
typescript^6.0.3devDependenciesTypeScript compiler

Sources: packages/basehub/package.json:31-48

Note

The @types/react peer dependency is marked as optional in peerDependenciesMeta, allowing consuming projects flexibility in their React typing setups while enforcing the basehub 9.x.x and fumadocs-core ^16.8.0 runtime contracts. Sources: packages/basehub/package.json:43-53

Table of Contents Transformation

Options Configuration and Interface

The parseToc function extracts table of contents entries from BaseHub rich-text structures. It accepts an options object conforming to the ParseTOCOptions interface, which defines the input node structures, starting nesting level, and a rendering callback for rich text nodes.

Option PropertyTypeDefault ValueDescription
nodeRichTextTocNode | RichTextTocNode[]none (required)Root AST node or array of nodes representing the table of contents data from BaseHub
initialLevelnumber0Base heading depth offset applied during recursive tree traversal
render(node: RichTextNode) => ReactNodenone (required)Callback function responsible for rendering rich text child nodes into React nodes

Sources: packages/basehub/src/toc.tsx:5-12

export interface ParseTOCOptions {
  node: RichTextTocNode | RichTextTocNode[];
  /** @default 0 */
  initialLevel?: number;
 
  /** render rich text node */
  render: (node: RichTextNode) => ReactNode;
}

Sources: packages/basehub/src/toc.tsx:5-12

Recursive Traversal and Node Processing

The traversal algorithm processes root nodes, accepting either a single RichTextTocNode or an array of nodes. It invokes an internal next function recursively, incrementing levels when encountering list items or ordered lists while ignoring nodes without content or text types.

export function parseToc(options: ParseTOCOptions): TOCItemType[] {
  const { node: root, render, initialLevel = 0 } = options;
  const results: TOCItemType[] = [];
 
  function next(node: RichTextTocNode, level = initialLevel) {
    if (node.type === 'text' || !node.content) return;
 
    for (const item of node.content) {
      if (item.type === 'orderedList' || item.type === 'listItem') {
        next(item, level + 1);
        continue;
      }
 
      if (item.type !== 'paragraph' || !item.content || item.content.length === 0) continue;
 
      for (const text of findTextNode(item.content[0])) {
        const mark = text.marks?.find((m) => m.type === 'link');
        if (!mark) continue;
 
        results.push({
          depth: level + 1,
          url: mark.attrs.href,
          title: render({
            ...text,
            marks: text.marks?.filter((m) => m.type !== 'link'),
          } as RichTextNode),
        });
      }
    }
  }
 
  if (Array.isArray(root)) for (const item of root) next(item);
  else next(root);
 
  return results;
}

Sources: packages/basehub/src/toc.tsx:14-49

Warning

Paragraph items without link marks are skipped during tree traversal. Each target TOC item requires a valid link mark inside its text node structure to supply the url attribute for Fumadocs navigation. Sources: packages/basehub/src/toc.tsx:27-35

Text Node Extraction and Filtering

Text nodes are flattened using the findTextNode helper utility, which recursively unpacks nested node content into an array of text leaf nodes. The parser inspects the marks of each extracted text node for link types, strips the link mark before passing the node to the render callback, and constructs the final TOCItemType entry with the calculated depth and anchor URL.

function findTextNode(n: RichTextTocNode): Extract<RichTextTocNode, { type: 'text' }>[] {
  if (n.type === 'text') {
    return [n];
  }
 
  return n.content?.flatMap(findTextNode) ?? [];
}

Sources: packages/basehub/src/toc.tsx:51-57

Tip

When filtering marks during title rendering, parseToc removes only the link mark type from the text node so that other formatting marks like bold or italic remain intact for the rendering callback. Sources: packages/basehub/src/toc.tsx:36-40

Rich Text Structure Mapping

AST Node Parsing and Flattening

The extraction of table of contents entries from BaseHub abstract syntax trees relies on recursive AST node traversal and flattening helpers. The findTextNode function processes incoming RichTextTocNode items, checking if an individual node matches the text type or traversing nested content arrays via flatMap.

function findTextNode(n: RichTextTocNode): Extract<RichTextTocNode, { type: 'text' }>[] {
  if (n.type === 'text') {
    return [n];
  }
 
  return n.content?.flatMap(findTextNode) ?? [];
}

Sources: packages/basehub/src/toc.tsx:51-57

Note

When findTextNode encounters a node without a content property or a non-text node lacking nested children, it safely returns an empty array to prevent runtime traversal errors during deep AST recursion. Sources: packages/basehub/src/toc.tsx:56-56

Heading Hierarchy and Depth Resolution

The parser builds heading depth hierarchies by tracking nesting levels across ordered lists, list items, and paragraph blocks. The parseToc routine handles root inputs as either single node objects or arrays, iterating through child collections to assign appropriate depth offsets.

AST Node Type / CheckConditionAction Taken
Array.isArray(root)Root input is an arrayIterates over each item, invoking next(item)
node.type === 'text' || !node.contentLeaf node or empty containerTerminates current traversal branch immediately
item.type === 'orderedList' || item.type === 'listItem'List container or list itemRecursively invokes next(item, level + 1)
item.type !== 'paragraph' || !item.content || length === 0Non-paragraph or empty blockSkips item iteration via continue

Sources: packages/basehub/src/toc.tsx:18-28

function next(node: RichTextTocNode, level = initialLevel) {
  if (node.type === 'text' || !node.content) return;
 
  for (const item of node.content) {
    if (item.type === 'orderedList' || item.type === 'listItem') {
      next(item, level + 1);
      continue;
    }
 
    if (item.type !== 'paragraph' || !item.content || item.content.length === 0) continue;
 

Sources: packages/basehub/src/toc.tsx:18-28

Warning

Paragraph blocks that contain empty content arrays are bypassed during text extraction. The parser explicitly verifies that item.content.length > 0 before attempting to access nested text children. Sources: packages/basehub/src/toc.tsx:27-27

Source Loader Integration Patterns

Content Source Adapters and Collection Stores

Fumadocs manages remote headless CMS queries and static document files by bridging collection adapters into standardized source trees. The toFumadocsSource helper function transforms active file collection stores—specifically MDX stores and metadata stores—into a uniform StaticSource structure containing explicit page and metadata file entries.

export function toFumadocsSource<
  Mdx extends MDXStoreData<PageData> | MDXStoreLazyData<PageData, unknown> = MDXStoreData<
    PageData,
    unknown
  >,
  Meta extends { data: MetaData } = { data: MetaData },
>(mdxStore?: FileCollectionStore<Mdx>, metaStore?: FileCollectionStore<Meta>) {
  const out: StaticSource<{
    pageData: ToPageData<Mdx>;
    metaData: Meta['data'];
  }> = { files: [] };
  for (const page of mdxStore?.list() ?? []) {
    out.files.push({
      type: 'page',
      data: ('compiled' in page
        ? { ...page, ...page.compiled.frontmatter }
        : { ...page, ...page.frontmatter }) as unknown as ToPageData<Mdx>,
      path: page.path,
      absolutePath: page.fullPath,
    });
  }
  for (const file of metaStore?.list() ?? []) {
    out.files.push({
      type: 'meta',
      data: file.data,
      path: file.path,
      absolutePath: file.fullPath,
    });
  }
  return out;
}

Sources: packages/content/src/runtime.ts:12-42

Note

During collection store iteration, toFumadocsSource inspects page objects for a compiled property, merging frontmatter attributes from either page.compiled.frontmatter or page.frontmatter depending on the compilation state of the underlying store item. Sources: packages/content/src/runtime.ts:26-31

Dynamic Sanity and Content Collection Routing

Headless CMS platforms like Sanity map remote document structures to virtual file paths by resolving document slugs and types through client fetch options. The createSanitySource function queries documents filtered by document type, constructs filesystem-like paths via generatePath or slug arrays, and attaches lazy loading handlers and structured data extraction routines to each page record.

export function createSanitySource<Doc extends BaseDoc>(
  options: SanityOptions<Doc>,
): DynamicSource<{
  pageData: DocToPage<Doc>;
  metaData: MetaData;
}> {
  const { docType, baseDir, generatePath } = options;
  let sanityFetch: <R = unknown>(query: string, params?: QueryParams) => Promise<R>;
 
  if ('sanityFetch' in options) {
    const fn = options.sanityFetch;
    sanityFetch = async (query, params) => {
      const res = await fn({ query, params });
      return res.data as never;
    };
  } else {
    const client = options.client;
    sanityFetch = client.fetch.bind(client);
  }
 
  return {
    async files() {
      const data = await sanityFetch<ShallowDoc<Doc>[]>(
        `*[_type == $docType]{ _id, _type, title, slug, description }`,
        {
          docType,
        },
      );
 
      return data.map((file) => {
        const slugs = file.slug?.current?.split('/').filter((v) => v.length > 0) ?? [];
        let filePath = generatePath
          ? generatePath(file)
          : slugs.length === 0
            ? 'index.mdx'
            : `${slugs.join('/')}.mdx`;
        if (baseDir) filePath = path.join(baseDir, filePath);
 
        return {
          type: 'page',
          data: {
            ...file,
            title: file.title ?? file._id,
            async load() {
              const data = await sanityFetch<Doc & { _toc?: PortableTextBlock[] }>(
                `*[_type == $docType && _id == $id][0]{
                  ...,
                  "_toc": body[style in ["h1", "h2", "h3", "h4", "h5", "h6"]]
                }`,
                {
                  id: file._id,
                  docType,
                },
              );
 
              return {
                ...data,
                _toc: data._toc ?? [],
                renderToc(opts) {
                  if (!data._toc) return [];
                  return renderToc({ toc: data._toc, ...opts });
                },
              };
            },
            async structuredData() {
              const data = await sanityFetch<{ structuredBody: StructuredBlock[] } | undefined>(
                `*[_type == $docType && _id == $id][0]{
                  "structuredBody": body[]{
                    ...,
                    _type == "block" => {
                      "heading": select(
                        style == "h1" => "h1",
                        style == "h2" => "h2",
                        style == "h3" => "h3",
                        style == "h4" => "h4",
                        style == "h5" => "h5",
                        style == "h6" => "h6",
                        null
                      ),
                      "content": pt::text(@)

Sources: packages/sanity/src/index.ts:63-161

Configuration OptionTypePurpose
docTypestringCMS document type identifier for filtering queries
generatePath(doc: ShallowDoc<Doc>) => stringCustom virtual file path generator function
baseDirstringBase directory prefix applied to virtual file paths
clientSanityClientDirect Sanity client instance for fetching queries
sanityFetchDefinedFetchTypeLive or custom fetch handler for Next.js Sanity integrations

Sources: packages/sanity/src/index.ts:16-36

Tip

When slug arrays evaluate to an empty collection during Sanity source ingestion, the loader defaults the virtual file path to index.mdx before applying any configured baseDir offsets. Sources: packages/sanity/src/index.ts:93-99

Page Tree and Navigation Synthesis

Fumadocs builds sequential page navigation and linear document flows by traversing root and folder nodes in the standard page tree structure. The useFooterItems utility accesses the active page tree context, walking down folder collections to extract inner index pages and child items while systematically ignoring external links and non-page nodes.

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/base-ui/src/utils/use-footer-items.ts:10-27

Note

Performance is optimized across tree rendering cycles by caching computed footer lists in a WeakMap keyed directly against the PageTree.Root or PageTree.Folder instance. Sources: packages/base-ui/src/utils/use-footer-items.ts:5-13

Layout configuration functions assemble complete navigation models by merging global application settings with dynamic source loaders. The layoutConfig module initializes documentation layouts by retrieving generated page trees from source loaders and injecting them alongside custom base navigation branding and logo elements.

async docs(): Promise<DocsLayoutProps> {
  const config = await getConfigRuntime();
  const source = await getSource(config);
  return {
    tree: source.getPageTree(),
    ...(await this.base()),
  };
},

Sources: packages/preview/src/layouts/config.tsx:31-38

Layout PropertyReturn TypePurpose
base()Promise<BaseLayoutProps>Resolves default navigation branding and logo parameters
docs()Promise<DocsLayoutProps>Combines source page trees with base layout configurations
home()Promise<HomeLayoutProps>Returns landing page configuration settings

Sources: packages/preview/src/layouts/config.tsx:13-41

Warning

When traversing folder nodes during footer synthesis, the recursive walker checks for the existence of node.index before iterating over node.children, ensuring index documents are always positioned ahead of standard sibling entries. Sources: packages/base-ui/src/utils/use-footer-items.ts:17-19

Was this page helpful?