Overview
Fumadocs provides a flexible routing system that generates URLs from file paths, handles internationalization, and supports custom URL patterns. The routing layer sits between your content sources and the final URLs users see.
URL Generation
URLs are generated from slugs, which are path segments derived from file paths:
packages/core/src/source/loader.ts
export function createGetUrl ( baseUrl : string , i18n ?: I18nConfig ) {
const baseSlugs = baseUrl . split ( '/' );
return ( slugs : string [], locale ?: string ) => {
const hideLocale = i18n ?. hideLocale ?? 'never' ;
let urlLocale : string | undefined ;
if ( hideLocale === 'never' ) {
urlLocale = locale ;
} else if ( hideLocale === 'default-locale' && locale !== i18n ?. defaultLanguage ) {
urlLocale = locale ;
}
const paths = [ ... baseSlugs , ... slugs ];
if ( urlLocale ) paths . unshift ( urlLocale );
return `/ ${ paths . filter (( v ) => v . length > 0 ). join ( '/' ) } ` ;
};
}
Example
File Path
Generated Slugs
Generated URL
docs / getting - started / installation . mdx
Slug Generation
Slugs are automatically generated from file paths by the slugs plugin:
packages/core/src/source/plugins/slugs.ts
const GroupRegex = / ^ \( . + \) $ / ;
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 ;
}
Slugs are automatically URI-encoded to support non-ASCII characters in URLs.
File Groups
Wrap directory names in parentheses to exclude them from URLs:
With Groups
Without Groups
docs/
(getting-started)/
introduction.mdx -> /docs/introduction
installation.mdx -> /docs/installation
(guides)/
tutorial.mdx -> /docs/tutorial
Groups organize files without affecting URLs. docs/
getting-started/
introduction.mdx -> /docs/getting-started/introduction
installation.mdx -> /docs/getting-started/installation
guides/
tutorial.mdx -> /docs/guides/tutorial
Standard nested URLs.
File groups can only be used in directory names, not file names. Using groups in file names will throw an error.
Index Pages
Files named index.mdx map to their parent directory’s URL:
docs/
index.mdx -> /docs
getting-started/
index.mdx -> /docs/getting-started
installation.mdx -> /docs/getting-started/installation
Index Slug Conflicts
When both dir.mdx and dir/index.mdx exist:
docs/
api.mdx -> /docs/api
api/
index.mdx -> /docs/api/index (conflict resolved)
reference.mdx -> /docs/api/reference
The plugin automatically appends /index to avoid conflicts.
Custom Slugs
Override automatic slug generation:
From Frontmatter
import { loader } from 'fumadocs-core/source' ;
import { slugsFromData } from 'fumadocs-core/source' ;
const docs = loader ({
source: mySource ,
baseUrl: '/docs' ,
slugs: slugsFromData ( 'slug' ), // Read from frontmatter 'slug' field
});
---
title : Custom URL Page
slug : custom/url/path
---
This page will be accessible at ` /docs/custom/url/path `
Custom Function
const docs = loader ({
source: mySource ,
baseUrl: '/docs' ,
slugs : ( file ) => {
// Custom logic
if ( file . data . permalink ) {
return file . data . permalink . split ( '/' );
}
// Return undefined to use default behavior
return undefined ;
},
});
Base URL
Set a base URL prefix for all pages:
const docs = loader ({
source: mySource ,
baseUrl: '/docs' , // All URLs start with /docs
});
// docs/guide.mdx -> /docs/guide
Multiple Loaders
Use different base URLs for different content types:
const docs = loader ({
source: docsSource ,
baseUrl: '/docs' ,
});
const blog = loader ({
source: blogSource ,
baseUrl: '/blog' ,
});
Custom URL Function
Completely customize URL generation:
const docs = loader ({
source: mySource ,
url : ( slugs , locale ) => {
// Custom URL logic
if ( locale ) {
return `/ ${ locale } / ${ slugs . join ( '-' ) } ` ;
}
return `/ ${ slugs . join ( '-' ) } ` ;
},
});
// File: docs/getting-started.mdx
// Slugs: ['getting-started']
// URL: /getting-started
Custom URL functions must return normalized URLs (leading slash, no trailing slash).
Internationalization
Fumadocs supports three locale visibility modes:
Never Hide Locale
const docs = loader ({
source: mySource ,
baseUrl: '/docs' ,
i18n: {
languages: [ 'en' , 'zh' , 'ja' ],
defaultLanguage: 'en' ,
hideLocale: 'never' , // Default
},
});
// en: /en/docs/guide
// zh: /zh/docs/guide
// ja: /ja/docs/guide
All URLs include the locale prefix.
Hide Default Locale
const docs = loader ({
source: mySource ,
baseUrl: '/docs' ,
i18n: {
languages: [ 'en' , 'zh' , 'ja' ],
defaultLanguage: 'en' ,
hideLocale: 'default-locale' ,
},
});
// en: /docs/guide (no locale prefix)
// zh: /zh/docs/guide
// ja: /ja/docs/guide
Default language URLs omit the locale prefix.
Always Hide Locale
const docs = loader ({
source: mySource ,
baseUrl: '/docs' ,
i18n: {
languages: [ 'en' , 'zh' ],
defaultLanguage: 'en' ,
hideLocale: 'always' ,
},
});
// All URLs: /docs/guide
// Locale determined by cookies or Accept-Language header
Locales are handled via middleware, not URLs.
Middleware for i18n
Use middleware to handle locale routing:
packages/core/src/i18n/middleware.ts
import { createI18nMiddleware } from 'fumadocs-core/i18n/middleware' ;
export default createI18nMiddleware ({
languages: [ 'en' , 'zh' , 'ja' ] ,
defaultLanguage: 'en' ,
hideLocale: 'default-locale' ,
}) ;
Middleware Behavior
Detect Locale
Extract locale from URL path, cookies, or Accept-Language header
Validate Locale
Ensure the locale is in the configured languages list
Rewrite or Redirect
Rewrite internal URLs or redirect to add/remove locale prefix
Set Cookie
Store user’s locale preference in cookies
const middleware = createI18nMiddleware ({
languages: [ 'en' , 'zh' ],
defaultLanguage: 'en' ,
format: {
get ( url ) {
// Extract locale from subdomain
const host = url . hostname . split ( '.' )[ 0 ];
return host === 'en' || host === 'zh' ? host : undefined ;
},
add ( url , locale ) {
// Add locale as subdomain
const next = new URL ( url );
next . hostname = ` ${ locale } . ${ url . hostname } ` ;
return next ;
},
remove ( url ) {
// Remove locale subdomain
const next = new URL ( url );
next . hostname = url . hostname . split ( '.' ). slice ( 1 ). join ( '.' );
return next ;
},
},
});
// en.example.com/docs/guide
// zh.example.com/docs/guide
Locale Parsing
Content sources support two locale parsing strategies:
Directory-based
Filename-based
docs/
en/
getting-started.mdx
api.mdx
zh/
getting-started.mdx
api.mdx
Configure with: i18n : {
parser : 'dir' , // Default
// ...
}
docs/
getting-started.en.mdx
getting-started.zh.mdx
api.en.mdx
api.zh.mdx
Configure with: i18n : {
parser : 'dot' ,
// ...
}
Path Utilities
Fumadocs provides cross-platform path utilities:
packages/core/src/source/path.ts
export function basename ( path : string , ext ?: string ) : string {
const idx = path . lastIndexOf ( '/' );
return path . substring ( idx === - 1 ? 0 : idx + 1 , ext ? path . length - ext . length : path . length );
}
export function dirname ( path : string ) : string {
return path . split ( '/' ). slice ( 0 , - 1 ). join ( '/' );
}
export function joinPath ( ... paths : string []) : string {
const out = [];
const parsed = paths . flatMap ( splitPath );
for ( const seg of parsed ) {
switch ( seg ) {
case '..' :
out . pop ();
break ;
case '.' :
break ;
default :
out . push ( seg );
}
}
return out . join ( '/' );
}
These utilities use forward slashes consistently, regardless of the operating system.
Resolving References
The loader provides methods to resolve links:
By Href
packages/core/src/source/loader.ts
getPageByHref (
href : string ,
options ?: {
language? : string ;
dir ?: string ; // Virtual path for resolving relative paths
}
): { page: Page ; hash ?: string } | undefined
Supports:
Relative paths : ./sibling.mdx, ../parent.mdx
Absolute URLs : /docs/guide
Hash fragments : ./page.mdx#section
Resolve Href
resolveHref ( href : string , parent : Page ): string
Converts relative file paths to absolute URLs:
const page = docs . getPage ([ 'guide' ]);
const resolved = docs . resolveHref ( './setup.mdx' , page );
// Returns: /docs/setup
Static Generation
Generate static params for SSG frameworks:
const docs = loader ({ /* ... */ });
// Next.js
export function generateStaticParams () {
return docs . generateParams ();
}
// Returns:
// [
// { slug: ['introduction'] },
// { slug: ['getting-started', 'installation'] },
// { slug: ['api', 'reference'] },
// ]
With i18n
export function generateStaticParams () {
return docs . generateParams ();
}
// Returns:
// [
// { slug: ['introduction'], lang: 'en' },
// { slug: ['introduction'], lang: 'zh' },
// { slug: ['installation'], lang: 'en' },
// { slug: ['installation'], lang: 'zh' },
// ]
Custom Parameter Names
export function generateStaticParams () {
return docs . generateParams ( 'path' , 'locale' );
}
// Returns:
// [
// { path: ['introduction'], locale: 'en' },
// { path: ['introduction'], locale: 'zh' },
// ]
URL Normalization
URLs are automatically normalized:
import { normalizeUrl } from 'fumadocs-core/utils/normalize-url' ;
normalizeUrl ( '/docs/guide/' ); // '/docs/guide'
normalizeUrl ( 'docs/guide' ); // '/docs/guide'
normalizeUrl ( '//docs///guide' ); // '/docs/guide'
Leading slash Always present
Trailing slash Always removed
Multiple slashes Collapsed to single
Empty segments Filtered out
Non-ASCII URLs
Fumadocs properly handles international characters:
// File: docs/安装指南.mdx
// Slugs: ['%E5%AE%89%E8%A3%85%E6%8C%87%E5%8D%97']
// URL: /docs/%E5%AE%89%E8%A3%85%E6%8C%87%E5%8D%97
// Browser displays: /docs/安装指南
The encodeURI() function preserves Unicode characters while ensuring URL compatibility.
Framework Integration
Fumadocs routing works with multiple frameworks:
Next.js App Router
Next.js Pages Router
React Router
app/docs/[[...slug]]/page.tsx
import { docs } from '@/lib/source' ;
export default async function Page ({ params }) {
const page = docs . getPage ( params . slug );
if ( ! page ) notFound ();
return < DocsPage page ={ page } />;
}
export function generateStaticParams () {
return docs . generateParams ();
}
import { docs } from '@/lib/source' ;
export async function getStaticPaths () {
return {
paths: docs . generateParams (). map (({ slug }) => ({
params: { slug },
})),
fallback: false ,
};
}
export async function getStaticProps ({ params }) {
const page = docs . getPage ( params . slug );
return { props: { page } };
}
import { docs } from '@/lib/source' ;
export const routes = [
{
path: '/docs/*' ,
element: < DocsLayout />,
children: docs . getPages (). map (( page ) => ({
path: page . url . replace ( '/docs/' , '' ),
element: < DocsPage page ={ page } />,
})),
},
];
Next Steps
Architecture Understand the overall system architecture
Internationalization Configure multi-language routing
Middleware Implement custom routing middleware
SEO Optimize URLs for search engines