HB DevTools

Knowledge

AppHeader のパンくずリストを多段階構成に変更した記録

1段階だったブレッドクラム(HB DevTools > ページ名)を、カテゴリを挟んだ多段階構成(HB DevTools > Tools > JSON Formatter)に変更した実装記録。BreadcrumbSegment 型の導入と useHeaderState フックの返り値変更を中心に解説します。

Next.jsアクセシビリティリファクタリング

1. 変更前の課題

変更前の AppHeader のブレッドクラムは1段階のみ(例: HB DevTools > JSON Formatter)でした。 ツールページ・ドキュメントページ・ナレッジ記事がすべて同じフラットな構造で表示されるため、 ユーザーが「いま自分はサイトのどの領域にいるか」を把握しにくい状態でした。

変更前のコードの核心部分は以下の通りです。

function useHeaderState(): { isHome: boolean; pageLabel: string | null } {
  const pathname = usePathname()
  // ...
  const tool = tools.find((t) => t.href === pathname)
  if (tool) return { isHome: false, pageLabel: tool.label }
  // ...
}

返り値が pageLabel: string | null の1値のため、 カテゴリ階層を表現する手段がありませんでした。

2. 変更方針

各パスで表示するブレッドクラムを以下のように設計しました。

  • ツールページ/tools/*): HB DevTools > Tools > JSON Formatter
    /tools インデックスページが存在しないため、Tools はリンクなしのプレーンテキストにする
  • Docs 記事/docs/*): HB DevTools > Technical Docs > JSON Formatter の設計
    /docs は実ページがあるため中間セグメントをリンクにする
  • Knowledge 記事/knowledge/*): HB DevTools > Knowledge > 記事タイトル
    同様に /knowledge をリンクにする
  • インデックス・静的ページ/docs, /about 等): 1セグメントのまま変更なし

3. BreadcrumbSegment 型の導入

1値の string | null では複数セグメントを表現できないため、 セグメント1件を表す型を新設しました。

type BreadcrumbSegment = { label: string; href?: string }

href を省略可能にすることで、 リンクにするかプレーンテキストにするかをデータ側で制御できます。 描画層では href の有無だけを見ればよいため、 条件分岐がシンプルになります。

4. useHeaderState の変更

フックの返り値を pageLabel: string | null からbreadcrumbs: BreadcrumbSegment[] に変更しました。

// Before
function useHeaderState(): { isHome: boolean; pageLabel: string | null }

// After
function useHeaderState(): { isHome: boolean; breadcrumbs: BreadcrumbSegment[] }

各パスの解決ロジックは以下のようになります。

const tool = tools.find((t) => t.href === pathname)
if (tool) return {
  isHome: false,
  breadcrumbs: [{ label: 'Tools' }, { label: tool.label }],
}

const doc = docs.find((d) => d.href === pathname)
if (doc) return {
  isHome: false,
  breadcrumbs: [{ label: 'Technical Docs', href: '/docs' }, { label: doc.label }],
}

const article = knowledge.find((k) => k.href === pathname)
if (article) return {
  isHome: false,
  breadcrumbs: [{ label: 'Knowledge', href: '/knowledge' }, { label: article.label }],
}

ツール(Tools)は href なし、 Docs・Knowledge の中間セグメントは href あり、 という差異をデータで表現しています。

5. 描画ロジックの変更

単一の pageLabel を表示していた箇所を、breadcrumbs 配列のマップに置き換えました。 各セグメントの間に ChevronRight アイコンを挟む必要があるため、 React の Fragment を使ってラッパーなしで並べています。

{breadcrumbs.map((crumb, i) => {
  const isLast = i === breadcrumbs.length - 1
  return (
    <Fragment key={crumb.label}>
      <ChevronRight className='size-3.5 text-muted-foreground/60' aria-hidden='true' />
      {isLast ? (
        <span className='font-medium text-muted-foreground' aria-current='page'>
          {crumb.label}
        </span>
      ) : crumb.href ? (
        <Link href={crumb.href} className='... hover:text-foreground'>
          {crumb.label}
        </Link>
      ) : (
        <span className='font-medium text-muted-foreground/70'>{crumb.label}</span>
      )}
    </Fragment>
  )
})}

セグメントの種別は3パターンに分類されます。

  • 最後のセグメント(現在ページ): aria-current='page' 付きの span
  • 中間セグメント・リンクあり: hover:text-foreground 付きの Link
  • 中間セグメント・リンクなし(Tools): プレーンな span

6. アクセシビリティ対応

ブレッドクラムに関するアクセシビリティ上の変更点は2つです。

  • コンテナを div から nav に変更: <nav aria-label="パンくずリスト"> とすることで、 スクリーンリーダーがナビゲーションランドマークとして認識できるようになります。
  • 現在ページに aria-current='page' を付与: 最後のセグメント(現在閲覧中のページ名)の span にこの属性を追加し、 スクリーンリーダーに「このリンクが現在のページです」と明示します。

ChevronRight アイコンは装飾目的のため aria-hidden='true' を維持しています。