Getting Started
Core Engine
UI Layouts
API Documentation
Content Adapters
Developer Tooling
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?