Skip to main content

Overview

Content sources are the foundation of Fumadocs, providing an abstraction layer that enables loading documentation from various providers - local files, CMSs, APIs, or databases. The source adapter system transforms content into a unified format for processing.

Source Interface

All content sources implement the Source interface:
packages/core/src/source/source.ts
export interface Source<Config extends SourceConfig = SourceConfig> {
  files: VirtualFile<Config>[];
}

export interface SourceConfig {
  pageData: PageData;
  metaData: MetaData;
}
The Source interface is intentionally simple - it’s just an array of virtual files. This makes it easy to create custom source adapters.

Virtual Files

Content is represented as virtual files with two types:
interface VirtualPage<Data> {
  type: 'page';
  
  // Virtualized path (relative to content directory)
  path: string;
  
  // Absolute path of source file (optional)
  absolutePath?: string;
  
  // Slugs for URL generation
  slugs?: string[];
  
  // Page metadata (title, description, etc.)
  data: Data;
}

Virtual Paths

Virtual paths are normalized, framework-agnostic paths that represent the logical structure of your documentation:
// Real filesystem path
/Users/you/project/docs/getting-started/installation.mdx

// Virtual path
docs/getting-started/installation.mdx
Virtual paths must NOT start with ./ or ../. They are always relative to the content root.

Page Data

Page data contains metadata extracted from frontmatter or provided by your source:
packages/core/src/source/source.ts
export interface PageData {
  icon?: string | undefined;
  title?: string;
  description?: string | undefined;
}
You can extend PageData with custom fields in your source adapter:
interface CustomPageData extends PageData {
  author?: string;
  tags?: string[];
  lastModified?: Date;
}

const source = createSource<CustomPageData, MetaData>({
  // your implementation
});

Meta Data

Meta files configure folder behavior in the page tree:
packages/core/src/source/source.ts
export interface MetaData {
  icon?: string | undefined;
  title?: string | undefined;
  description?: string | undefined;
  
  // Mark as root folder (appears in top-level navigation)
  root?: boolean | undefined;
  
  // Custom page ordering
  pages?: string[] | undefined;
  
  // Folder starts open
  defaultOpen?: boolean | undefined;
  
  // Folder can be collapsed
  collapsible?: boolean | undefined;
}

Meta File Patterns

The pages array supports special patterns:

Rest Operator

... - Include all remaining files in alphabetical order

Reverse Rest

z...a - Include remaining files in reverse alphabetical order

Extract Folder

...folder-name - Inline a folder’s children

Exclude Files

!file-name - Exclude specific files from auto-ordering
meta.json
{
  "title": "Getting Started",
  "pages": [
    "introduction",
    "installation",
    "...",
    "!internal-doc"
  ]
}

Creating a Source

Use the source() helper to create a source:
packages/core/src/source/source.ts
export function source<Page extends PageData, Meta extends MetaData>(config: {
  pages: VirtualPage<Page>[];
  metas: VirtualMeta<Meta>[];
}): Source<{
  pageData: Page;
  metaData: Meta;
}> {
  return {
    files: [...config.pages, ...config.metas],
  };
}

Example: Simple Source

import { source } from 'fumadocs-core/source';

const mySource = source({
  pages: [
    {
      type: 'page',
      path: 'docs/introduction.mdx',
      slugs: ['introduction'],
      data: {
        title: 'Introduction',
        description: 'Get started with Fumadocs',
      },
    },
  ],
  metas: [
    {
      type: 'meta',
      path: 'docs/meta.json',
      data: {
        title: 'Documentation',
        root: true,
      },
    },
  ],
});

Multiple Sources

Combine multiple sources with the multiple() helper:
packages/core/src/source/source.ts
export function multiple<T extends Record<string, Source>>(sources: T) {
  const out: Source = { files: [] };

  for (const [type, source] of Object.entries(sources)) {
    for (const file of source.files) {
      out.files.push({
        ...file,
        data: {
          ...file.data,
          type, // Add type discriminator
        },
      });
    }
  }

  return out;
}

Example: Multi-Source Setup

import { multiple } from 'fumadocs-core/source';
import { docsSource } from './docs-source';
import { blogSource } from './blog-source';

const combined = multiple({
  docs: docsSource,
  blog: blogSource,
});

// Pages now have a 'type' field
const page = combined.files[0];
if (page.data.type === 'docs') {
  // TypeScript knows this is a docs page
}

Transforming Sources

The update() function enables source transformations:
packages/core/src/source/source.ts
export function update<Config>(source: Source<Config>) {
  return {
    files(fn) {
      source.files = fn(source.files);
      return this;
    },
    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;
    },
    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;
    },
    build() {
      return source;
    },
  };
}

Example: Adding Custom Fields

import { update } from 'fumadocs-core/source';

const enhanced = update(mySource)
  .page((page) => ({
    ...page,
    data: {
      ...page.data,
      author: 'Team Fumadocs',
      lastModified: new Date(),
    },
  }))
  .build();

Built-in Source Adapters

Fumadocs provides official source adapters:

Fumadocs MDX

Load MDX files from the filesystem with full type safety

Content Collections

Integrate with Content Collections for advanced content management

TypeScript Config

Generate documentation from TypeScript type definitions

OpenAPI

Generate API documentation from OpenAPI/Swagger specs

Locale Handling

Content storage handles localization through path parsing:
packages/core/src/source/storage/content.ts
const parsers = {
  // Parse locale from directory: en/docs/page.mdx
  dir(path: string): [string, string?] {
    const [locale, ...segs] = path.split('/');
    if (locale && segs.length > 0 && isLocaleValid(locale)) {
      return [segs.join('/'), locale];
    }
    return [path];
  },
  
  // Parse locale from filename: docs/page.en.mdx
  dot(path: string): [string, string?] {
    const dir = dirname(path);
    const base = basename(path);
    const parts = base.split('.');
    if (parts.length < 3) return [path];
    
    const [locale] = parts.splice(parts.length - 2, 1);
    if (!isLocaleValid(locale)) return [path];
    
    return [joinPath(dir, parts.join('.')), locale];
  },
  
  // No locale parsing
  none(path: string): [string, string?] {
    return [path];
  },
};
docs/
  en/
    getting-started.mdx
    api.mdx
  zh/
    getting-started.mdx
    api.mdx
Use parser: 'dir' in i18n config.

Slug Generation

The slugs plugin generates URL slugs from file paths:
packages/core/src/source/plugins/slugs.ts
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 (name !== 'index') {
    slugs.push(encodeURI(name));
  }

  return slugs;
}

File Groups

Wrap folder names in parentheses to exclude them from URLs:
docs/
  (getting-started)/
    introduction.mdx  -> /docs/introduction
    setup.mdx        -> /docs/setup
  api/
    reference.mdx    -> /docs/api/reference
File groups help organize content without affecting URL structure.

Custom Slug Functions

Override default slug generation:
import { loader } from 'fumadocs-core/source';
import { slugsFromData } from 'fumadocs-core/source';

const docs = loader({
  source: mySource,
  baseUrl: '/docs',
  slugs: (file) => {
    // Use slug from frontmatter if available
    if (file.data.slug) {
      return file.data.slug.split('/');
    }
    // Otherwise use default behavior
    return undefined;
  },
});

Content Storage Builder

The content storage builder processes virtual files into the in-memory file system:
packages/core/src/source/storage/content.ts
export function createContentStorageBuilder(loaderConfig: ResolvedLoaderConfig) {
  const { source, plugins = [], i18n } = loaderConfig;

  return {
    i18n(): Record<string, ContentStorage> {
      // Create separate storage for each locale
      const storages: Record<string, ContentStorage> = {};
      for (const lang of i18n.languages) {
        storages[lang] = makeStorage(lang);
      }
      return storages;
    },
    single(): ContentStorage {
      // Single storage for non-localized content
      return makeStorage();
    },
  };
}

Next Steps

Page Tree

Learn how sources are transformed into navigation trees

Fumadocs MDX

Use the official MDX source adapter

Custom Sources

Build your own source adapter

Plugin Development

Create plugins that transform sources

Build docs developers (and LLMs) love