コンテンツへスキップ
Fumadocs

ブログ

アプリにブログを追加します。

インストール

npx @fumadocs/cli@latest add blog

コンポーネントは components/blog に、投稿用のユーティリティは lib/blog.ts にインストールされます。

blogPageSchema を使って投稿用のコレクションを定義し、著者を登録します。

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 配下に作成します。

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 です。投稿のカードに表示されます

ページ

getPosts() は、ページの著者とタグを解決したうえで、新しい順に返します。返されたページは、Home Layout 配下のルートでレンダリングしてください。

投稿

src/routes/blog/index.tsx
import { createFileRoute } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
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';

const getBlogPosts = createServerFn({ method: 'GET' }).handler(() =>
  getPosts(blogSource.getPages(), blogAuthors),
);

export const Route = createFileRoute('/blog/')({
  loader: () => getBlogPosts(),
  component: Page,
});

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

投稿

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

src/routes/blog/$slug.tsx
import { createFileRoute, notFound } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
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';

const getPost = createServerFn({ method: 'GET' })
  .validator((slug: string) => slug)
  .handler(({ data: slug }) => {
    const page = blogSource.getPage([slug]);
    const found = page && findPost(getPosts(blogSource.getPages(), blogAuthors), page.url);
    if (!page || !found) throw notFound();

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

export const Route = createFileRoute('/blog/$slug')({
  loader: ({ params }) => getPost({ data: params.slug }),
  component: Page,
});

function Page() {
  const { path, post, newer, older } = Route.useLoaderData();
  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>
  );
}

タグ

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

src/routes/blog/tags/index.tsx
import { createFileRoute } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
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';

const getBlogTags = createServerFn({ method: 'GET' }).handler(() =>
  getTags(getPosts(blogSource.getPages())),
);

export const Route = createFileRoute('/blog/tags/')({
  loader: () => getBlogTags(),
  component: Page,
});

function Page() {
  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={Route.useLoaderData()} />
      </div>
    </HomeLayout>
  );
}

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

src/routes/blog/tags/$tag.tsx
import { createFileRoute, notFound } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
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';

const getTag = createServerFn({ method: 'GET' })
  .validator((tag: string) => tag)
  .handler(({ data: tag }) => {
    const result = getTagPosts(getPosts(blogSource.getPages(), blogAuthors), tag);
    if (!result) throw notFound();

    return result;
  });

export const Route = createFileRoute('/blog/tags/$tag')({
  loader: ({ params }) => getTag({ data: params.tag }),
  component: Page,
});

function Page() {
  const { tag, posts } = Route.useLoaderData();

  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>{tag.name}</BlogTitle>
        </BlogHeader>
        <BlogCards posts={posts} />
      </div>
    </HomeLayout>
  );
}

国際化

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

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

lib/blog.ts
import { i18n } from '@/lib/i18n';

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

投稿のフィードを公開する方法については、RSS フィードを参照してください。

How is this guide?

最終更新日