# Fumadocs (Framework Mode, Waku): ブログ
URL: /ja/docs/waku/integrations/blog
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/[framework]/integrations/blog.ja.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()` は、ページの著者とタグを解決したうえで、新しい順に返します。返されたページは、[Home Layout](/docs/ui/layouts/home-layout) 配下のルートでレンダリングしてください。

### 投稿 [#posts]

```tsx title="src/pages/(home)/blog/index.tsx"
import { blogAuthors, blogSource } from '@/lib/source';
import { getPosts } from '@/lib/blog';
import { BlogCards } from '@/components/blog';

export default function Page() {
  return (
    <div className="mx-auto w-full max-w-(--fd-layout-width) px-4 py-12">
      <BlogCards posts={getPosts(blogSource.getPages(), blogAuthors)} />
    </div>
  );
}

export const getConfig = async () => ({ render: 'static' }) as const;
```

### 投稿 [#post]

`findPost()` は、投稿とその前後の投稿 (新しい投稿と古い投稿) をまとめて取得します。`toc` を指定すると、目次と共有ボタンを含むフローティングパネルが表示されます。

```tsx title="src/pages/(home)/blog/[slug].tsx"
import type { PageProps } from 'waku/router';
import { unstable_notFound } from 'waku/router/server';
import { blogAuthors, blogSource } from '@/lib/source';
import { findPost, getPosts } from '@/lib/blog';
import { getMDXComponents } from '@/components/mdx';
import {
  BlogAuthors,
  BlogBackLink,
  BlogBody,
  BlogDate,
  BlogDescription,
  BlogHeader,
  BlogPage,
  BlogPostNav,
  BlogTags,
  BlogTitle,
} from '@/components/blog';

export default function Page({ slug }: PageProps<'/blog/[slug]'>) {
  const page = blogSource.getPage([slug]);
  const found = page && findPost(getPosts(blogSource.getPages(), blogAuthors), page.url);
  if (!page || !found) unstable_notFound();

  const { post, newer, older } = found;
  const MDX = page.data.body;

  return (
    <BlogPage toc={page.data.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={getMDXComponents()} />
      </BlogBody>
      <BlogPostNav newer={newer} older={older} />
    </BlogPage>
  );
}

export async function getConfig() {
  return {
    render: 'static',
    staticPaths: blogSource.getPages().map((page) => page.slugs),
  } as const;
}
```

### タグ [#tags]

`getTags()` を使うと、すべてのタグを一覧表示できます。

```tsx title="src/pages/(home)/blog/tags/index.tsx"
import { blogSource } from '@/lib/source';
import { getPosts, getTags } from '@/lib/blog';
import { BlogHeader, BlogTagCards, BlogTitle } from '@/components/blog';

export default function Page() {
  const tags = getTags(getPosts(blogSource.getPages()));

  return (
    <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={tags} />
    </div>
  );
}

export const getConfig = async () => ({ render: 'static' }) as const;
```

また、特定のタグが付いた投稿は `getTagPosts()` で表示できます。存在しないタグの場合、この関数は `undefined` を返します。

```tsx title="src/pages/(home)/blog/tags/[tag].tsx"
import type { PageProps } from 'waku/router';
import { unstable_notFound } from 'waku/router/server';
import { blogAuthors, blogSource } from '@/lib/source';
import { getPosts, getTagPosts, getTags } from '@/lib/blog';
import { BlogCards, BlogHeader, BlogTitle } from '@/components/blog';

export default function Page({ tag }: PageProps<'/blog/tags/[tag]'>) {
  const result = getTagPosts(getPosts(blogSource.getPages(), blogAuthors), tag);
  if (!result) unstable_notFound();

  return (
    <div className="mx-auto flex w-full max-w-(--fd-layout-width) flex-col gap-4 px-4 py-12">
      <BlogHeader>
        <BlogTitle>{result.tag.name}</BlogTitle>
      </BlogHeader>
      <BlogCards posts={result.posts} />
    </div>
  );
}

export async function getConfig() {
  return {
    render: 'static',
    staticPaths: getTags(getPosts(blogSource.getPages())).map((tag) => tag.slug),
  } as const;
}
```

## 国際化 [#internationalization]

`getPosts()` には、`blogSource.getPages(lang)` のように特定のロケールのページを渡してください。

タグページのURLは `lib/blog.ts` の `getBlogUrl` によって生成されます。ローダーのURLと一致させるには、`getBlogUrl` に i18n の設定を渡してください。

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

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

投稿のフィードを公開する方法については、[RSS フィード](/docs/waku/guides/rss)を参照してください。