Skip to main content

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

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:
docs/
  (getting-started)/
    introduction.mdx  -> /docs/introduction
    installation.mdx  -> /docs/installation
  (guides)/
    tutorial.mdx      -> /docs/tutorial
Groups organize files without affecting 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
});
page.mdx
---
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

1

Detect Locale

Extract locale from URL path, cookies, or Accept-Language header
2

Validate Locale

Ensure the locale is in the configured languages list
3

Rewrite or Redirect

Rewrite internal URLs or redirect to add/remove locale prefix
4

Set Cookie

Store user’s locale preference in cookies

Custom Formatter

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:
docs/
  en/
    getting-started.mdx
    api.mdx
  zh/
    getting-started.mdx
    api.mdx
Configure with:
i18n: {
  parser: 'dir',  // Default
  // ...
}

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:
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();
}

Next Steps

Architecture

Understand the overall system architecture

Internationalization

Configure multi-language routing

Middleware

Implement custom routing middleware

SEO

Optimize URLs for search engines

Build docs developers (and LLMs) love