Fumadocs完全ガイド - Next.jsベースの最新ドキュメントフレームワーク


Fumadocs完全ガイド - Next.jsベースの最新ドキュメントフレームワーク

Fumadocsは、Next.js App Routerをベースにした最新のドキュメントフレームワークです。美しいUI、強力な検索機能、MDXサポートなど、プロフェッショナルなドキュメントサイトを簡単に構築できます。

この記事では、Fumadocsのセットアップから高度なカスタマイズまで、包括的に解説します。

Fumadocsとは

Fumadocsは、開発者向けドキュメントサイトを構築するための完全なソリューションです。

主な特徴

  • Next.js App Router: 最新のNext.js機能を活用
  • MDXサポート: マークダウンにReactコンポーネント埋め込み
  • 全文検索: Algolia、Flexsearch統合
  • 美しいUI: カスタマイズ可能なテーマ
  • TypeScript完全対応: 型安全な開発
  • 国際化対応: 多言語サポート

セットアップ

プロジェクト作成

# Fumadocsプロジェクト作成
npx create-fumadocs-app@latest my-docs

cd my-docs

# 依存関係インストール
npm install

# 開発サーバー起動
npm run dev

手動セットアップ

既存のNext.jsプロジェクトに追加:

npm install fumadocs-ui fumadocs-core fumadocs-mdx
// next.config.mjs
import { createMDX } from 'fumadocs-mdx/config'

const withMDX = createMDX()

/** @type {import('next').NextConfig} */
const config = {
  reactStrictMode: true,
}

export default withMDX(config)

ディレクトリ構造

my-docs/
├── app/
│   ├── layout.tsx
│   ├── page.tsx
│   └── docs/
│       ├── [[...slug]]/
│       │   └── page.tsx
│       └── layout.tsx
├── content/
│   └── docs/
│       ├── index.mdx
│       ├── getting-started.mdx
│       └── guides/
│           ├── installation.mdx
│           └── configuration.mdx
├── components/
├── lib/
│   └── source.ts
└── fumadocs.config.ts

基本設定

Fumadocs設定

// fumadocs.config.ts
import { defineConfig } from 'fumadocs-mdx/config'

export default defineConfig({
  mdxOptions: {
    rehypePlugins: [],
    remarkPlugins: [],
  },
})

ソース設定

// lib/source.ts
import { loader } from 'fumadocs-core/source'
import { createMDXSource } from 'fumadocs-mdx'
import { icons } from 'lucide-react'

export const source = loader({
  baseUrl: '/docs',
  icon: (icon) => {
    if (icon && icon in icons) {
      return icons[icon as keyof typeof icons]
    }
  },
  source: createMDXSource({
    // MDXファイルのパス
    files: './content/docs/**/*.mdx',
  }),
})

レイアウト

ルートレイアウト

// app/layout.tsx
import { RootProvider } from 'fumadocs-ui/provider'
import type { ReactNode } from 'react'
import './globals.css'

export default function RootLayout({
  children,
}: {
  children: ReactNode
}) {
  return (
    <html lang="ja" suppressHydrationWarning>
      <body>
        <RootProvider>{children}</RootProvider>
      </body>
    </html>
  )
}

ドキュメントレイアウト

// app/docs/layout.tsx
import { DocsLayout } from 'fumadocs-ui/layout'
import type { ReactNode } from 'react'
import { source } from '@/lib/source'

export default function Layout({
  children,
}: {
  children: ReactNode
}) {
  return (
    <DocsLayout
      tree={source.pageTree}
      nav={{
        title: 'My Docs',
        url: '/',
      }}
      links={[
        {
          text: 'GitHub',
          url: 'https://github.com/username/repo',
          icon: <GithubIcon />,
        },
      ]}
    >
      {children}
    </DocsLayout>
  )
}

ページコンポーネント

// app/docs/[[...slug]]/page.tsx
import { source } from '@/lib/source'
import { DocsPage, DocsBody } from 'fumadocs-ui/page'
import { notFound } from 'next/navigation'

export default async function Page({
  params,
}: {
  params: { slug?: string[] }
}) {
  const page = source.getPage(params.slug)

  if (!page) notFound()

  const MDX = page.data.exports.default

  return (
    <DocsPage
      toc={page.data.exports.toc}
      lastUpdate={page.data.lastModified}
    >
      <DocsBody>
        <h1>{page.data.title}</h1>
        <MDX />
      </DocsBody>
    </DocsPage>
  )
}

export async function generateStaticParams() {
  return source.generateParams()
}

export function generateMetadata({ params }: { params: { slug?: string[] } }) {
  const page = source.getPage(params.slug)

  if (!page) notFound()

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

MDXコンテンツ

基本的なMDX

---
title: はじめに
description: Fumadocsの基本的な使い方
---

# はじめに

Fumadocsへようこそ!

## インストール

プロジェクトを作成します:

```bash
npx create-fumadocs-app@latest

主な機能

  • 高速なビルド
  • 美しいUI
  • 簡単なセットアップ

### コンポーネント埋め込み

```mdx
---
title: コンポーネント例
---

import { Callout } from 'fumadocs-ui/components/callout'
import { Card } from '@/components/card'

# コンポーネント例

<Callout type="info">
  これは情報メッセージです。
</Callout>

<Card title="カスタムコンポーネント">
  MDX内でカスタムコンポーネントを使用できます。
</Card>

組み込みコンポーネント

Callout

import { Callout } from 'fumadocs-ui/components/callout'

<Callout type="info">
  情報メッセージ
</Callout>

<Callout type="warn">
  警告メッセージ
</Callout>

<Callout type="error">
  エラーメッセージ
</Callout>

Tabs

import { Tabs, Tab } from 'fumadocs-ui/components/tabs'

<Tabs items={['npm', 'pnpm', 'yarn']}>
  <Tab value="npm">
    ```bash
    npm install fumadocs-ui
    ```
  </Tab>
  <Tab value="pnpm">
    ```bash
    pnpm add fumadocs-ui
    ```
  </Tab>
  <Tab value="yarn">
    ```bash
    yarn add fumadocs-ui
    ```
  </Tab>
</Tabs>

Steps

import { Steps, Step } from 'fumadocs-ui/components/steps'

<Steps>
  <Step>
    ### インストール

    パッケージをインストールします。
  </Step>
  <Step>
    ### 設定

    設定ファイルを作成します。
  </Step>
  <Step>
    ### 実行

    アプリケーションを起動します。
  </Step>
</Steps>

Accordion

import { Accordions, Accordion } from 'fumadocs-ui/components/accordion'

<Accordions>
  <Accordion title="質問1">
    回答内容1
  </Accordion>
  <Accordion title="質問2">
    回答内容2
  </Accordion>
</Accordions>

全文検索

Flexsearch統合

npm install fumadocs-core/search/server
// app/api/search/route.ts
import { source } from '@/lib/source'
import { createSearchAPI } from 'fumadocs-core/search/server'

export const { GET } = createSearchAPI('search', {
  indexes: source.getPages().map((page) => ({
    title: page.data.title,
    description: page.data.description,
    content: page.data.content,
    url: page.url,
  })),
})

検索UI

// app/docs/layout.tsx
import { DocsLayout } from 'fumadocs-ui/layout'

export default function Layout({
  children,
}: {
  children: ReactNode
}) {
  return (
    <DocsLayout
      tree={source.pageTree}
      nav={{
        title: 'My Docs',
      }}
      // 検索設定
      search={{
        enabled: true,
        api: '/api/search',
      }}
    >
      {children}
    </DocsLayout>
  )
}

Algolia統合

npm install algoliasearch
import { DocsLayout } from 'fumadocs-ui/layout'
import algoliasearch from 'algoliasearch/lite'

const searchClient = algoliasearch(
  'APP_ID',
  'SEARCH_API_KEY'
)

export default function Layout({
  children,
}: {
  children: ReactNode
}) {
  return (
    <DocsLayout
      tree={source.pageTree}
      search={{
        enabled: true,
        algolia: {
          searchClient,
          indexName: 'docs',
        },
      }}
    >
      {children}
    </DocsLayout>
  )
}

テーマカスタマイズ

カラーテーマ

/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 240 10% 3.9%;
    --primary: 240 5.9% 10%;
    --primary-foreground: 0 0% 98%;
    --secondary: 240 4.8% 95.9%;
    --secondary-foreground: 240 5.9% 10%;
    --accent: 240 4.8% 95.9%;
    --accent-foreground: 240 5.9% 10%;
  }

  .dark {
    --background: 240 10% 3.9%;
    --foreground: 0 0% 98%;
    --primary: 0 0% 98%;
    --primary-foreground: 240 5.9% 10%;
    --secondary: 240 3.7% 15.9%;
    --secondary-foreground: 0 0% 98%;
    --accent: 240 3.7% 15.9%;
    --accent-foreground: 0 0% 98%;
  }
}

カスタムコンポーネント

// components/custom-callout.tsx
import { ReactNode } from 'react'

export function CustomCallout({
  children,
  type = 'info',
}: {
  children: ReactNode
  type?: 'info' | 'warn' | 'error'
}) {
  const styles = {
    info: 'bg-blue-50 border-blue-200 text-blue-900',
    warn: 'bg-yellow-50 border-yellow-200 text-yellow-900',
    error: 'bg-red-50 border-red-200 text-red-900',
  }

  return (
    <div className={`p-4 border-l-4 ${styles[type]}`}>
      {children}
    </div>
  )
}

MDXで使用:

import { CustomCallout } from '@/components/custom-callout'

<CustomCallout type="info">
  カスタムスタイルのCalloutコンポーネント
</CustomCallout>

国際化(i18n)

設定

// lib/i18n.ts
export const i18n = {
  defaultLanguage: 'ja',
  languages: ['ja', 'en'],
}

// lib/source.ts
import { loader } from 'fumadocs-core/source'
import { i18n } from './i18n'

export const source = loader({
  baseUrl: '/docs',
  i18n,
  source: createMDXSource({
    files: {
      ja: './content/docs/ja/**/*.mdx',
      en: './content/docs/en/**/*.mdx',
    },
  }),
})

言語切り替えUI

// components/language-toggle.tsx
'use client'

import { useParams, useRouter } from 'next/navigation'
import { i18n } from '@/lib/i18n'

export function LanguageToggle() {
  const params = useParams()
  const router = useRouter()
  const currentLang = params.lang || i18n.defaultLanguage

  return (
    <select
      value={currentLang}
      onChange={(e) => {
        router.push(`/${e.target.value}`)
      }}
    >
      {i18n.languages.map((lang) => (
        <option key={lang} value={lang}>
          {lang.toUpperCase()}
        </option>
      ))}
    </select>
  )
}

コードブロック

シンタックスハイライト

// fumadocs.config.ts
import { defineConfig } from 'fumadocs-mdx/config'
import rehypePrettyCode from 'rehype-pretty-code'

export default defineConfig({
  mdxOptions: {
    rehypePlugins: [
      [
        rehypePrettyCode,
        {
          theme: 'github-dark',
          keepBackground: false,
        },
      ],
    ],
  },
})

ファイル名表示

```typescript title="example.ts"
export function hello() {
  console.log('Hello, World!')
}

### 行ハイライト

```mdx
```typescript {2,4-6}
export function calculate(a: number, b: number) {
  const sum = a + b // ハイライト

  if (sum > 100) { // ハイライト開始
    return 100
  } // ハイライト終了

  return sum
}

## 目次(TOC)

### 自動生成

```tsx
// app/docs/[[...slug]]/page.tsx
import { DocsPage } from 'fumadocs-ui/page'

export default async function Page({
  params,
}: {
  params: { slug?: string[] }
}) {
  const page = source.getPage(params.slug)

  if (!page) notFound()

  return (
    <DocsPage
      // 目次を自動生成
      toc={page.data.exports.toc}
    >
      {/* コンテンツ */}
    </DocsPage>
  )
}

カスタムTOC

import { DocsPage } from 'fumadocs-ui/page'

export default function Page() {
  return (
    <DocsPage
      toc={[
        { title: 'セクション1', url: '#section-1', depth: 2 },
        { title: 'サブセクション', url: '#subsection', depth: 3 },
        { title: 'セクション2', url: '#section-2', depth: 2 },
      ]}
    >
      {/* コンテンツ */}
    </DocsPage>
  )
}

ナビゲーション

サイドバー構造

// lib/source.ts
export const source = loader({
  baseUrl: '/docs',
  source: createMDXSource({
    files: './content/docs/**/*.mdx',
  }),
  // ページツリー構造
  pageTree: {
    root: {
      name: 'ドキュメント',
      index: 'index',
      children: [
        {
          name: 'はじめに',
          index: 'getting-started',
        },
        {
          name: 'ガイド',
          children: [
            { name: 'インストール', index: 'guides/installation' },
            { name: '設定', index: 'guides/configuration' },
          ],
        },
      ],
    },
  },
})

パンくずリスト

import { Breadcrumb } from 'fumadocs-ui/components/breadcrumb'

export default function Page({ page }) {
  return (
    <>
      <Breadcrumb
        items={[
          { title: 'ホーム', url: '/' },
          { title: 'ドキュメント', url: '/docs' },
          { title: page.data.title, url: page.url },
        ]}
      />
      {/* コンテンツ */}
    </>
  )
}

メタデータとSEO

ページメタデータ

---
title: APIリファレンス
description: 詳細なAPIドキュメント
keywords: [API, リファレンス, ドキュメント]
---

Open Graph

// app/docs/[[...slug]]/page.tsx
export function generateMetadata({ params }: { params: { slug?: string[] } }) {
  const page = source.getPage(params.slug)

  if (!page) notFound()

  return {
    title: page.data.title,
    description: page.data.description,
    openGraph: {
      title: page.data.title,
      description: page.data.description,
      type: 'article',
      url: `https://example.com${page.url}`,
    },
    twitter: {
      card: 'summary_large_image',
      title: page.data.title,
      description: page.data.description,
    },
  }
}

デプロイ

Vercel

# Vercelにデプロイ
vercel

静的エクスポート

// next.config.mjs
const config = {
  output: 'export',
  images: {
    unoptimized: true,
  },
}

export default withMDX(config)
npm run build

高度な機能

カスタムプラグイン

// lib/plugins/custom-plugin.ts
import { visit } from 'unist-util-visit'

export function customPlugin() {
  return (tree: any) => {
    visit(tree, 'element', (node) => {
      if (node.tagName === 'a') {
        node.properties.target = '_blank'
        node.properties.rel = 'noopener noreferrer'
      }
    })
  }
}
// fumadocs.config.ts
import { customPlugin } from './lib/plugins/custom-plugin'

export default defineConfig({
  mdxOptions: {
    rehypePlugins: [customPlugin],
  },
})

動的インポート

// components/dynamic-demo.tsx
'use client'

import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('./chart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false,
})

export function DynamicDemo() {
  return <Chart data={[1, 2, 3]} />
}

ベストプラクティス

ディレクトリ構造

content/
└── docs/
    ├── index.mdx                    # トップページ
    ├── getting-started/
    │   ├── index.mdx               # 概要
    │   ├── installation.mdx        # インストール
    │   └── quick-start.mdx         # クイックスタート
    ├── guides/
    │   ├── index.mdx
    │   ├── basics.mdx
    │   └── advanced.mdx
    └── api/
        ├── index.mdx
        └── reference.mdx

パフォーマンス最適化

// next.config.mjs
const config = {
  // 画像最適化
  images: {
    formats: ['image/avif', 'image/webp'],
  },

  // 圧縮
  compress: true,

  // 実験的機能
  experimental: {
    optimizeCss: true,
  },
}

まとめ

Fumadocsは、Next.jsの最新機能を活用した強力なドキュメントフレームワークです。

主なメリット:

  • 簡単セットアップ: 数分で美しいドキュメントサイト
  • 優れた開発体験: MDX、TypeScript、ホットリロード
  • 高機能: 検索、i18n、テーマカスタマイズ
  • 高速: Next.js App Routerによる最適化

プロフェッショナルなドキュメントサイトを構築するなら、Fumadocsが最適な選択肢です。

参考リンク