---
title: "Source Loader"
description: "Fumadocs source loaders ingest, normalize, and index document collections from diverse sources like local disk storage, Content Collections, and remote OpenAPI or AsyncAPI specs into a unified virt..."
last_updated: "2026-10-12T07:53:59.827853+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/source-loader"
---

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

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

- [packages/preview/src/lib/source/storage.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/storage.ts)
- [packages/content/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content/src/index.ts)
- [packages/local-md/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts)
- [packages/core/src/source/loader.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/loader.ts)
- [packages/mdx/src/runtime/server.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/runtime/server.ts)
- [packages/content-collections/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content-collections/src/index.ts)
- [packages/content/src/runtime.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content/src/runtime.ts)
- [packages/asyncapi/src/server/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/server/index.tsx)
- [packages/mdx/src/loaders/meta.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/loaders/meta.ts)
- [packages/core/src/source/dynamic.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/dynamic.ts)
- [packages/openapi/src/server/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/server/index.tsx)
- [packages/preview/src/lib/source/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/index.ts)
- [packages/local-md/src/storage.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts)
- [packages/mdx/src/runtime/dynamic.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/runtime/dynamic.ts)
- [packages/mdx/src/webpack/meta.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/webpack/meta.ts)
- [packages/core/src/source/storage/content.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/storage/content.ts)
- [packages/mdx/src/plugins/index-file.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/plugins/index-file.ts)
- [packages/sanity/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/sanity/src/index.ts)
- [packages/preview/src/pages/...slugs.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx)
- [packages/openapi/src/utils/document/load.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/utils/document/load.ts)
- [packages/core/src/source/client/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx)
- [packages/content-collections/src/configuration.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content-collections/src/configuration.ts)
- [packages/mdx/src/loaders/mdx/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/loaders/mdx/index.ts)
- [packages/obsidian/src/build-storage.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/build-storage.ts)
- [packages/core/src/source/source.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/source.ts)
- [packages/core/src/source/plugins/slugs.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/plugins/slugs.ts)
- [packages/python/fumapy/mksource/__init__.py](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/python/fumapy/mksource/__init__.py)
- [packages/story/src/webpack/story.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/story/src/webpack/story.ts)
- [packages/mdx/src/core.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/core.ts)
- [packages/mdx/src/loaders/mdx/remark-include.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/loaders/mdx/remark-include.ts)
</details>

## Overview

Fumadocs source loaders ingest, normalize, and index document collections from diverse sources like local disk storage, Content Collections, and remote OpenAPI or AsyncAPI specs into a unified virtual file system. They expose public contracts and virtual file definitions via `source` and `StaticSource`, mapping files to typed page and metadata objects.
Sources: [packages/core/src/source/source.ts:1-114](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/source.ts#L1-L114), [packages/core/src/source/storage/content.ts:1-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/storage/content.ts#L1-L42)

The core loader pipeline parses frontmatter, handles internationalization directories or extension codes, applies loader plugins for slugs and builds hierarchical page trees and search indices.
Sources: [packages/core/src/source/loader.ts:268-315](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/loader.ts#L268-L315), [packages/core/src/source/storage/content.ts:50-126](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/storage/content.ts#L50-L126), [packages/core/src/source/plugins/slugs.ts:15-42](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/plugins/slugs.ts#L15-L42)

Through dynamic loaders and client-side deserialization hooks, Fumadocs supports runtime revalidation, hot reloading, and preloaded remote document integration.
Sources: [packages/core/src/source/dynamic.ts:27-119](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/dynamic.ts#L27-L119), [packages/core/src/source/client/index.tsx:14-60](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L14-L60), [packages/local-md/src/index.ts:84-208](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L84-L208)

## Unified source interface and contracts

### Core data structures and virtual file contracts

Fumadocs source loaders rely on a shared type architecture defined by `SourceUnion`, distinguishing between `StaticSource` and `DynamicSource` objects via guard functions `isStaticSource` and `isDynamicSource`. Static sources maintain an array of `VirtualFile` items directly, whereas dynamic sources supply an asynchronous or synchronous builder function `files: () => Awaitable<VirtualFile<Config>[]>` alongside an optional `configure` hook accepting a `DynamicLoader`.
Sources: [packages/core/src/source/source.ts:5-21](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/source.ts#L5-L21), [packages/core/src/source/source.ts:172-178](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/source.ts#L172-L178)

Every virtual file extends `BaseVirtualFile`, holding a virtualized relative path and an optional absolute path on disk. Depending on its `type` discriminant, a file resolves to either a `VirtualPage` carrying `slugs` and typed `pageData`, or a `VirtualMeta` carrying typed `metaData`.
Sources: [packages/core/src/source/source.ts:48-78](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/source.ts#L48-L78)

| Property / Type | Field Type | Purpose & Description |
| :--- | :--- | :--- |
| `path` | `string` | Virtualized path relative to the content directory (e.g. `docs/page.mdx`). |
| `absolutePath` | `string` (optional) | Absolute path of the underlying file on disk. |
| `type` | `'page' \| 'meta'` | Discriminant property identifying whether the virtual file is a page or metadata file. |
| `slugs` | `string[]` (optional) | Explicit route segments assigned to a virtual page. |
| `data` | `PageData \| MetaData` | Payload containing frontmatter attributes, titles, icons, or navigation controls. |

Sources: [packages/core/src/source/source.ts:52-78](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/source.ts#L52-L78)

> [!NOTE]
> The `multiple` helper utility is deprecated in favor of passing a record of source objects directly to the main `loader()` function, which automatically scopes namespaces and injects type tags.
> Sources: [packages/core/src/source/source.ts:80-82](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/source.ts#L80-L82)

### Metadata schemas and builder utilities

The base metadata structures define standard properties supported across documentation nodes. `MetaData` configures folder navigation and display parameters such as `icon`, `title`, `root`, `pages`, `pagesIndex`, `defaultOpen`, `collapsible`, and `description`. `PageData` configures individual document attributes including `icon`, `title`, `description`, and `structuredData` (which accepts search index payloads or asynchronous generators).
Sources: [packages/core/src/source/source.ts:28-46](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/source.ts#L28-L46)

```typescript
export function source<Page extends PageData, Meta extends MetaData>(config: {
  pages: VirtualPage<Page>[];
  metas: VirtualMeta<Meta>[];
}): StaticSource<{
  pageData: Page;
  metaData: Meta;
}> {
  return {
    files: [...config.pages, ...config.metas],
  };
}

interface SourceUpdater<Config extends SourceConfig> {
  files: <Page extends PageData, Meta extends MetaData>(
    fn: (files: VirtualFile<Config>[]) => (VirtualPage<Page> | VirtualMeta<Meta>)[],
  ) => SourceUpdater<{
    pageData: Page;
    metaData: Meta;
  }>;
  page: <V extends PageData>(
    fn: (page: VirtualPage<Config['pageData']>) => VirtualPage<V>,
  ) => SourceUpdater<{
    pageData: V;
    metaData: Config['metaData'];
  }>;

  meta: <V extends MetaData>(
    fn: (meta: VirtualMeta<Config['metaData']>) => VirtualMeta<V>,
  ) => SourceUpdater<{
    pageData: Config['pageData'];
    metaData: V;
  }>;
  build: () => StaticSource<Config>;
}

/**
 * update a source object in-place.
 */
export function update<Config extends SourceConfig>(
  source: StaticSource<Config>,
): SourceUpdater<Config> {
  return {
    files(fn) {
      source.files = fn(source.files);
      return this as SourceUpdater<never>;
    },
    page(fn) {
      for (let i = 0; i < source.files.length; i++) {
        const file = source.files[i];
        if (file.type === 'page') source.files[i] = fn(file);
      }

      return this as SourceUpdater<never>;
    },
    meta(fn) {
      for (let i = 0; i < source.files.length; i++) {
        const file = source.files[i];
        if (file.type === 'meta') source.files[i] = fn(file);
      }

      return this as SourceUpdater<never>;
    },
    build() {
      return source;
    },
  };
}
```
Sources: [packages/core/src/source/source.ts:104-170](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/source.ts#L104-L170)

### External adapters and runtime transformation

External integrations bridge third-party content providers into the standard Fumadocs source contract. The [Content Collections Adapter](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/content-sources/content-collections-adapter) maps `allDocs` and `allMetas` arrays into `VirtualPage` and `VirtualMeta` structures via `createMDXSource`, binding `_meta.filePath` directly to file paths.
Sources: [packages/content-collections/src/index.ts:4-35](https://github.com/packages/content-collections/src/index.ts#L4-L35)

Similarly, runtime content stores convert compiled MDX collections and metadata stores into standard `StaticSource` objects through `toFumadocsSource` and `docsStore`. Frontmatter properties are merged directly into page data objects alongside absolute path tracking.
Sources: [packages/content/src/runtime.ts:12-56](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content/src/runtime.ts#L12-L56)

> [!WARNING]
> When integrating custom runtime stores, ensure frontmatter properties do not overwrite core `VirtualPage` discriminatory fields (`type`, `path`, `absolutePath`) during object spreading.
> Sources: [packages/content/src/runtime.ts:23-32](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/content/src/runtime.ts#L23-L32)

## In-memory filesystem and content storage

### Storage construction and plugin transformation

The `createContentStorageBuilder` function transforms raw static or multi-namespace content inputs into an indexed virtual file system. When internationalization is enabled, parser routines strip locale codes from file paths based on either directory structures or filename extensions, mapping entries into a shared `fileMap` indexed by target language or an `EmptyLang` symbol.
Sources: [packages/core/src/source/storage/content.ts:43-93](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/storage/content.ts#L43-L93)

```typescript
export function createContentStorageBuilder(loaderConfig: ResolvedLoaderConfig) {
  const { input, plugins, i18n } = loaderConfig;

  let parser: (normalizedPath: string) => [string, (string | string[])?];
  if (!i18n) {
    parser = (path) => [path];
  } else if (i18n.parser === 'dir') {
    const langSet = new Set(i18n.languages);

    parser = (path) => {
      const [locale, ...segs] = path.split('/');
      if (!locale || segs.length === 0) return [path];

      if (langSet.has(locale)) return [segs.join('/'), locale];
      if (locale === '$') return [segs.join('/'), i18n.languages];

      return [path];
    };
  } else {
    const langSet = new Set(i18n.languages);

    parser = (path) => {
      const segs = path.split('/');
      const base = segs.pop();
      if (!base) return [path];

      const parts = base.split('.');
      if (parts.length < 3) return [path];

      const [locale] = parts.splice(parts.length - 2, 1);
      segs.push(parts.join('.'));

      if (langSet.has(locale)) return [segs.join('/'), locale];
      if (locale === '$') return [segs.join('/'), i18n.languages];

      return [path];
    };
  }
```
Sources: [packages/core/src/source/storage/content.ts:50-87](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/storage/content.ts#L50-L87)

During the scanning phase, input files are categorized as either page or metadata files. Each file receives a normalized path, format descriptor, source type, and underlying data payload before being inserted into the storage map.
Sources: [packages/core/src/source/storage/content.ts:94-126](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/storage/content.ts#L94-L126)

| Storage File Type | Format Discriminant | Required Properties | Optional Properties |
| :--- | :--- | :--- | :--- |
| `ContentStoragePageFile` | `'page'` | `format`, `path`, `slugs`, `data` | `type`, `absolutePath` |
| `ContentStorageMetaFile` | `'meta'` | `format`, `path`, `data` | `type`, `absolutePath` |

Sources: [packages/core/src/source/storage/content.ts:17-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/storage/content.ts#L17-L41)

> [!NOTE]
> When evaluating language parsers, if a file path segment equals `$`, the parser maps the file across all configured internationalization languages simultaneously.
> Sources: [packages/core/src/source/storage/content.ts:64-83](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/storage/content.ts#L64-L83)

Once files are indexed within `fileMap`, `makeStorage` instantiates a `FileSystem` instance for a given locale. It populates the filesystem with corresponding storage entries and executes any registered plugin storage transformation hooks.
Sources: [packages/core/src/source/storage/content.ts:134-146](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/storage/content.ts#L134-L146)

```typescript
function makeStorage(locale: string | typeof EmptyLang, inherit?: ContentStorage) {
  const storage = new FileSystem(inherit) as ContentStorage;
  for (const [storageKey, file] of fileMap.get(locale) ?? []) {
    storage.write(storageKey, file);
  }

  const context = { storage };
  for (const plugin of plugins) {
    plugin.transformStorage?.(context);
  }

  return storage;
}
```
Sources: [packages/core/src/source/storage/content.ts:134-146](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/storage/content.ts#L134-L146)

## Dynamic loader resolution and caching

```mermaid
sequenceDiagram
    participant dynamicLoader as dynamic.ts
    participant resolveSources as dynamic.ts
    participant resolveSource as dynamic.ts
    participant isStaticSource as source.ts
    dynamicLoader->>resolveSources: get()
    resolveSources->>resolveSource: map input items
    resolveSource->>isStaticSource: check static vs dynamic
```
Sources: [packages/core/src/source/dynamic.ts:52-79](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/dynamic.ts#L52-L79), [packages/core/src/source/source.ts:172-174](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/source.ts#L172-L174)

The `dynamicLoader` factory handles runtime content resolution, source configuration, and memoization for dynamic and static documentation sources. During initialization, `configureSources` iterates over input sources and executes `v.configure?.(dynamicLoader)` when an input matches dynamic source criteria.
Sources: [packages/core/src/source/dynamic.ts:40-50](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/dynamic.ts#L40-L50), [packages/core/src/source/dynamic.ts:116-116](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/dynamic.ts#L116-L116)

The resolution pipeline follows a strict call chain: `dynamicLoader` invokes `resolveSources`, which calls `resolveSource` on each source object, delegating to `isStaticSource` for type discrimination.
Sources: [packages/core/src/source/dynamic.ts:52-79](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/dynamic.ts#L52-L79)

```typescript
async function resolveSources(skipCache = false): Promise<ResolvedSource> {
  if (isStaticSource(input) || isDynamicSource(input)) {
    return resolveSource(input, skipCache);
  }

  const entries = await Promise.all(
    Object.entries(input).map(async ([k, v]) => [k, await resolveSource(v, skipCache)]),
  );

  return Object.fromEntries(entries);
}
```
Sources: [packages/core/src/source/dynamic.ts:52-62](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/dynamic.ts#L52-L62)

> [!WARNING]
> Bypassing cache via `skipCache = true` forces `resolveSource` to re-execute `v.files()` and overwrite existing entries in `sourceCache`, discarding previously memoized virtual file arrays.
> Sources: [packages/core/src/source/dynamic.ts:64-79](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/dynamic.ts#L64-L79)

Source equality is evaluated by `isEqual`, which checks reference identity for static sources or compares key counts and strict reference equality for multi-source records.
Sources: [packages/core/src/source/dynamic.ts:121-133](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/dynamic.ts#L121-L133)

```typescript
function isEqual(a: ResolvedSource, b: ResolvedSource): boolean {
  if (isStaticSource(a) && isStaticSource(b)) {
    return a === b;
  }

  if (!isStaticSource(a) && !isStaticSource(b)) {
    const aKeys = Object.keys(a);
    const bKeys = Object.keys(b);
    return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
  }

  return false;
}
```
Sources: [packages/core/src/source/dynamic.ts:121-133](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/dynamic.ts#L121-L133)

## Loader configuration, plugin pipeline, and execution walkthrough

```mermaid
sequenceDiagram
    participant dynamicLoader as dynamic.ts
    participant loader as loader.ts
    participant resolveConfig as loader.ts
    dynamicLoader->>loader: dynamicLoader invokes loader(resolved, options)
    loader->>resolveConfig: loader calls resolveConfig(source, options)
    resolveConfig-->>loader: returns resolved config containing plugins
```
Sources: [packages/core/src/source/dynamic.ts:90-93](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/dynamic.ts#L90-L93), [packages/core/src/source/loader.ts:287-301](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/loader.ts#L287-L301), [packages/core/src/source/loader.ts:474-499](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/loader.ts#L474-L499)

The execution walkthrough for building plugins and resolving configurations traces through `dynamicLoader` → `loader` → `resolveConfig`. When `dynamicLoader` computes its cache via `get()`, it invokes `loader()` with the resolved source and options. The `loader` function immediately delegates to `resolveConfig()`, which merges base settings, slugs options, icons, and plugins into a unified `ResolvedLoaderConfig`.
Sources: [packages/core/src/source/dynamic.ts:27-119](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/dynamic.ts#L27-L119), [packages/core/src/source/loader.ts:287-499](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/loader.ts#L287-L499)

```typescript
export function loader<I extends ResolvedInput, I18n extends I18nConfig | undefined = undefined>(
  ...args:
    | [
        LoaderOptions & {
          source: I;
        },
      ]
    | [I, LoaderOptions]
): LoaderOutput<{
  meta: GenerateMeta<I>;
  page: GeneratePage<I>;
  i18n: I18n;
}> {
  const loaderConfig =
    args.length === 2 ? resolveConfig(args[0], args[1]) : resolveConfig(args[0].source, args[0]);
```
Sources: [packages/core/src/source/loader.ts:287-301](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/loader.ts#L287-L301)

> [!NOTE]
> `resolveConfig` unpacks base loader configurations and validates internationalization parameters before content storage builder initialization.
> Sources: [packages/core/src/source/loader.ts:474-479](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/loader.ts#L474-L479)

The slugs plugin (`slugsPlugin`) inspects files via `storage.getFiles()`, processing non-page items or files with pre-existing slugs. When a custom `slugFn` is absent, it identifies index files via `isIndex`, pushing them to an index queue to prevent collision conflicts between `dir/index.mdx` and `dir.mdx`.
Sources: [packages/core/src/source/plugins/slugs.ts:15-35](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/plugins/slugs.ts#L15-L35)

```typescript
transformStorage({ storage }) {
  const indexFiles: string[] = [];
  const taken = new Set<string>();

  for (const path of storage.getFiles()) {
    const file = storage.read(path);
    if (!file || file.format !== 'page' || file.slugs) continue;

    const customSlugs = slugFn?.(file);
    // for custom slugs function, don't handle conflicting cases like `dir/index.mdx` vs `dir.mdx`
    if (customSlugs === undefined && isIndex(path)) {
      indexFiles.push(path);
      continue;
    }

    file.slugs = customSlugs ?? getSlugs(path);
    const key = file.slugs.join('/');
    if (taken.has(key)) throw new Error(`Duplicated slugs: ${key}`);
    taken.add(key);
  }
```
Sources: [packages/core/src/source/plugins/slugs.ts:22-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/plugins/slugs.ts#L22-L41)

> [!WARNING]
> Duplicate slug keys detected across storage entries trigger an immediate error throw via `if (taken.has(key)) throw new Error(...)`.
> Sources: [packages/core/src/source/plugins/slugs.ts:38-40](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/plugins/slugs.ts#L38-L40)

Path segregation filters out empty directory names and folder groups matching `GroupRegex` (`/^\(.+\)$/`). Any attempt to use a folder group directly inside a file name throws an error.
Sources: [packages/core/src/source/plugins/slugs.ts:69-85](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/plugins/slugs.ts#L69-L85)

```typescript
export function getSlugs(file: string): string[] {
  const dir = dirname(file);
  const name = basename(file, extname(file));
  const slugs: string[] = [];

  for (const seg of dir.split('/')) {
    // filter empty names and file groups like (group_name)
    if (seg.length > 0 && !GroupRegex.test(seg)) slugs.push(encodeURI(seg));
  }

  if (GroupRegex.test(name)) throw new Error(`Cannot use folder group in file names: ${file}`);

  if (name !== 'index') {
    slugs.push(encodeURI(name));
  }

  return slugs;
```
Sources: [packages/core/src/source/plugins/slugs.ts:74-90](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/plugins/slugs.ts#L74-L90)

## Local markdown and disk storage

### File scanning and storage implementation

```mermaid
graph TD
    subgraph Storage Flow
        A[glob include patterns] -->|cwd: dir| B[chunked file promises]
        B -->|CHUNK_SIZE: 100| C[buildFile]
        C -->|extname: .json| D[json meta parser]
        C -->|extname: .md or .mdx| E[md frontmatter parser]
    end
```
Sources: [packages/local-md/src/storage.ts:23-56](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts#L23-L56), [packages/local-md/src/storage.ts:110-126](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts#L110-L126)

Local markdown collections are managed by `createStorage`, which scans content directories using `tinyglobby` with configurable include patterns. Discovered files are batched into chunks of 100 items (`CHUNK_SIZE`) and processed concurrently via `buildFile`. Files are discriminated by extension: `.json` files are parsed as metadata collections, while `.md` and `.mdx` files are processed for frontmatter and body content.
Sources: [packages/local-md/src/storage.ts:23-56](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts#L23-L56), [packages/local-md/src/storage.ts:110-126](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts#L110-L126)

```typescript
async getPages() {
  const files = await glob(include, {
    cwd: dir,
  });
  const chunks: Promise<($Page | $Meta | undefined)[]>[] = [];

  for (let i = 0; i < files.length; i += CHUNK_SIZE) {
    const promises: Promise<$Page | $Meta | undefined>[] = [];
    const L = Math.min(files.length, i + CHUNK_SIZE);

    for (let j = i; j < L; j++) {
      promises.push(buildFile(files[j]));
    }

    chunks.push(Promise.all(promises));
  }

  const pages: $Page[] = [];
  const metas: $Meta[] = [];
  for await (const chunk of chunks) {
    for (const item of chunk) {
      if (!item) continue;
      if ('frontmatter' in item) pages.push(item);
      else metas.push(item);
    }
  }

  return { pages, metas };
},
```
Sources: [packages/local-md/src/storage.ts:110-138](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts#L110-L138)

> [!WARNING]
> If building a file throws an error, the catch block logs the failure to the console and deletes the corresponding absolute path entry from `filesCache`.
> Sources: [packages/local-md/src/storage.ts:62-66](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts#L62-L66)

### Frontmatter parsing and schema validation

```typescript
async function md(absolutePath: string, file: string): Promise<$Page> {
  const content = await fs.readFile(absolutePath, 'utf-8');
  const parsed = parseFrontmatter(content);

  const frontmatterResult = await frontmatterSchema['~standard'].validate(parsed.data);
  if (frontmatterResult.issues) {
    const message = frontmatterResult.issues
      .map((issue) => (issue.path ? `${issue.path}: ${issue.message}` : issue.message))
      .join('\n');
    throw new Error(`invalid frontmatter in "${absolutePath}": ${message}`);
  }

  return {
    path: file,
    absolutePath,
    content: parsed.content,
    frontmatter: frontmatterResult.value,
  };
}
```
Sources: [packages/local-md/src/storage.ts:68-86](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts#L68-L86)

Markdown and MDX files are read from disk and processed with `parseFrontmatter` to separate content from metadata headers. The extracted frontmatter object is validated against standard schemas using `frontmatterSchema['~standard'].validate()`. If validation fails, issues are mapped into a detailed multi-line error string reporting path and message specifics.
Sources: [packages/local-md/src/storage.ts:68-86](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts#L68-L86)

| Extension | Target Interface | Validation Schema | Parser Utility |
| :--- | :--- | :--- | :--- |
| `.md`, `.mdx` | `RawPage` | `frontmatterSchema` | `parseFrontmatter` |
| `.json` | `RawMeta` | `metaSchema` | `JSON.parse` |

Sources: [packages/local-md/src/storage.ts:10-21](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts#L10-L21), [packages/local-md/src/storage.ts:72-97](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts#L72-L97)

> [!CAUTION]
> Schema validation errors halt execution immediately by throwing an `Error` containing formatted validation issues for either invalid frontmatter or invalid JSON data.
> Sources: [packages/local-md/src/storage.ts:73-78](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts#L73-L78), [packages/local-md/src/storage.ts:92-97](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/storage.ts#L92-L97)

### Runtime adapters and dev server integration

```typescript
export function localMd<
  FrontmatterSchema extends StandardSchemaV1 = typeof defaultSchemas.pageSchema,
  MetaSchema extends StandardSchemaV1 = typeof defaultSchemas.metaSchema,
>(
  config: LocalMarkdownConfig<FrontmatterSchema, MetaSchema>,
): LocalMarkdown<FrontmatterSchema, MetaSchema> {
  const storage = createStorage(config);
  const compiler = createMarkdownCompiler(config);
  const compilerCache = new WeakMap<RawPage<unknown>, Promise<CompileResult>>();
  const registeredLoaders = new Set<DynamicLoader>();
  let cachedStaticSource: Promise<
    StaticSource<{
      pageData: LocalMarkdownPage<StandardSchemaV1.InferOutput<FrontmatterSchema>, never>;
      metaData: StandardSchemaV1.InferOutput<MetaSchema> & MetaData;
    }>
  > | null = null;

  async function createFiles(options?: SourceOptions) {
    const { metas, pages } = await storage.getPages();
    const baseDir = options?.baseDir;
    const files: VirtualFile<{
      pageData: LocalMarkdownPage<StandardSchemaV1.InferOutput<FrontmatterSchema>, never>;
      metaData: StandardSchemaV1.InferOutput<MetaSchema> & MetaData;
    }>[] = [];

    for (const page of pages) {
      const frontmatter = page.frontmatter as PageData & { _openapi?: unknown };

      files.push({
        type: 'page',
        path: baseDir ? path.join(baseDir, page.path) : page.path,
        absolutePath: page.absolutePath,
        data: {
          title: frontmatter.title ?? path.basename(page.path, path.extname(page.path)),
          description: frontmatter.description,
          icon: frontmatter.icon,
          // for Fumadocs OpenAPI
          ['_openapi' as never]: frontmatter._openapi,

          content: page.content,
          frontmatter: page.frontmatter,
          async load() {
            let promise = compilerCache.get(page);
            if (!promise) {
              promise = compiler.compile({
                path: page.absolutePath,
                value: page.content,
                data: { frontmatter: page.frontmatter },
              });
            }

            const res = await promise;
            return res.type === 'ast'
              ? fromAst({
                  tree: res.tree,
                  filePath: res.file.path,
                  rehypeToc: res.file.data.rehypeToc,
                  structuredData: res.file.data.structuredData,
                  ...config.rendererOptions,
                })
              : fromJS({
                  code: res.code,
                  filePath: res.file.path,
                  baseUrl: pathToFileURL(res.file.path).href,
                  structuredData: res.file.data.structuredData,
                });
          },
        },
      });
    }
```
Sources: [packages/local-md/src/index.ts:84-153](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L84-L153)

The `localMd` function exposes `staticSource` and `dynamicSource` runtime adapters that transform raw storage entities into virtual files consumed by core loaders. Each page object provides an asynchronous `load` method that compiles content via a markdown compiler instance, utilizing `compilerCache` to memoize compilation promises per page. When connected to a development server via `devServer`, the runner watches the target directory and dispatches file invalidation events.
Sources: [packages/local-md/src/index.ts:84-153](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L84-L153), [packages/local-md/src/index.ts:174-195](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L174-L195)

> [!NOTE]
> Calling `invalidateFile` resets `cachedStaticSource` to `null`, evicts the path from `storage`, and triggers invalidation callbacks on all registered dynamic loaders.
> Sources: [packages/local-md/src/index.ts:168-173](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L168-L173)

## Client deserialization and preview runtime

### Client-side page tree consumption

```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)

The client package handles serialized navigation trees generated by the server-side loader through `deserializePageTree`. It inspects incoming objects using `isSerializedPageTree`, verifying that `$fumadocs_loader` is set to `'page-tree'`. Once validated, `visit` traverses the tree nodes, parsing raw HTML strings for item icons and names into React span elements configured with `dangerouslySetInnerHTML`.
Sources: [packages/core/src/source/client/index.tsx:5-8](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L5-L8), [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), [packages/core/src/source/client/index.tsx:62-69](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L62-L69)

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

The `useFumadocsLoader` hook memoizes deserialization across re-renders. It iterates over the properties of the provided serialized object, converting matching page tree structures while preserving all unrelated properties in the output.
Sources: [packages/core/src/source/client/index.tsx:47-60](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L47-L60)

> [!NOTE]
> `useFumadocsLoader` preserves any non-loader properties present on the input object alongside the freshly parsed page tree elements.
> Sources: [packages/core/src/source/client/index.tsx:42-46](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L42-L46), [packages/core/src/source/client/index.tsx:54-56](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/source/client/index.tsx#L54-L56)

### Preview revalidation hooks and route rendering

```typescript
export const getSource = revalidable({
  async create(config: ParsedAppConfig) {
    return loader({
      source: source(await getPages(config.content)),
      plugins: [lucideIconsPlugin()],
      baseUrl: '/',
    });
  },
});
```
Sources: [packages/preview/src/lib/source/index.ts:7-15](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/index.ts#L7-L15)

The preview application defines `getSource` via a revalidable wrapper that instantiates a content loader using `source` with local page data and the `lucideIconsPlugin`. Document paths and page image routes are managed by `getPageImage`, which appends `image.webp` segments to the URL path array.
Sources: [packages/preview/src/lib/source/index.ts:7-27](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/source/index.ts#L7-L27)

```tsx
export default async function DocPage({ slugs }: PageProps<'/docs/[...slugs]'>) {
  const config = await getConfigRuntime();
  const source = await getSource(config);
  const page = source.getPage(slugs);
  const mdPreset = config.layout.presets?.md ?? 'docs';
  let mdPresetComponents: MdPresetComponents;

  if (mdPreset === 'docs') {
    mdPresetComponents = {
      layout: await import('fumadocs-ui/layouts/docs'),
      page: await import('fumadocs-ui/layouts/docs/page'),
    };
  } else {
    mdPresetComponents = {
      layout: await import('fumadocs-ui/layouts/flux'),
      page: await import('fumadocs-ui/layouts/flux/page'),
    };
  }

  return <MdContent slugs={slugs} page={page} components={mdPresetComponents} />;
}
```
Sources: [packages/preview/src/pages/...slugs.tsx:76-96](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L76-L96)

The dynamic route component `DocPage` resolves runtime configurations and dynamic layout presets (`docs` or `flux`) before rendering document contents. During compilation, remark and rehype plugins process markdown tables, mathematics, mermaid diagrams, code blocks, and [table of contents](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/table-of-contents) data.
Sources: [packages/preview/src/pages/...slugs.tsx:29-46](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L29-L46), [packages/preview/src/pages/...slugs.tsx:76-96](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L76-L96)

| Preset Name | Layout Import | Page Import |
| :--- | :--- | :--- |
| `docs` | `fumadocs-ui/layouts/docs` | `fumadocs-ui/layouts/docs/page` |
| Other | `fumadocs-ui/layouts/flux` | `fumadocs-ui/layouts/flux/page` |

Sources: [packages/preview/src/pages/...slugs.tsx:83-93](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx#L83-L93)

> [!WARNING]
> If markdown compilation encounters an error, `DocPage` catches the exception and renders a fallback error view containing the exception message and stack trace inside a code block.
> Sources: [packages/preview/src/pages/[...slugs].tsx:169-194](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/[...slugs].tsx#L169-L194)

## Related

- [Page Tree Generation](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/page-tree-generation)
- [MDX Loader Pipeline](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/mdx-pipeline/mdx-loader-pipeline)


## Sitemap

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