Docus実践ガイド — Nuxt Contentからの移行と運用ノウハウ

Docus実践ガイド — Nuxt Contentからの移行と運用ノウハウ

Docusの具体的な導入手順、app.config.ts設定、カスタムコンポーネント実装、パフォーマンス最適化、トラブルシューティングまで網羅した実践ガイド。

はじめに

この記事は、前回の概要編でDocusの導入を決めた方に向けて、具体的な実装手順とノウハウを提供します。

この記事の対象読者:

  • Docus導入を決定し、実装フェーズに入った方
  • 具体的な設定ファイルやコード例が必要な方
  • トラブルシューティングや最適化のノウハウを求めている方

この記事で扱うこと:

  • プロジェクトのセットアップ手順
  • app.config.tsの実践的な設定例
  • Frontmatter設計パターン
  • カスタムコンポーネントの実装
  • Tailwind CSS統合とスタイルカスタマイズ
  • パフォーマンス最適化の実践
  • よくあるトラブルシューティング
  • 運用効率化のスクリプトとチェックリスト

1. プロジェクトセットアップ

新規プロジェクトの作成

# Docusテンプレートでプロジェクト作成
npx nuxi init docs -t docus
cd docs

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

# 開発サーバーの起動
pnpm dev

ブラウザで http://localhost:3000 にアクセスし、Docusのデフォルトページが表示されることを確認します。

ディレクトリ構造の設計

docs/
├── content/               # Markdownコンテンツ
│   ├── 1.intro/          # 章1(orderで制御)
│   │   ├── index.md
│   │   ├── 1.getting-started.md
│   │   └── 2.installation.md
│   ├── 2.guide/          # 章2
│   │   ├── index.md
│   │   └── 1.basic-usage.md
│   └── index.md          # トップページ
├── public/               # 静的アセット
│   ├── images/           # 画像ファイル
│   ├── favicon.ico
│   └── og-image.png
├── components/           # Vueコンポーネント
│   └── content/          # Markdownで使用するコンポーネント
│       ├── LinkCard.vue
│       └── Badge.vue
├── app.config.ts         # Docus設定
├── nuxt.config.ts        # Nuxt設定
└── package.json

ディレクトリ設計のポイント:

  • 章 = ディレクトリ、ページ = .md ファイル
  • ディレクトリ名の先頭に数字を付けて順序を明示(1.intro, 2.guide
  • 各章に index.md を配置して章の概要を記載
  • 画像は public/images/ に配置し、絶対パス /images/... で参照

既存プロジェクトへの統合

既存のNuxtプロジェクトにDocusを追加する場合:

# Docusパッケージのインストール
pnpm add docus

# 必要に応じて追加パッケージをインストール
pnpm add @nuxt/content @nuxt/ui

nuxt.config.ts に以下を追加:

export default defineNuxtConfig({
  extends: ['docus'],
  // その他の既存設定...
})

2. コンテンツ移設とFrontmatter設計

コンテンツの移設手順

  1. 既存Markdownを content/ 配下へコピー:
    • 章ごとにディレクトリを作成
    • ファイル名の先頭に数字を付けて順序を明示(1.getting-started.md
  2. 画像アセットの移設:
    • public/images/ 配下に配置
    • Markdown内のパスを /images/... に修正
  3. Frontmatterの整形:
    • 必須項目(title, description, order)を追加
    • 後述のテンプレートを活用

Frontmatter設計パターン

ページ用テンプレート

---
title: ページのタイトル
description: 120字以内の要約(検索とOGPカード用)
order: 10
draft: false
navigation:
  title: サイドバー表示名(省略可。titleと同じなら削除)
  icon: i-heroicons-book-open  # アイコン(省略可)
---

セクション用テンプレート(index.md

---
title: セクション名
description: セクションの概要
order: 20
navigation:
  icon: i-heroicons-folder
---

ナビ非表示ページ

---
title: ナビに表示しないページ
description: プライバシーポリシーなど
navigation: false
---

ドラフト管理

---
title: 執筆中の記事
description: まだ公開していない記事
draft: true  # ビルド時に除外される
---

Frontmatter一括整形スクリプト

既存Markdownに必須項目を一括追加するスクリプト:

// scripts/fix-frontmatter.ts
import fg from 'fast-glob'
import fs from 'node:fs/promises'
import matter from 'gray-matter'

function inferTitleFromHeading(content: string) {
  const m = content.match(/^#\s+(.+)$/m)
  return m ? m[1].trim() : 'タイトル未設定'
}

function summarize(content: string) {
  return content
    .replace(/`{1,3}[\s\S]*?`{1,3}/g, '')    // コードを除去
    .replace(/\[(.*?)\]\(.*?\)/g, '$1')      // リンクをテキスト化
    .replace(/\s+/g, ' ')
    .trim()
    .slice(0, 120)
}

const files = await fg('content/**/*.md', { dot: false })

for (const file of files) {
  const raw = await fs.readFile(file, 'utf8')
  const parsed = matter(raw)
  const data: any = parsed.data || {}

  // 必須項目の補完
  data.title ||= inferTitleFromHeading(parsed.content)
  data.description = data.description || summarize(parsed.content)
  data.order ??= 999
  data.draft ??= false

  const next = matter.stringify(parsed.content, data)
  await fs.writeFile(file, next)
}

console.log(`Updated ${files.length} files`)

使用方法:

# 依存関係のインストール
pnpm add -D fast-glob gray-matter tsx

# package.jsonにスクリプトを追加
# "frontmatter:fix": "tsx scripts/fix-frontmatter.ts"

# 実行
pnpm frontmatter:fix

3. app.config.tsの実践的設定

ドキュメントルート直下に app.config.ts を配置し、Docusの動作をカスタマイズします。

基本設定

app.config.ts
import { defineAppConfig } from '#app'

export default defineAppConfig({
  docus: {
    // サイト情報
    title: 'サイト名',
    description: 'サイトの説明文',
    image: '/images/og-image.png',
    url: 'https://example.com',

    // ソーシャルリンク
    socials: {
      twitter: '@handle',
      github: 'org/repo',
      linkedin: 'company-name',
      youtube: 'channel-id'
    },

    // ヘッダー設定
    header: {
      logo: {
        src: '/images/logo.svg',
        alt: 'サイトロゴ',
        width: 40,
        height: 40
      },
      title: 'サイト名',
      showLinkIcon: true,  // ナビリンクにアイコン表示
      fluid: true          // 全幅レイアウト
    },

    // サイドバー設定
    aside: {
      level: 1,            // 目次で拾う見出し階層の開始(h1から)
      collapsed: false,    // デフォルトで開く
      exclude: []          // 除外するパス(例: ['/blog/*'])
    },

    // 目次(TOC)設定
    toc: {
      title: 'このページの目次',
      depth: 2,            // h2まで表示(3にするとh3まで)
      searchDepth: 2
    },

    // メインコンテンツ設定
    main: {
      padded: true,        // コンテンツに余白を追加
      fluid: false         // 全幅レイアウト(falseで最大幅制限)
    },

    // フッター設定
    footer: {
      credits: {
        enabled: true,
        repository: 'https://github.com/org/repo'
      },
      textLinks: [
        { text: 'プライバシーポリシー', href: '/privacy' },
        { text: 'お問い合わせ', href: '/contact' }
      ],
      iconLinks: [
        {
          label: 'GitHub',
          href: 'https://github.com/org/repo',
          icon: 'simple-icons:github'
        },
        {
          label: 'X',
          href: 'https://x.com/handle',
          icon: 'simple-icons:x'
        }
      ]
    },

    // GitHub統合("Edit this page on GitHub"リンク)
    github: {
      owner: 'org',
      repo: 'repo',
      branch: 'main',
      dir: 'content',
      edit: true           // 編集リンクを表示
    }
  }
})

検索設定(DocSearch統合)

ページ数が増えたら、Algolia DocSearchに移行:

export default defineAppConfig({
  docus: {
    // ... 既存設定

    // DocSearch設定(Algoliaから取得した認証情報)
    algolia: {
      appId: 'YOUR_APP_ID',
      apiKey: 'YOUR_SEARCH_API_KEY',
      indexName: 'YOUR_INDEX_NAME',
      langAttribute: 'lang',
      docSearch: {
        placeholder: 'サイト内を検索',
        translations: {
          button: {
            buttonText: '検索',
            buttonAriaLabel: 'サイト内を検索'
          },
          modal: {
            searchBox: {
              cancelButtonText: 'キャンセル',
              resetButtonTitle: 'クリア'
            }
          }
        }
      }
    }
  }
})

4. ページ構成とコンポーネント設計

Docusのページルーティング戦略

DocusはContent-Drivenな設計を採用しており、ページ構成には2つのアプローチがあります。

アプローチ1: content/ ディレクトリでルーティング(推奨)

トップページ(/)の例:

content/index.md
---
title: Nuxtation
description: ブログと図書館のコンテンツを集約したサイト
navigation: false
layout: page
---

<div class="not-prose">
  <home-landing />
</div>

仕組み:

  1. content/index.md が存在 → Docusが / ルートを自動生成
  2. マークダウン内で <home-landing /> を使用してVueコンポーネントを埋め込み
  3. pages/index.vue不要(重複を避ける)

メリット:

  • フロントマター(title, description, layout)でメタデータを管理
  • コンテンツとコードの分離
  • SEOに必要な情報をマークダウンで一元管理

アプローチ2: pages/ ディレクトリで独自UI(一覧ページ向け)

ブログ一覧(/blog)の例:

pages/blog/index.vue
<template>
  <BlogIndexPage />
</template>
components/blog/BlogIndexPage.vue
<script setup lang="ts">
// 複雑なロジック: データ取得、ページネーション、フィルタリング
const { data: articlesData } = useLazyAsyncData('docs-blog-articles', async () => {
  const articles = await queryCollection('blog').all()
  return articles.map(article => ({
    title: article.title,
    path: article.path,
    // ... 必要なフィールドのみ
  }))
})

// ページネーションロジック
const currentPage = computed(() => Number(route.query.page) || 1)
const paginatedArticles = computed(() => {
  // ...
})
</script>

<template>
  <!-- 200行以上の複雑なUI -->
</template>

なぜ pages/index.vuecomponents/**IndexPage.vue なのか:

項目pages/blog/index.vuecomponents/BlogIndexPage.vue
責務ルーティングのみビジネスロジック + UI
行数3行200行以上
テスト不要単体テスト可能
再利用不可他ページから参照可能
Storybook不可プレビュー可能

設計原則:

  1. 関心の分離(Separation of Concerns)
    • pages/ は薄く保ち、ロジックは components/ に
  2. 一貫性のあるパターン
    pages/blog/index.vue → BlogIndexPage.vue
    pages/biblio/index.vue → BiblioIndexPage.vue
    pages/jenre/tags/[slug].vue → JenreTagsPage.vue
    
  3. Nuxt 3 / Docus のベストプラクティス
    • コンポーネントの自動インポート機能を活用
    • グローバル登録(components/global: true)で簡潔に

ページ構成の実例

現在のプロジェクト構造:

app/
├── pages/
│   ├── blog/
│   │   └── index.vue              # → BlogIndexPage
│   ├── biblio/
│   │   └── index.vue              # → BiblioIndexPage
│   └── jenre/
│       └── tags/
│           └── [slug].vue         # → JenreTagsPage(存在する場合)
├── components/
│   ├── blog/
│   │   ├── BlogIndexPage.vue      # 複雑なロジック + UI
│   │   └── BlogCardHorizontal.vue
│   ├── biblio/
│   │   ├── BiblioIndexPage.vue    # 複雑なロジック + UI
│   │   └── BiblioCard.vue
│   └── content/
│       └── home/
│           └── HomeLanding.vue    # トップページのUI
└── content/
    ├── index.md                   # / → <home-landing />
    ├── blog/
    │   └── *.md                   # 個別記事
    └── biblio/
        └── *.md                   # 個別書籍

使い分けの基準:

ページタイプ使用するアプローチ理由
トップページcontent/index.mdコンテンツ主体、SEO最適化
記事詳細content/blog/*.mdマークダウンで執筆
一覧ページpages/ + components/複雑なUI、動的データ
カスタムページpages/ + components/独自レイアウト必要

参考: カスタムコンポーネント(LinkCard等)の実装例は、前回の記事06を参照してください。


5. Tailwind CSS統合とスタイルカスタマイズ

Tailwind CSS v4の統合

Docus 5.2.0以降、Tailwind CSS v4が標準統合されています。

カスタムカラーの定義app/assets/css/tailwind.css):

app/assets/css/tailwind.css
@reference "tailwindcss";

/* カスタムカラー */
:root {
  --color-primary-head: #380964;
  --color-sf-500: #4f46e5;
  --color-sf-600: #4338ca;
}

.dark {
  --color-primary-head: #9333ea;
}

コンポーネントでのTailwind活用

@apply ディレクティブで既存CSSをTailwindユーティリティに変換:

<style scoped>
@reference "tailwindcss";

/* Before: 従来のCSS */
.hero {
  position: relative;
  width: 100%;
  height: 500px;
  overflow: hidden;
}

@media (max-width: 768px) {
  .hero {
    height: 400px;
  }
}

/* After: Tailwind CSS */
.hero {
  @apply relative w-full h-[500px] overflow-hidden md:h-[400px];
}

.hero__overlay {
  @apply absolute inset-0 bg-gradient-to-b from-black/10 to-black/30;
}
</style>

Nuxt UI v4のuiプロップ活用

:deep()!importantを最小限に抑える:

<template>
  <UCard
    :ui="{
      root: 'overflow-hidden rounded-md ring-1 ring-gray-200 dark:ring-gray-800',
      body: 'p-0 sm:p-0',
      header: 'p-0 sm:p-0',
    }"
    class="my-3 not-prose"
  >
    <!-- コンテンツ -->
  </UCard>
</template>

重要ポイント:

  • not-proseクラスで.proseスタイルの影響を除外
  • uiプロップで全ブレークポイントのスタイルを制御
  • レスポンシブ指定(sm:md:)を必ず含める

MDCコンポーネントのスタイリング

::code-collapseボタンの視認性向上(app/assets/css/prose.css):

/* code-collapse ボタンのスタイル */
.prose div[class*="bg-gradient-to-t from-muted"] button,
.prose button[data-state="closed"],
.prose button[data-state="open"] {
  background-color: #d1d5db !important;
  padding: 0.5rem 1rem !important;
  border-radius: 0.375rem !important;
  transition: background-color 0.2s ease !important;
  color: #1f2937 !important;
  font-weight: 500 !important;
  opacity: 0.5;
}

.prose div[class*="bg-gradient-to-t from-muted"] button:hover,
.prose button[data-state="closed"]:hover,
.prose button[data-state="open"]:hover {
  background-color: #9ca3af !important;
  opacity: 1;
}

/* ダークモード */
.dark .prose div[class*="bg-gradient-to-t from-muted"] button,
.dark .prose button[data-state="closed"],
.dark .prose button[data-state="open"] {
  background-color: #374151 !important;
  color: #d1d5db !important;
  opacity: 0.5;
}

.dark .prose div[class*="bg-gradient-to-t from-muted"] button:hover,
.dark .prose button[data-state="closed"]:hover,
.dark .prose button[data-state="open"]:hover {
  background-color: #4b5563 !important;
  opacity: 1;
}

MDC構文の注意点:

::code-collapse を使用する際、内側のコードブロック開始マーカー(```)は必ず行頭から始める必要があります。

  • 誤り: ```typescript の前にスペースやタブ文字がある(インデントされている状態)
  • 正しい: ```typescript が行の最初の文字から始まる(インデントなし)

正しい記述の構造:

  1. 1行目: ::code-collapse
  2. 2行目: 空行
  3. 3行目: ```typescript行頭から始める(スペースを入れない)
  4. 4行目以降: コード内容
  5. コード終了: ```
  6. 最終行: ::code-collapse を閉じる

6. パフォーマンス最適化の実践

画像最適化(WebP変換)

変換コマンド:

cd public/img/blog
for file in *.png; do
  cwebp -q 80 "$file" -o "${file%.png}.webp"
done

<NuxtPicture>コンポーネントの活用:

<NuxtPicture
  :src="heroImage.src"
  :width="heroImage.width"
  :height="heroImage.height"
  :alt="heroImage.alt"
  :img-attrs="{ class: 'hero-image', loading: 'eager' }"
  sizes="sm:100vw md:1356px lg:1356px"
  :modifiers="{ fit: 'cover', quality: 80 }"
  format="webp"
/>

効果:

  • PNG → WebP変換で84%削減(9.5MB → 1.5MB)
  • レスポンシブ画像配信でモバイル81%削減

サードパーティスクリプトの最適化

@nuxt/scriptsで統合管理(nuxt.config.ts):

export default defineNuxtConfig({
  modules: [
    '@nuxt/scripts',
    // ...
  ],

  scripts: {
    registry: {
      clarity: process.env.NODE_ENV === 'production' ? {
        id: 'xxxxxxxx',
        trigger: 'idle'
      } : false,
      googleAnalytics: process.env.NODE_ENV === 'production' ? {
        id: 'G-xxxxxxx',
        trigger: 'idle'
      } : false
    },
  },
})

重要: $productionブロック内ではなく、トップレベルに配置し、process.env.NODE_ENVで制御します。


7. トラブルシューティング

ナビゲーションの順序が意図通りでない

問題: ディレクトリの並び順が不明瞭で、意図しない順序で表示される

解決策:

  1. ディレクトリの index.mdorder を持たせる
  2. 同一階層は order でソート、未指定は末尾へ流す
---
title: イントロダクション
order: 10
---

見出しとTOCが見づらい

問題: 見出しレベルがバラバラで、TOCが見づらい

解決策:

  1. h2/h3 中心に構造化し、h4以降は極力使わない
  2. TOC深度は 2 か 3 に固定(app.config.tstoc.depth
  3. 長すぎる見出しは簡潔に書き直す

画像が表示されない

問題: 相対パスのズレでビルドエラーや画像が表示されない

解決策:

  1. public/ 配下に配置し、絶対パス /images/... で参照
  2. 記事固有の画像は content/ 相対でも可だが、一貫性を優先

Hydration Mismatchエラー

問題: SSRとクライアントサイドレンダリングの不一致

Hydration completed but contains mismatches.

解決策: <ClientOnly> でラップ

<template>
  <ClientOnly>
    <UColorModeButton />
  </ClientOnly>
</template>

メタデータ欠落の補完

問題: description 欠落や見出し深度の揺れで、検索やOGPが不完全

解決策:

  1. 前述のFrontmatter一括整形スクリプトを実行
  2. titledescriptionorderdraft を必須化
  3. CI で Frontmatter の必須項目チェックを自動化

8. 運用効率化のチェックリストとツール

デプロイ前チェックリスト

  • ✅ すべてのMarkdownに必須Frontmatter(title, description)が存在
  • ✅ リンク切れチェック済み(pnpm linkcheck
  • ✅ 見出し深度の統一(h2/h3中心)
  • ✅ 画像の最適化(WebP変換済み)
  • ✅ Lighthouseスコア確認(Performance 75点以上)

リンクチェックスクリプト

# broken-link-checkerのインストール
pnpm add -D broken-link-checker

# package.jsonにスクリプトを追加
# "linkcheck": "blc http://localhost:3000 -ro"

# 実行(開発サーバー起動後)
pnpm linkcheck

リダイレクト管理

Vercel(vercel.json:

{
  "redirects": [
    { "source": "/old-path", "destination": "/docs/new-path", "permanent": true },
    { "source": "/legacy/:slug", "destination": "/docs/:slug", "permanent": true }
  ]
}

Netlify(_redirects:

/old-path           /docs/new-path   301
/legacy/:slug       /docs/:slug      301

9. まとめと次のステップ

核心的な学び

  1. 設定ドリブンの運用: app.config.ts で一元管理し、最小カスタマイズ運用
  2. Tailwind CSS統合: @apply ディレクティブで保守性向上、未使用CSS自動削除
  3. 段階的な改善: 初期は最小設定で運用開始、運用しながら必要な機能を追加

避けた落とし穴

  • ❌ 大規模なカスタムCSS開発(Tailwindで解決)
  • ❌ 過度な機能追加(必要最小限に留める)
  • ❌ スタイルの過剰な上書き(既定テーマを尊重)

短期的な次のステップ(1-2ヶ月)

  • パフォーマンス最適化の継続(Lighthouseスコア追跡)
  • 記事テンプレの運用徹底
  • 残りのコンポーネントのTailwind v4移行

中期的な次のステップ(3-6ヶ月)

  • DocSearch導入(ページ数の閾値到達時)
  • 更新頻度の高い領域の章立て再設計
  • Nuxt Studio導入検討(執筆体験のさらなる向上)

長期的な次のステップ(6ヶ月以降)

  • i18n対応(多言語展開)
  • バージョニング機能(ドキュメントのバージョン管理)
  • デザインシステムの構築(Tailwindベースのコンポーネントライブラリ化)

参考リソース

公式ドキュメント:

関連記事(本サイト内):

  • 概要編: Docus導入の判断材料
  • Tailwind CSS移行ガイド(予定): スタイルカスタマイズの詳細
  • パフォーマンス最適化実践(予定): Core Web Vitals改善の詳細

この記事が、Docusの導入と運用の参考になれば幸いです。質問やフィードバックがあれば、ぜひコメントやSNSでお寄せください。