圣堂之魂
Fumadocs 折腾记

新增内容区域操作指南

如何在 Fumadocs 博客中新增一个独立的内容区域(新的内容源 + 对应的页面路由)

本文档说明如何在本博客中新增一个独立的内容区域(即一个新的内容源 + 对应的页面路由)。当前已有的内容区域是 docs(访问路径 /docs),以下以新增一个名为 articles(访问路径 /articles)的内容区域为例。

替换说明

如果你想用其他名称,将下文所有 articles 替换为你的名称即可(注意大小写一致)。


整体流程概览

需要修改/创建的文件共 6 处

序号文件操作
1source.config.ts新增一个 defineDocs 导出
2lib/source.ts新增一个 source loader 导出
3app/articles/layout.tsx新建,页面布局
4app/articles/[[...slug]]/page.tsx新建,页面渲染
5content/articles/新建文件夹,存放 mdx 文件
6(可选)导航栏添加链接修改 lib/layout.shared.tsx

第一步:修改 source.config.ts

在项目根目录的 source.config.ts 中,新增一个 defineDocs 调用。

修改前

import { defineDocs, defineConfig } from 'fumadocs-mdx/config';
import { transformerMetaHighlight } from '@shikijs/transformers';

export const docs = defineDocs({
  dir: 'content/docs',
});

export default defineConfig({
  mdxOptions: {
    rehypeCodeOptions: {
      transformers: [transformerMetaHighlight()],
    } as any,
  },
});

修改后(新增部分已标注)

import { defineDocs, defineConfig } from 'fumadocs-mdx/config';
import { transformerMetaHighlight } from '@shikijs/transformers';

export const docs = defineDocs({
  dir: 'content/docs',
});

export const articles = defineDocs({
  dir: 'content/articles',
});

export default defineConfig({
  mdxOptions: {
    rehypeCodeOptions: {
      transformers: [transformerMetaHighlight()],
    } as any,
  },
});


第二步:修改 lib/source.ts

新增一个 source loader,用于将内容源转换为 Fumadocs 可用的页面树和路由数据。

修改前

import { createElement } from 'react';
import { icons as lucideIcons } from 'lucide-react';
import { docs } from 'collections/server';
import { loader } from 'fumadocs-core/source';
import { attachSidebarIcons } from '@/lib/sidebar-icons';

function resolveIcon(icon: string | undefined) {
  if (!icon) return undefined;
  const Component = (lucideIcons as Record<string, unknown>)[icon];
  if (Component) return createElement(Component as React.ComponentType);
  return undefined;
}

export const source = loader({
  baseUrl: '/docs',
  source: docs.toFumadocsSource(),
  icon: resolveIcon,
});

attachSidebarIcons(source.pageTree);

修改后(新增部分已标注)

import { createElement } from 'react';
import { icons as lucideIcons } from 'lucide-react';
import { docs, articles } from 'collections/server';
import { loader } from 'fumadocs-core/source';
import { attachSidebarIcons } from '@/lib/sidebar-icons';

function resolveIcon(icon: string | undefined) {
  if (!icon) return undefined;
  const Component = (lucideIcons as Record<string, unknown>)[icon];
  if (Component) return createElement(Component as React.ComponentType);
  return undefined;
}

export const source = loader({
  baseUrl: '/docs',
  source: docs.toFumadocsSource(),
  icon: resolveIcon,
});

export const articlesSource = loader({
  baseUrl: '/articles',
  source: articles.toFumadocsSource(),
  icon: resolveIcon,
});

attachSidebarIcons(source.pageTree);
attachSidebarIcons(articlesSource.pageTree);


第三步:创建路由文件夹和文件

app/ 目录下创建对应的路由结构。文件夹名即为访问路径。

layout.tsx

3.1 创建 app/articles/layout.tsx

import { articlesSource } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <DocsLayout
      tree={articlesSource.pageTree}
      {...baseOptions()}
    >
      {children}
    </DocsLayout>
  );
}

3.2 创建 app/articles/[[...slug]]/page.tsx

import { articlesSource } from '@/lib/source';
import {
  DocsBody,
  DocsDescription,
  DocsPage,
  DocsTitle,
} from 'fumadocs-ui/layouts/docs/page';
import { notFound } from 'next/navigation';
import { getMDXComponents } from '@/components/mdx';
import type { Metadata } from 'next';
import { createRelativeLink } from 'fumadocs-ui/mdx';

export default async function Page(props: {
  params: Promise<{ slug?: string[] }>;
}) {
  const params = await props.params;
  const page = articlesSource.getPage(params.slug);
  if (!page) notFound();

  const MDX = page.data.body;

  return (
    <DocsPage toc={page.data.toc} full={page.data.full} tableOfContent={{ style: 'clerk' }}>
      <DocsTitle>{page.data.title}</DocsTitle>
      <DocsDescription>{page.data.description}</DocsDescription>
      <DocsBody>
        <MDX
          components={getMDXComponents({
            a: createRelativeLink(articlesSource, page),
          })}
        />
      </DocsBody>
    </DocsPage>
  );
}

export async function generateStaticParams() {
  return articlesSource.generateParams();
}

export async function generateMetadata(props: {
  params: Promise<{ slug?: string[] }>;
}): Promise<Metadata> {
  const params = await props.params;
  const page = articlesSource.getPage(params.slug);
  if (!page) notFound();

  return {
    title: page.data.title,
    description: page.data.description,
  };
}

要点:所有 source.xxx 替换为 articlesSource.xxx。这与 app/docs/[[...slug]]/page.tsx 结构完全一致,只是换了数据源。


第四步:创建内容文件夹和示例文件

4.1 文件夹结构

content/ 目录下创建 articles/ 文件夹及示例文件:

index.mdx

4.2 创建首页文件 content/articles/index.mdx

---
title: 文章
description: 这里是文章区域
---

## 欢迎来到文章区域

这里是新的内容区域,可以开始写你的文章了。

4.3 meta.json 格式

最简格式:

{
  "title": "分类名称"
}

带图标的格式(需要 Lucide 图标支持):

{
  "title": "分类名称",
  "icon": "FolderIcon"
}

第五步(可选):在导航栏添加入口

修改 lib/layout.shared.tsx,在 links 数组中添加导航链接:

links: [
  {
    text: '文章',
    url: '/articles',
    active: 'nested-url',
  },
  // ... 原有的其他链接
],

或者如果导航栏使用的是 nav 配置中的其他字段,在对应位置添加即可。


第六步:重新构建

修改完成后,需要重启开发服务器(或重新构建),让 fumadocs-mdx 重新生成 .source/ 目录下的文件。

# 停止当前运行的开发服务器,然后重新启动
pnpm dev

如果是生产环境构建:

pnpm build

文件结构对照图

修改完成后的完整结构(标有 [新增] 的为新增部分):

source.config.ts
source.ts
layout.shared.tsx
layout.tsx
layout.tsx
layout.tsx
index.mdx

常见问题


快速添加模板(复制即用)

如果你需要添加第三个、第四个内容源,按以下模板替换 NAME 为你的内容源名称。

source.config.ts
source.ts
layout.tsx
index.mdx

source.config.ts -- 添加:

export const NAME = defineDocs({
  dir: 'content/NAME',
});

lib/source.ts -- 添加:

import { docs, NAME } from 'collections/server';

export const NAME_source = loader({
  baseUrl: '/NAME',
  source: NAME.toFumadocsSource(),
  icon: resolveIcon,
});

app/NAME/layout.tsx -- 新建:

import { NAME_source } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <DocsLayout tree={NAME_source.pageTree} {...baseOptions()}>
      {children}
    </DocsLayout>
  );
}

app/NAME/[[...slug]]/page.tsx -- 新建:

import { NAME_source } from '@/lib/source';
import {
  DocsBody,
  DocsDescription,
  DocsPage,
  DocsTitle,
} from 'fumadocs-ui/layouts/docs/page';
import { notFound } from 'next/navigation';
import { getMDXComponents } from '@/components/mdx';
import type { Metadata } from 'next';
import { createRelativeLink } from 'fumadocs-ui/mdx';

export default async function Page(props: {
  params: Promise<{ slug?: string[] }>;
}) {
  const params = await props.params;
  const page = NAME_source.getPage(params.slug);
  if (!page) notFound();

  const MDX = page.data.body;

  return (
    <DocsPage toc={page.data.toc} full={page.data.full} tableOfContent={{ style: 'clerk' }}>
      <DocsTitle>{page.data.title}</DocsTitle>
      <DocsDescription>{page.data.description}</DocsDescription>
      <DocsBody>
        <MDX
          components={getMDXComponents({
            a: createRelativeLink(NAME_source, page),
          })}
        />
      </DocsBody>
    </DocsPage>
  );
}

export async function generateStaticParams() {
  return NAME_source.generateParams();
}

export async function generateMetadata(props: {
  params: Promise<{ slug?: string[] }>;
}): Promise<Metadata> {
  const params = await props.params;
  const page = NAME_source.getPage(params.slug);
  if (!page) notFound();

  return {
    title: page.data.title,
    description: page.data.description,
  };
}

content/NAME/index.mdx -- 新建:

---
title: NAME 首页
description: NAME 区域描述
---

## 欢迎来到 NAME

在这里开始写内容。

然后重启开发服务器即可。


本页目录