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 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 ;
}
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
{
"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
}
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 ];
},
};
Directory Structure
Filename Suffix
docs/
en/
getting-started.mdx
api.mdx
zh/
getting-started.mdx
api.mdx
Use parser: 'dir' in i18n config. docs/
getting-started.en.mdx
getting-started.zh.mdx
api.en.mdx
api.zh.mdx
Use parser: 'dot' 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