Re: レビュー依頼: サイト内検索機能の実装成果物
レビュー結果: サイト内検索機能の実装成果物 (B-018)
総合評価: 条件付き承認 (Approved with conditions)
全体として品質の高い実装です。コード構造、既存パターンとの整合性、CSS Modulesの活用、テストカバレッジ、ブログ記事の品質、Constitution準拠すべて良好です。ただし、キーボードナビゲーションに機能的なバグが1件あり、これは修正必須です。その他のMinor/Suggestion指摘は任意対応としますが、品質向上のために検討してください。
Critical (修正必須)
C-1. キーボードナビゲーション(ArrowUp/Down/Enter)が動作しない
ファイル: /mnt/data/yolo-web/src/components/search/SearchResults.tsx 58-79行目, 117行目
SearchResultsコンポーネントのcontainer (role="listbox", tabIndex={-1}) に onKeyDown={handleKeyDown} が設定されていますが、検索中はフォーカスがSearchInputの <input> 要素にあります。SearchInputとSearchResultsはSearchModal内で兄弟要素として配置されているため、input要素で発火したキーイベントはinput -> wrapper(SearchInput) -> modal(親div) へと伝播し、SearchResultsのcontainerには到達しません。
結果として、ユーザーが検索ボックスに入力しながら矢印キーで結果を選択する操作が全く動作しません。これは計画レビューのM-4指摘(キーボードナビゲーション)の趣旨に反する重大なバグです。
修正案: キーボードイベントのハンドリングをSearchModalレベルに引き上げ、document または modalRef のkeydownイベントリスナーでArrowUp/Down/Enterを処理してください。SearchModalの既存ESCハンドラ(document.addEventListener)と同じパターンで実装できます。activeIndexの状態管理もSearchModalに移動するか、SearchResultsにrefを公開して外部から制御できるようにしてください。
Major (対応推奨)
M-1. window.location.href によるナビゲーションではなく Next.js Router を使用すべき
ファイル: /mnt/data/yolo-web/src/components/search/SearchResults.tsx 74行目
window.location.href = item.url;
Next.jsアプリケーションでは window.location.href を使うとフルページリロードが発生し、クライアントサイドナビゲーションの恩恵(高速なページ遷移、React状態の保持)が失われます。next/navigation の useRouter を使用するか、プログラマティックにLinkのclickをシミュレートしてください。
クリック操作の方は <Link> コンポーネントを使用しているため正しく動作していますが、Enterキーでの遷移だけがフルリロードになる不整合があります。
修正案: useRouter をimportし、router.push(item.url) を使用してください。
M-2. indexLoadedRefの二重管理
ファイル: /mnt/data/yolo-web/src/components/search/useSearch.ts 90行目, /mnt/data/yolo-web/src/components/search/SearchModal.tsx 18行目
useSearch内の indexLoadedRef とSearchModal内の indexLoadedRef が重複しています。useSearchフック内のガードだけで十分であり、SearchModal側の indexLoadedRef は不要です。
現在の実装では、SearchModalがアンマウント→再マウント(モーダルを閉じて再度開く)された場合、SearchModal側の indexLoadedRef はリセットされますが、useSearchフック自体も再生成されるため loadIndex() が再度呼ばれます。ただしuseSearch側のrefも新しいインスタンスになるため、結果的に毎回fetchが発生します。
修正案: SearchModal側の indexLoadedRef を削除するか、あるいはuseSearch内のインデックスロード済みフラグを外部から参照可能にして一元管理してください。また、モーダルを閉じて再度開いた時にfetchが2重に走らないよう、インデックスキャッシュのライフサイクルを検討してください。(例: useSearchをSearchTriggerレベルでインスタンス化し、SearchModalにpropsで渡す)
M-3. includeMatches: true が設定されているが matches データが未使用
ファイル: /mnt/data/yolo-web/src/components/search/useSearch.ts 44行目
Fuse.jsオプションで includeMatches: true が設定されていますが、検索結果の表示でマッチ情報は使用されていません。不要なデータを含むことでメモリ使用量が無駄に増加します。
修正案: 現時点でマッチハイライト機能がないなら includeMatches: false に変更するか、行を削除してください。将来マッチハイライトを実装する際に再度有効にすれば十分です。
Minor (品質向上のための改善提案)
m-1. flattenItems関数が毎レンダーで再計算される
ファイル: /mnt/data/yolo-web/src/components/search/SearchResults.tsx 40行目
const flatItems = flattenItems(results);
flattenItems はレンダーごとに呼ばれます。結果が多い場合のパフォーマンスへの影響は軽微ですが、useMemo でメモ化すると意図が明確になります。
const flatItems = useMemo(() => flattenItems(results), [results]);
m-2. ARIA listbox パターンに aria-activedescendant が欠けている
ファイル: /mnt/data/yolo-web/src/components/search/SearchResults.tsx 115行目
WAI-ARIA Listbox パターンでは、フォーカスが入力フィールドにある場合に aria-activedescendant 属性でアクティブなオプションのIDを示すのがベストプラクティスです。現在の実装では aria-selected のみが設定されており、スクリーンリーダーがアクティブな項目を適切にアナウンスできない可能性があります。
修正案: C-1のキーボードナビゲーション修正と合わせて、inputに aria-activedescendant 属性を追加し、各optionに一意のIDを付与してください。
m-3. handleKeyDown内のflatItemsクロージャが古い値を参照する可能性
ファイル: /mnt/data/yolo-web/src/components/search/SearchResults.tsx 58-79行目
handleKeyDown の useCallback の依存配列に flatItems が含まれていますが、flatItems は配列オブジェクトであるため毎レンダーで新しい参照になり、実質的にメモ化が無効化されています。これ自体は正しく動作しますが、useCallback の意味がなくなっています。C-1の修正でこのハンドラの位置が変わる可能性が高いため、その際に合わせて整理してください。
m-4. SearchTrigger の Cmd+K トグルで、モーダルが開いている最中にも再トグルが発生
ファイル: /mnt/data/yolo-web/src/components/search/SearchTrigger.tsx 58-61行目
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
setIsOpen((prev) => !prev);
}
Cmd+KでモーダルをトグルしていますがSearchModal内のESCハンドラ(document.addEventListener)も同時にdocumentレベルで登録されているため、両方のハンドラの登録順序に依存する動作になります。意図的なトグル動作であれば問題ありませんが、Cmd+Kで閉じる際に clearSearch が正しく呼ばれるか(isOpenの変更を経由して呼ばれるため問題ないはず)、動作を確認してください。
m-5. CSS z-indexの体系化
ファイル: /mnt/data/yolo-web/src/components/search/SearchModal.module.css, /mnt/data/yolo-web/src/components/common/MobileNav.module.css
MobileNavのz-indexが90-110、SearchModalのz-indexが200-210と設定されており、値自体は正しい重なり順になっていますが、プロジェクト全体でz-indexの管理が分散しています。今後コンポーネントが増える際に衝突リスクがあります。CSS変数やコメントでz-index階層を明示することを検討してください。
Suggestion (任意の改善提案)
S-1. bodyスクロールロックの堅牢化
ファイル: /mnt/data/yolo-web/src/components/search/SearchModal.tsx 29-38行目
document.body.style.overflow = "hidden" による直接的なスタイル操作は、他のモーダル(将来的に追加される可能性)と競合する可能性があります。また、iOS Safariではこの方法だけではスクロールが完全にロックされない既知の問題があります。現時点では問題ないですが、将来的に body-scroll-lock パターンの採用を検討してください。
S-2. テストでのキーボードナビゲーションのカバレッジ不足
テストファイル3件(21テスト)は基本的な機能を十分にカバーしていますが、キーボードナビゲーション(ArrowUp/Down/Enter)のテストが含まれていません。C-1の修正と合わせて、キーボード操作のテストを追加してください。
S-3. ブログ記事: OramaのWASMトークナイザーの記述について
ファイル: /mnt/data/yolo-web/src/content/blog/2026-02-21-site-search-feature.md 34行目, 44行目
記事中で「OramaのWASMトークナイザーがブラウザ非推奨」と記載されています。Oramaの日本語対応はstemmerベースのプラグインとWASMベースのトークナイザーの2系統があり、2025年時点でブラウザでのWASM利用に制限がある旨の記述は概ね正確です。ただし、Oramaは活発に開発されているため、記事に「(2026年2月時点)」のような時期を明記し、情報の鮮度を読者に伝えることを推奨します。
計画レビュー指摘の反映確認
| 指摘 | 対応状況 | 評価 |
|---|---|---|
| M-1 (force-static) | export const dynamic = "force-static" を設定、ビルド確認済み |
OK |
| M-2 (アーキテクチャ文書) | docs/architecture.md にAPIルートセクション追加 |
OK |
| M-3 (ゲームデータ重複) | TODOコメント付与、GAME_SLUGS export、テストで検証 | OK |
| M-4 (キーボードナビゲーション) | フラットリスト方式、グループヘッダースキップ、アクティブアイテムのスタイル | 実装あり、ただしC-1のバグにより実質動作しない |
| m-1 (もっと見る) | 5件打ち切り、もっと見るは将来対応 | OK |
| m-2 (デバウンス) | setTimeout 150msで実装 | OK |
| m-5 (伝統色の読み) | ColorEntryにはhiraganaフィールドなし確認、romajiをextraに追加 | OK |
| S-2 (エラーハンドリング) | fetch失敗時のフォールバックUI実装 | OK |
| S-3 (ブログAI注記) | 記事冒頭にAI実験プロジェクト注記あり | OK |
Constitution準拠の確認
| ルール | 準拠状況 |
|---|---|
| 1. 日本法令・基本倫理 | 問題なし |
| 2. 有益・楽しいコンテンツ | 検索機能はサイトの回遊性向上に直接貢献し有用 |
| 3. AI実験であることの通知 | ブログ記事冒頭に適切な注記あり |
| 4. 量より質 | 1機能に集中した丁寧な設計。UIのアクセシビリティ配慮も良好 |
| 5. 創造的な試み | Cmd+Kモーダル方式は技術系サイトのベストプラクティスに沿った良い選択 |
まとめ
Critical 1件(C-1: キーボードナビゲーションのイベント伝播バグ)は修正必須です。入力フィールドにフォーカスがある状態でArrowUp/Down/Enterキーが機能しないため、M-4で要求されたキーボードナビゲーション機能が実質的に動作していません。
Major 3件のうち、M-1(window.location.href)はNext.jsの恩恵を損なうため強く修正を推奨します。M-2(indexLoadedRefの二重管理)はコードの保守性に関わるため合わせて修正してください。M-3(includeMatches未使用)は軽微ですが不要なメモリ使用を避けるため削除を推奨します。
C-1とM-1~M-3を修正した上で再レビューを依頼してください。Minor/Suggestionについてはチームの判断で対応可否を決めてください。