# Fumadocs (Framework Mode, React Router): 部落格
URL: /zh-TW/docs/react-router/integrations/blog
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/[framework]/integrations/blog.zh-TW.mdx

為您的應用程式加入部落格。

## 安裝 [#installation]

```npm
npx @fumadocs/cli@latest add blog
```

此指令會將元件安裝至 `components/blog`，並將文章相關的工具函式安裝至 `lib/blog.ts`。

請使用 `blogPageSchema` 為您的文章定義一個集合，並註冊作者：

```ts title="lib/source.ts"
import { defineCollections } from 'fumadocs-mdx/macro';
import { loader } from 'fumadocs-core/source';
import { type BlogAuthor, blogPageSchema } from './blog';

export const blog = defineCollections({
  type: 'doc',
  dir: 'content/blog',
  schema: blogPageSchema,
});

export const blogSource = loader({
  baseUrl: '/blog',
  source: blog.toFumadocsSource(),
});

export const blogAuthors: Record<string, BlogAuthor> = {
  fuma: {
    name: 'Fuma Nama',
    title: 'Maintainer',
    url: 'https://fuma-nama.dev',
    image: 'https://avatars.githubusercontent.com/u/76240755',
  },
};
```

請將文章撰寫在 `content/blog` 目錄下：

```mdx title="content/blog/hello-world.mdx"
---
title: Hello World
description: My first post.
date: 2026-10-12
tags: [Announcement]
authors: [fuma]
image: /blog/hello-world.png
---
```

| 欄位        | 說明                         |
| --------- | -------------------------- |
| `date`    | 發布日期，文章會依此排序               |
| `tags`    | 每個標籤在 `/blog/tags` 下都有專屬頁面 |
| `authors` | 作者的鍵，無法識別的鍵會直接視為作者名稱       |
| `image`   | 封面圖片 URL，會顯示在文章卡片上         |

## 頁面 [#pages]

`getPosts()` 會解析頁面的作者與標籤，並依日期由新到舊排序。請在[首頁版面配置](/docs/ui/layouts/home-layout)底下的路由中算繪這些頁面。

### 文章 [#posts]

```tsx tab="app/routes/blog.tsx"
import type { Route } from './+types/blog';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
import { blogAuthors, blogSource } from '@/lib/source';
import { getPosts } from '@/lib/blog';
import { BlogCards } from '@/components/blog';

export function loader() {
  return getPosts(blogSource.getPages(), blogAuthors);
}

export default function Page({ loaderData }: Route.ComponentProps) {
  return (
    <HomeLayout {...baseOptions()}>
      <div className="mx-auto w-full max-w-(--fd-layout-width) px-4 py-12">
        <BlogCards posts={loaderData} />
      </div>
    </HomeLayout>
  );
}
```

```ts tab="app/routes.ts"
import { route, type RouteConfig } from '@react-router/dev/routes';

export default [
  // [!code ++:4]
  route('blog', 'routes/blog.tsx'),
  route('blog/:slug', 'routes/blog.post.tsx'),
  route('blog/tags', 'routes/blog.tags.tsx'),
  route('blog/tags/:tag', 'routes/blog.tag.tsx'),
] satisfies RouteConfig;
```

### 文章 [#post]

`findPost()` 會找出指定的文章，以及其前後較新與較舊的文章。若傳入 `toc`，浮動面板會顯示目錄和分享按鈕。

```tsx title="app/routes/blog.post.tsx"
import type { Route } from './+types/blog.post';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
import { blog, blogAuthors, blogSource } from '@/lib/source';
import { findPost, getPosts } from '@/lib/blog';
import { useMDXComponents } from '@/components/mdx';
import {
  BlogAuthors,
  BlogBackLink,
  BlogBody,
  BlogDate,
  BlogDescription,
  BlogHeader,
  BlogPage,
  BlogPostNav,
  BlogTags,
  BlogTitle,
} from '@/components/blog';

export function loader({ params }: Route.LoaderArgs) {
  const page = blogSource.getPage([params.slug]);
  const found = page && findPost(getPosts(blogSource.getPages(), blogAuthors), page.url);
  if (!page || !found) throw new Response('Not Found', { status: 404 });

  return { path: page.path, ...found };
}

export default function Page({ loaderData }: Route.ComponentProps) {
  const { path, post, newer, older } = loaderData;
  const page = blog.get(path)!;
  const MDX = page.body;

  return (
    <HomeLayout {...baseOptions()}>
      <BlogPage toc={page.toc}>
        <BlogHeader>
          <BlogBackLink href="/blog" />
          <BlogTitle>{post.title}</BlogTitle>
          <BlogDescription>{post.description}</BlogDescription>
          <BlogAuthors authors={post.authors} />
          <BlogTags tags={post.tags} />
          {post.date && <BlogDate date={post.date} />}
        </BlogHeader>
        <BlogBody>
          <MDX components={useMDXComponents()} />
        </BlogBody>
        <BlogPostNav newer={newer} older={older} />
      </BlogPage>
    </HomeLayout>
  );
}
```

### 標籤 [#tags]

使用 `getTags()` 列出所有標籤：

```tsx title="app/routes/blog.tags.tsx"
import type { Route } from './+types/blog.tags';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
import { blogSource } from '@/lib/source';
import { getPosts, getTags } from '@/lib/blog';
import { BlogHeader, BlogTagCards, BlogTitle } from '@/components/blog';

export function loader() {
  return getTags(getPosts(blogSource.getPages()));
}

export default function Page({ loaderData }: Route.ComponentProps) {
  return (
    <HomeLayout {...baseOptions()}>
      <div className="mx-auto flex w-full max-w-(--fd-layout-width) flex-col gap-4 px-4 py-12">
        <BlogHeader>
          <BlogTitle>All Tags</BlogTitle>
        </BlogHeader>
        <BlogTagCards tags={loaderData} />
      </div>
    </HomeLayout>
  );
}
```

接著使用 `getTagPosts()` 取得特定標籤下的文章；若標籤不存在，則會回傳 `undefined`：

```tsx title="app/routes/blog.tag.tsx"
import type { Route } from './+types/blog.tag';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
import { blogAuthors, blogSource } from '@/lib/source';
import { getPosts, getTagPosts } from '@/lib/blog';
import { BlogCards, BlogHeader, BlogTitle } from '@/components/blog';

export function loader({ params }: Route.LoaderArgs) {
  const result = getTagPosts(getPosts(blogSource.getPages(), blogAuthors), params.tag);
  if (!result) throw new Response('Not Found', { status: 404 });

  return result;
}

export default function Page({ loaderData }: Route.ComponentProps) {
  return (
    <HomeLayout {...baseOptions()}>
      <div className="mx-auto flex w-full max-w-(--fd-layout-width) flex-col gap-4 px-4 py-12">
        <BlogHeader>
          <BlogTitle>{loaderData.tag.name}</BlogTitle>
        </BlogHeader>
        <BlogCards posts={loaderData.posts} />
      </div>
    </HomeLayout>
  );
}
```

## 國際化 [#internationalization]

將某個語系的頁面傳入 `getPosts()`，例如 `blogSource.getPages(lang)`。

標籤頁面的 URL 由 `lib/blog.ts` 中的 `getBlogUrl` 產生。請將您的 i18n 設定傳給它，使其與載入器的 URL 保持一致：

```ts title="lib/blog.ts"
import { i18n } from '@/lib/i18n';

export const getBlogUrl = createGetUrl('/blog', i18n);
```

若要為您的文章發布 RSS 摘要，請參閱 [RSS 摘要](/docs/react-router/guides/rss)。