Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Sanity CMS Integration

Last updated Oct 12, 2026100% grounded
<details>
<summary>Relevant source files</summary>

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

- [packages/sanity/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/index.ts)
- [packages/sanity/src/client.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/client.ts)
- [packages/content/src/runtime.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content/src/runtime.ts)
- [packages/preview/src/pages/...slugs.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx)
- [packages/sanity/registry/steps.component.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/steps.component.tsx)
- [packages/content/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content/src/index.ts)
- [packages/sanity/registry/tabs.component.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/tabs.component.tsx)
- [packages/sanity/registry/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/index.ts)
- [packages/core/src/source/client/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx)
- [packages/shadcn/src/rsc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/shadcn/src/rsc.tsx)
- [packages/sanity/registry/files.component.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/files.component.tsx)
- [packages/sanity/registry/accordion.component.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/accordion.component.tsx)
- [packages/preview/src/lib/source/storage.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/storage.ts)
- [packages/sanity/registry/base.component.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/base.component.tsx)
- [packages/asyncapi/src/ui/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/ui/index.tsx)
- [packages/openapi/src/ui/base.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/ui/base.tsx)
- [packages/mdx/src/runtime/server.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/runtime/server.ts)
- [packages/sanity/package.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/package.json)
- [packages/asyncapi/src/utils/pages/to-text.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/utils/pages/to-text.ts)
- [packages/content-collections/src/configuration.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content-collections/src/configuration.ts)
- [packages/core/src/mdx-plugins/remark-structure.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-structure.ts)
- [packages/content-collections/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content-collections/src/index.ts)
- [packages/openapi/src/utils/pages/to-text.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/pages/to-text.ts)
- [packages/preview/src/lib/source/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/index.ts)
- [packages/preview/src/components/provider.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/provider.tsx)
- [packages/base-ui/src/layouts/flux/page/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/flux/page/index.tsx)
- [packages/base-ui/src/layouts/notebook/page/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/layouts/notebook/page/index.tsx)
- [packages/cli/src/commands/file-tree.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/cli/src/commands/file-tree.ts)
- [packages/radix-ui/src/layouts/flux/page/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/flux/page/index.tsx)
- [packages/radix-ui/src/layouts/notebook/page/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/layouts/notebook/page/index.tsx)
</details>

## Overview

The Sanity CMS Integration package (`@fumadocs/sanity`) connects documents and Portable Text blocks stored in Sanity CMS to Fumadocs source abstractions and UI components. It provides a dynamic source factory that queries documents, constructs virtual file paths, and maps structured data and [table of contents](/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/table-of-contents) trees for documentation projects.

Sources: [packages/sanity/src/index.ts:1-161](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/index.ts#L1-L161), [packages/sanity/package.json:1-73](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/package.json#L1-L73)

## Sanity Source Factory Architecture

### Factory Contracts and Options

The `createSanitySource` function instantiates a dynamic [source loader](/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/source-loader) for Fumadocs by accepting either a generic Sanity client configuration or a `next-sanity` live fetcher. Document types must conform to `BaseDoc`, which defines the mandatory identifiers and optional metadata fields.

| Option Key | Type | Description |
| :--- | :--- | :--- |
| `docType` | `string` | Document type matching the `_type` field in Sanity CMS. |
| `generatePath` | `(doc: ShallowDoc<Doc>) => string` | Optional custom generator for virtual file paths. |
| `baseDir` | `string` | Base directory prefix applied to all virtual file paths. |
| `client` | `SanityClient` | An instance of the official Sanity client. |
| `sanityFetch` | `DefinedFetchType` | Live query fetcher provided by `next-sanity`. |

Sources: [packages/sanity/src/index.ts:12-36](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/index.ts#L12-L36), [packages/sanity/src/index.ts:63-82](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/index.ts#L63-L82)

> [!WARNING]
> When configuring `NextSanityOptions`, providing a `client` is optional if `sanityFetch` is supplied, but omitting both will fail query resolution during file discovery.

Sources: [packages/sanity/src/index.ts:33-36](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/index.ts#L33-L36), [packages/sanity/src/index.ts:72-81](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/index.ts#L72-L81)

### Virtual Path Resolution and Data Loading

The source factory queries shallow document summaries using GROQ filters to construct virtual file paths and lazy-load full document payloads. Slugs are parsed by splitting current slug strings on forward slashes, defaulting root entries to `index.mdx`.

\`\`\`typescript
export function createSanitySource<Doc extends BaseDoc>(
  options: SanityOptions<Doc>,
): DynamicSource<{
  pageData: DocToPage<Doc>;
  metaData: MetaData;
}> {
  // ...
}
\`\`\`

Sources: [packages/sanity/src/index.ts:63-100](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/index.ts#L63-L100)

> [!TIP]
> The `load` function fetches complete document bodies and extracts heading blocks matching styles `h1` through `h6` into `_toc`, while `structuredData` compiles text nodes for search indexing.

Sources: [packages/sanity/src/index.ts:106-153](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/index.ts#L106-L153)

## Portable Text Client and Navigation Parsing

### Table of Contents Generation from Portable Text Blocks

The package provides client utilities to process Portable Text blocks and construct navigation structures compatible with Fumadocs core types. The `renderToc` function iterates over an array of `PortableTextBlock` items, transforming heading elements into standard `TOCItemType` entries for sidebar or floating table of contents renderers.

\`\`\`typescript
export function renderToc(opts: {
  toc: PortableTextBlock[];
  render: (body: PortableTextBlock) => ReactNode;
}): TOCItemType[] {
  const { render, toc } = opts;

  return toc.map(
    (item): TOCItemType => ({
      depth: Number(item.level ?? 0),
      title: render({ ...item, style: undefined }),
      url: `#${item._key}`,
    }),
  );
}
\`\`\`

Sources: [packages/sanity/src/client.ts:5-18](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/client.ts#L5-L18)

| Property | Expression / Transformation | Target Type |
| :--- | :--- | :--- |
| `depth` | `Number(item.level ?? 0)` | `number` |
| `title` | `render({ ...item, style: undefined })` | `ReactNode` |
| `url` | `#${item._key}` | `string` |

Sources: [packages/sanity/src/client.ts:11-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/client.ts#L11-L17)

> [!NOTE]
> When rendering table of contents items, `renderToc` clones each block item with its `style` property stripped to prevent duplicate heading styles while generating anchor targets from the Sanity document key (`_key`).

Sources: [packages/sanity/src/client.ts:13-16](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/client.ts#L13-L16)

### Page Tree Deserialization and Hook Utilities

Client-side integration also handles page tree hydration via `useFumadocsLoader` and `deserializePageTree`. These utilities inspect serialized page trees marked with the `$fumadocs_loader` identifier and inject raw HTML into tree node icons and item names using `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)

> [!IMPORTANT]
> The `deserializePageTree` function visits every node in the tree, checking whether `icon` or `name` properties contain string values, and wraps them in `<span>` elements with inner HTML bindings.

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)

## Core Element and Admonition Components

### Portable Text Node Renderers and Block Configurations

Fumadocs provides React component definitions and type interfaces to render Sanity Portable Text blocks into standard Fumadocs UI elements. The `baseBlocks` record maps block styles to heading and paragraph components, where `baseHeading` renders elements from `h1` through `h6` using `Heading` components from `fumadocs-ui/components/heading` with IDs derived from block keys (`_key`). Inline paragraphs are wrapped in `<span>` elements, while block-level paragraphs render as ``.

\`\`\`tsx
const baseHeading: PortableTextBlockComponent = (props) => {
  return (
    <Heading id={props.value._key} as={props.value.style as 'h1'}>
      {props.children}
    </Heading>
  );
};

export const baseBlocks: Record<
  'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'normal',
  PortableTextBlockComponent
> = {
  normal(props) {
    return props.isInline ? <span>{props.children}</span> : <p>{props.children}</p>;
  },
  h1: baseHeading,
  h2: baseHeading,
  h3: baseHeading,
  h4: baseHeading,
  h5: baseHeading,
  h6: baseHeading,
};
\`\`\`

Sources: [packages/sanity/registry/base.component.tsx:14-35](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/base.component.tsx#L14-L35)

| Block Style / Type | Target UI Component | Key Properties |
| :--- | :--- | :--- |
| `h1` – `h6` | `Heading` (`fumadocs-ui/components/heading`) | `id={props.value._key}`, `as={props.value.style}` |
| `normal` | `span` or `p` | `isInline` conditional wrapping |
| `code` | `DynamicCodeBlock` (`fumadocs-ui/components/dynamic-codeblock`) | `lang={props.value.language ?? 'text'}`, `code={props.value.code ?? ''}` |
| `callout` | `Callout` (`fumadocs-ui/components/callout`) | `title`, `type` |
| `cards` / `card` | `Cards` / `Card` (`fumadocs-ui/components/card`) | `title`, `href`, nested body blocks |
| `links` (mark) | `Link` (`fumadocs-core/link`) | `href={props.value.href}` |

Sources: [packages/sanity/registry/base.component.tsx:14-35](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/base.component.tsx#L14-L35), [packages/sanity/registry/base.component.tsx:77-114](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/base.component.tsx#L77-L114), [packages/sanity/registry/base.component.tsx:117-124](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/base.component.tsx#L117-L124)

> [!NOTE]
> The `renderInlines` and `renderBlocks` helper functions recursively process nested Portable Text arrays, passing `isInline: true` or `isInline: false` down to the `@portabletext/react` node renderer context.

Sources: [packages/sanity/registry/base.component.tsx:63-69](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/base.component.tsx#L63-L69)

### Specialized Component Values and Mark Definitions

Custom types like code blocks, callouts, and cards are strongly typed via TypeScript interfaces that define the expected Sanity document structures. The `CodeValue` interface accepts an optional `language` and `code` string, defaulting to `'text'` and empty strings respectively when rendered inside `DynamicCodeBlock`.

\`\`\`tsx
export interface CodeValue {
  _type: 'code';
  language?: string;
  code?: string;
}

export interface CalloutValue {
  _type: 'callout';
  title?: PortableTextBlock[];
  body?: PortableTextBlock[];
  type?: CalloutType;
}

export interface CardsValue {
  _type: 'cards';
  items?: CardValue[];
}

export interface CardValue {
  _type: 'card';
  _key?: string;
  title?: PortableTextBlock[];
  body?: PortableTextBlock[];
  url?: string;
}
\`\`\`

Sources: [packages/sanity/registry/base.component.tsx:37-61](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/base.component.tsx#L37-L61)

> [!WARNING]
> When rendering individual `card` items within a `cards` container, fallback keys use index iteration (`i`) only if item key (`_key`) is undefined on the incoming block value.

Sources: [packages/sanity/registry/base.component.tsx:93-95](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/base.component.tsx#L93-L95)

## Interactive Layout and Container Components

### Multi-Step Flows and Procedural Steps

Fumadocs processes Sanity Portable Text blocks to render multi-step guides and sequential procedures via `stepsComponents`. The `StepValue` interface defines individual step items containing an optional `_key` and an array of Portable Text body blocks (`body`), while `StepsValue` defines a collection container holding multiple `StepValue` items.

\`\`\`tsx
export interface StepValue {
  _key?: string;
  _type: 'step';
  body?: PortableTextBlock[];
}

export interface StepsValue {
  _type: 'steps';
  items?: StepValue[];
}
\`\`\`

Sources: [packages/sanity/registry/steps.component.tsx:8-17](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/steps.component.tsx#L8-L17)

The `stepsComponents` record maps the `step` and `steps` Sanity types to `Step` and `Steps` components from `fumadocs-ui/components/steps`. When rendering a standalone `step`, the renderer invokes `renderBlocks` on `value.body` and wraps the output in a single `<Step>` component. The `steps` renderer maps over `value.items`, generating `<Step>` elements for each item with keys falling back from `item._key` to array indices `index`.

\`\`\`tsx
export const stepsComponents: {
  step: PortableTextTypeComponent<StepValue>;
  steps: PortableTextTypeComponent<StepsValue>;
} = {
  step({ value, renderNode }) {
    return <Step>{renderBlocks(value.body, renderNode)}</Step>;
  },
  steps({ value, renderNode }) {
    return (
      <Steps>
        {value.items?.map((item, index) => (
          <Step key={item._key ?? index}>{renderBlocks(item.body, renderNode)}</Step>
        ))}
      </Steps>
    );
  },
};
\`\`\`

Sources: [packages/sanity/registry/steps.component.tsx:25-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/steps.component.tsx#L25-L41)

> [!NOTE]
> The `renderBlocks` helper function iterates over `PortableTextBlock` arrays, invoking the `@portabletext/react` node renderer with `isInline: false` for every child block.

Sources: [packages/sanity/registry/steps.component.tsx:19-23](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/steps.component.tsx#L19-L23)

### Tab Panels and Content Switchers

Structured tab interfaces are generated from Sanity documents using the `tabsComponents` implementation. The `TabValue` interface accepts an optional `_key`, `title` string, and `body` blocks, whereas `TabsValue` aggregates these into an `items` array.

\`\`\`tsx
export interface TabValue {
  _key?: string;
  _type: 'tab';
  title?: string;
  body?: PortableTextBlock[];
}

export interface TabsValue {
  _type: 'tabs';
  items?: TabValue[];
}
\`\`\`

Sources: [packages/sanity/registry/tabs.component.tsx:8-18](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/tabs.component.tsx#L8-L18)

| Component Key | Target UI Component | Key Properties and Fallbacks |
| :--- | :--- | :--- |
| `tab` | Direct child renderer | Renders `value.body` via `renderBlocks` |
| `tabs` | `Tabs` (`fumadocs-ui/components/tabs`) | Wraps `TabsList`, `TabsTrigger`, and `Tab` elements |
| `TabsTrigger` | `TabsTrigger` (`fumadocs-ui/components/tabs`) | `value={item._key ?? i.toString()}`, title defaults to `Tab ${i + 1}` |
| `Tab` | `Tab` (`fumadocs-ui/components/tabs`) | `value={item._key ?? i.toString()}` |

Sources: [packages/sanity/registry/tabs.component.tsx:26-54](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/tabs.component.tsx#L26-L54)

> [!WARNING]
> If a `tabs` block contains an empty items array (`items.length === 0`), the renderer immediately returns `null` and renders no output markup.

Sources: [packages/sanity/registry/tabs.component.tsx:34-36](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/tabs.component.tsx#L34-L36)

### Accordion and Collapsible Groups

Accordions and expandable container blocks are structured via `accordionComponents`, supporting both single-item wrappers and multi-item collapsible groups. The `AccordionValue` interface supports an optional `title` array, `id`, `value`, and `body` blocks, while `AccordionsValue` accepts a `type` property constrained to `'single'` or `'multiple'`.

\`\`\`tsx
export interface AccordionValue {
  _key?: string;
  _type: 'accordion';
  title?: PortableTextBlock[];
  id?: string;
  value?: string;
  body?: PortableTextBlock[];
}

export interface AccordionsValue {
  _type: 'accordions';
  type?: 'single' | 'multiple';
  items?: AccordionValue[];
}
\`\`\`

Sources: [packages/sanity/registry/accordion.component.tsx:8-21](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/accordion.component.tsx#L8-L21)

The `accordionComponents` record delegates rendering to `Accordion` and `Accordions` from `fumadocs-ui/components/accordion`. A standalone `accordion` block wraps itself in an `<Accordions type="single">` container, whereas the `accordions` renderer applies `value.type` defaulting to `'single'` if undefined.

\`\`\`tsx
export const accordionComponents: {
  accordion: PortableTextTypeComponent<AccordionValue>;
  accordions: PortableTextTypeComponent<AccordionsValue>;
} = {
  accordion({ value, renderNode }) {
    return (
      <Accordions type="single">
        <Accordion
          title={value.title && renderBlocks(value.title, renderNode)}
          id={value.id}
          value={value.value ?? value._key}
        >
          {renderBlocks(value.body, renderNode)}
        </Accordion>
      </Accordions>
    );
  },
  accordions({ value, renderNode }) {
    return (
      <Accordions type={value.type ?? 'single'}>
        {value.items?.map((item) => (
          <Accordion
            key={item._key}
            title={item.title && renderBlocks(item.title, renderNode)}
            id={item.id}
            value={item.value ?? item._key}
          >
            {renderBlocks(item.body, renderNode)}
          </Accordion>
        ))}
      </Accordions>
    );
  },
\`\`\`

Sources: [packages/sanity/registry/accordion.component.tsx:29-61](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/accordion.component.tsx#L29-L61)

> [!TIP]
> Accordion item identifiers fall back from explicitly defined `value` properties to Sanity's `_key` property via `item.value ?? item._key`.

Sources: [packages/sanity/registry/accordion.component.tsx:38-40](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/accordion.component.tsx#L38-L40), [packages/sanity/registry/accordion.component.tsx:53-55](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/accordion.component.tsx#L53-L55)

## File Hierarchy and Virtual Tree Representation

Fumadocs provides directory tree visualization components through Sanity CMS adapters by integrating with `fumadocs-ui/components/files`. The registry definition for files specifies file and component targets under the `files` registry item.

\`\`\`typescript
  name: 'files',
  files: [
    {
      type: 'lib',
      path: 'files.ts',
      target: '<dir>/sanity/files.ts',
    },
    {
      type: 'components',
      path: 'files.component.tsx',
      target: '<dir>/sanity/files.tsx',
    },
  ],
},
\`\`\`

Sources: [packages/sanity/registry/index.ts:35-48](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/index.ts#L35-L48)

The underlying schema data types for file trees are defined in `packages/sanity/registry/files.component.tsx`. These interfaces capture individual files, nested folders with optional expansion state, and root file collections.

\`\`\`tsx
export interface FileValue {
  _key?: string;
  _type: 'file';
  name?: string;
}

export interface FolderValue {
  _key?: string;
  _type: 'folder';
  name?: string;
  defaultOpen?: boolean;
  items?: FileTreeItem[];
}

export interface FilesValue {
  _type: 'files';
  items?: FileTreeItem[];
}

type FileTreeItem = FileValue | FolderValue;
\`\`\`

Sources: [packages/sanity/registry/files.component.tsx:4-23](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/files.component.tsx#L4-L23)

| Interface Name | CMS Block Type (`_type`) | Key Properties | Target UI Component |
| :--- | :--- | :--- | :--- |
| `FileValue` | `file` | `_key`, `name` | `File` (`fumadocs-ui/components/files`) |
| `FolderValue` | `folder` | `_key`, `name`, `defaultOpen`, `items` | `Folder` (`fumadocs-ui/components/files`) |
| `FilesValue` | `files` | `items` | `Files` (`fumadocs-ui/components/files`) |

Sources: [packages/sanity/registry/files.component.tsx:4-23](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/files.component.tsx#L4-L23), [packages/sanity/registry/files.component.tsx:46-58](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/files.component.tsx#L46-L58)

The recursive helper function `renderFileTree` traverses arrays of `FileTreeItem` elements, resolving keys from `item._key` or falling back to the array index. When an item has `_type` set to `'folder'`, it renders a nested `<Folder>` component with its child items and propagates the `defaultOpen` property.

\`\`\`tsx
function renderFileTree(items: FileTreeItem[] | undefined) {
  return items?.map((item, index) => {
    const key = item._key ?? index;

    if (item._type === 'folder') {
      return (
        <Folder key={key} name={item.name ?? ''} defaultOpen={item.defaultOpen}>
          {renderFileTree(item.items)}
        </Folder>
      );
    }

    return <File key={key} name={item.name ?? ''} />;
  });
}
\`\`\`

Sources: [packages/sanity/registry/files.component.tsx:25-39](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/files.component.tsx#L25-L39)

> [!NOTE]
> If a folder or file name is undefined in the Sanity document payload, the component defaults the name property to an empty string `''`.

Sources: [packages/sanity/registry/files.component.tsx:31-37](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/files.component.tsx#L31-L37), [packages/sanity/registry/files.component.tsx:47-51](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/registry/files.component.tsx#L47-L51)
</Accordions>
</Step>
</Step>

## Related

- [Source Loader](/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/source-loader)
- [BaseHub Integration](/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/content-sources/basehub-integration)

Was this page helpful?