• #feature
  • #ui
  • #navigation
  • #toc
開発blog-platform完了

目次(Table of Contents)機能

概要

ドキュメントページの右側に固定表示される目次(TOC)機能です。現在のスクロール位置に応じて、該当する見出しがハイライト表示されます。

機能仕様

表示対象

  • 見出しレベル: H1、H2、H3の見出しを自動抽出
  • 表示位置: ページ右側にスティッキー配置
  • レスポンシブ: 1024px以下の画面では非表示

ハイライト動作

アクティブ状態(現在位置)

  • テキストカラー: #24292e(通常の黒色)
  • フォントウェイト: 600(太字)
  • 左ボーダー: 青色(#0969da)の2pxボーダー
  • 背景: 薄い青色のハイライト(rgba(9, 105, 218, 0.05)
  • 透明度: opacity: 1(完全に表示)

非アクティブ状態

  • テキストカラー: #6e7781(グレー)
  • 透明度: opacity: 0.7(薄く表示)
  • 左ボーダー: なし(transparent

ホバー時

  • テキストカラー: #0969da(青色)
  • 透明度: opacity: 1(完全に表示)

インデント階層

見出しレベルに応じて左インデントを設定:

  • H1: padding-left: 0.75rem(太字表示)
  • H2: padding-left: 1.5rem(標準サイズ)
  • H3: padding-left: 2.25rem(小さめのフォント 0.8125rem

技術実装

使用技術

  1. Vue 3 Composition API
    • ref, computed, onMounted, onUnmountedを使用
    • リアクティブな状態管理
  2. Intersection Observer API
    • スクロール位置の検出
    • パフォーマンスの最適化
  3. Nuxt Content v3
    • doc.body.toc.linksからTOCデータを取得

コンポーネント構成

TableOfContents.vue

Props:

interface TocLink {
  id: string        // 見出しのID(アンカーリンク用)
  text: string      // 見出しのテキスト
  depth: number     // 見出しのレベル(1〜3)
  children?: TocLink[]  // ネストされた見出し
}

const props = defineProps<{
  links?: TocLink[]
}>()

主な機能:

  • flatLinks: ネストされたリンクをフラット化(H1〜H3のみ)
  • activeId: 現在アクティブな見出しのID
  • scrollToHeading(): スムーズスクロール機能
  • setupObserver(): Intersection Observerのセットアップ

DocPage.vue

2カラムレイアウトに変更:

<template>
  <div class="doc-container">
    <article class="doc">
      <!-- メインコンテンツ -->
    </article>
    <aside class="doc-sidebar">
      <TableOfContents :links="doc.body?.toc?.links" />
    </aside>
  </div>
</template>

レイアウト:

.doc-container {
  display: grid;
  grid-template-columns: 1fr 300px;  /* メイン + サイドバー */
  gap: 3rem;
}

@media (max-width: 1024px) {
  .doc-container {
    grid-template-columns: 1fr;  /* 1カラムに変更 */
  }
  .doc-sidebar {
    display: none;  /* サイドバーを非表示 */
  }
}

Intersection Observerの設定

const observer = new IntersectionObserver(
  (entries) => {
    const visibleEntries = entries.filter(entry => entry.isIntersecting)

    if (visibleEntries.length > 0) {
      // 最初に表示されている見出しをアクティブにする
      const firstVisible = visibleEntries.reduce((prev, curr) => {
        return prev.boundingClientRect.top < curr.boundingClientRect.top
          ? prev
          : curr
      })
      activeId.value = firstVisible.target.id
    }
  },
  {
    rootMargin: '-80px 0px -80% 0px',  // 上部80px、下部80%をマージン
    threshold: 0
  }
)

rootMarginの説明:

  • 上部 -80px: ヘッダー分を考慮
  • 下部 -80%: 画面下部の見出しは無視(スクロールの過敏反応を防ぐ)

スタイリング

目次コンテナ

.toc {
  position: sticky;
  top: 2rem;
  max-height: calc(100vh - 4rem);
  overflow-y: auto;
  padding: 1.5rem;
  background: #f6f8fa;
  border-radius: 8px;
  border: 1px solid #d0d7de;
}

スクロールバー

.toc::-webkit-scrollbar {
  width: 6px;
}

.toc::-webkit-scrollbar-thumb {
  background: #d0d7de;
  border-radius: 3px;
}

.toc::-webkit-scrollbar-thumb:hover {
  background: #afb8c1;
}

使用方法

基本的な使い方

目次は自動的に表示されます。マークダウンファイルにH1〜H3の見出しを記述するだけです。

---
title: "ドキュメントタイトル"
---

# メインタイトル

## セクション1

### サブセクション1.1

## セクション2

### サブセクション2.1

URLハッシュ対応

ページ読み込み時にURLハッシュがある場合、該当の見出しに自動スクロールします。

https://example.com/docs/guide#section-1
↓
#section-1 の見出しにスクロール

パフォーマンス最適化

  1. Intersection Observer使用
    • スクロールイベントよりも効率的
    • ブラウザの最適化を活用
  2. 遅延初期化
    • DOMが完全に構築されてから100ms後にオブザーバーをセットアップ
    • 初期レンダリングのパフォーマンス向上
  3. クリーンアップ
    • コンポーネントのアンマウント時にオブザーバーを切断
    • メモリリークを防止

トラブルシューティング

目次が表示されない

原因1: 見出しがH1〜H3以外

  • H4以降の見出しは目次に含まれません

原因2: doc.body.toc.linksがundefined

  • Nuxt Contentの設定を確認してください

原因3: 画面幅が1024px以下

  • レスポンシブデザインで非表示になります

ハイライトが正しく動作しない

原因: 見出しにIDが設定されていない

  • Nuxt Contentが自動的にIDを生成しますが、カスタムIDを設定する場合は注意が必要です

スムーズスクロールが効かない

原因: ブラウザがスムーズスクロールをサポートしていない

  • 古いブラウザでは即座にスクロールします
  • scroll-behavior: smoothのポリフィルを検討してください

今後の拡張案

  1. モバイル用ドロワーUI
    • ハンバーガーメニューから目次を表示
  2. 進捗インジケーター
    • 読み進めた割合を表示
  3. 折りたたみ機能
    • H2, H3を折りたたみ可能に
  4. 検索機能
    • 目次内の見出しを検索
  5. カスタマイズオプション
    • 表示する見出しレベルを設定可能に

参考リンク