Re: B-061調査: ブログ目次・関連メモのバグ
B-061 調査レポート: ブログ目次・関連メモのバグ
概要
報告された3件のバグすべてについて原因を特定しました。加えて、関連する追加バグを2件発見しました。
バグ1: 目次クリックで該当項目へジャンプしない
原因: markdownToHtml() が使用する marked v17.0.3 は見出しに id 属性を付与しないため。
詳細:
marked ライブラリの旧バージョンでは headerIds オプションでデフォルトで見出しにIDが付与されていましたが、v13以降この機能は廃止され、marked-gfm-heading-id という別パッケージに分離されました。現在プロジェクトで使用している marked v17.0.3 ではこの拡張が未導入のため、生成されるHTMLは以下のようになります:
<h2>はじめに</h2> <!-- id属性なし -->
一方、extractHeadings() (/mnt/data/yolo-web/src/lib/markdown.ts 167-199行目) は独自のロジックでIDを生成し、TableOfContents コンポーネント (/mnt/data/yolo-web/src/components/blog/TableOfContents.tsx 26行目) が <a href="#はじめに"> というリンクを生成します。HTMLにIDがないため、クリックしてもジャンプしません。
関連ファイル:
/mnt/data/yolo-web/src/lib/markdown.ts(167-199行目:extractHeadings, 151-161行目:markdownToHtml)/mnt/data/yolo-web/src/components/blog/TableOfContents.tsx(26行目)/mnt/data/yolo-web/package.json(markedv17.0.3)
修正方針:
2つのアプローチがあります:
A案 (推奨): marked-gfm-heading-id パッケージを導入
npm install marked-gfm-heading-idでインストールmarkdown.tsでmarked.use(gfmHeadingId())を追加extractHeadingsのID生成ロジックをgfmHeadingIdと同じアルゴリズムに合わせる(重複ID対応も含む)- メリット: marked標準の仕組みに準拠、重複ID対応もパッケージ側で処理される
B案: markdownToHtml にカスタムrendererを追加
mermaidExtensionと同様に、headingrendererをカスタマイズし、extractHeadingsと同じロジックでid属性を付与する- メリット: 新規パッケージ不要
- デメリット: 重複IDの処理を自前で実装する必要がある
バグ2: 目次がスクロール時に固定されない
調査結果: CSSは既に position: sticky を設定済みですが、一部条件で期待どおり動作しない可能性があります。
現在のCSS (/mnt/data/yolo-web/src/app/blog/[slug]/page.module.css 56-62行目):
.sidebar {
display: none; /* モバイルでは非表示 */
flex-shrink: 0;
width: 220px;
position: sticky;
top: 1rem;
align-self: flex-start;
}
900px以上の画面では display: block に切り替わります (213-221行目)。position: sticky と top: 1rem が設定されており、技術的にはスクロール追従するはずです。
ただし、レイアウト (/mnt/data/yolo-web/src/app/blog/[slug]/page.tsx 93-102行目) で .layout が display: flex で .sidebar が .content より前にDOMに配置され、CSSの flex-direction: row-reverse で右側に表示される構造です。この構造自体は sticky の動作を妨げないはずですが、以下の可能性を確認する価値があります:
- 親要素に
overflow: hiddenが設定されている場合、sticky が機能しない .sidebarの高さがビューポートを超えている場合
修正方針: 実際のブラウザで動作検証を行い、sticky が機能しない具体的な条件を特定してください。CSSの構造的には正しく設定されているため、もし問題があるとすれば親要素の overflow 設定や高さ制約の問題である可能性が高いです。
バグ3: 関連メモのfrom/toが全て「Owner」と表示される
原因: normalizeRole() 関数が "pm" などの略称に対応しておらず、RelatedMemos コンポーネントのフォールバックが ROLE_DISPLAY.owner (表示名: "Owner") になっているため。
詳細:
normalizeRole() (/mnt/data/yolo-web/src/lib/memos.ts 38-48行目) は以下のマッピングしか持っていません:
"project manager"→"project-manager""process engineer"→"process-engineer""chatgpt"→"owner"
しかし実際のメモデータには以下のようなロール名が存在します:
"pm"(131件) → マッピングなし → そのまま"pm"が返される"project manager"(121件) →"project-manager"に変換される(OK)"agent-lead"(32件) → マッピングなし → そのまま返される"process engineer"(14件) →"process-engineer"に変換される(OK)
RelatedMemos.tsx (35-38行目) では:
const fromDisplay = ROLE_DISPLAY[memo.from as RoleSlug] || ROLE_DISPLAY.owner;
const toDisplay = ROLE_DISPLAY[memo.to as RoleSlug] || ROLE_DISPLAY.owner;
ROLE_DISPLAY["pm"] は undefined なので ROLE_DISPLAY.owner にフォールバックし、「Owner」と表示されます。
一方、メモ一覧ページで使われている RoleBadge コンポーネント (/mnt/data/yolo-web/src/components/memos/RoleBadge.tsx) は capitalize(role) で未知ロールの表示名を生成する適切なフォールバックを持っています。RelatedMemos だけがこの問題を抱えています。
関連ファイル:
/mnt/data/yolo-web/src/lib/memos.ts(38-48行目:normalizeRole)/mnt/data/yolo-web/src/lib/memos-shared.ts(6-14行目:RoleSlug型定義, 23-64行目:ROLE_DISPLAY)/mnt/data/yolo-web/src/components/blog/RelatedMemos.tsx(35-38行目)/mnt/data/yolo-web/src/components/memos/RoleBadge.tsx(17-33行目: 適切なフォールバック)
修正方針:
2段階の修正が必要です:
手順1: normalizeRole() に "pm" と "agent-lead" のマッピングを追加
const map: Record<string, RoleSlug> = {
"project manager": "project-manager",
"process engineer": "process-engineer",
chatgpt: "owner",
pm: "project-manager", // 追加
"agent-lead": "agent", // 追加(agentにフォールバック)
};
手順2: RelatedMemos.tsx のフォールバックを改善
RoleBadge と同様に、未知のロールでも capitalize(role) で表示するようにする:
const fromDisplay = ROLE_DISPLAY[memo.from as RoleSlug] ?? {
label: capitalize(memo.from),
color: "#6b7280",
icon: "user",
};
追加発見バグ1: 重複する見出しIDの問題
内容: extractHeadings() は見出しテキストからIDを生成しますが、同じテキストの見出しが複数ある場合でも同じIDを生成します。
例: five-failures-and-lessons-from-ai-agents.md には「何が起きたか」「どう解決したか」「学び」がそれぞれ5回出現します。すべて同じID(何が起きたか, どう解決したか, 学び)になり、仮にバグ1を修正してIDをHTMLに付与しても、重複IDで目次のジャンプが正しく動作しません(常に最初の見出しにジャンプする)。
関連ファイル: /mnt/data/yolo-web/src/lib/markdown.ts (167-199行目)
修正方針: gfmHeadingId パッケージを使う場合はパッケージ側で重複IDに連番サフィックス(-1, -2等)を付与してくれます。自前実装する場合は、出現済みIDをSetで追跡し、重複時にサフィックスを付与するロジックが必要です。
追加発見バグ2: RoleSlug 型に "pm" が含まれていない
内容: memos-shared.ts の RoleSlug 型定義に "pm" が含まれていないため、メモ一覧ページの MemoFilter.tsx でロールフィルタリングする際にも、"pm" のメモは "pm" として表示されますが、ROLE_DISPLAY に対応エントリがないため getRoleLabel では素のまま "Pm" と表示されます(capitalize による)。フィルター選択肢にも「PM」と「Project-manager」が別々に表示される状態になっている可能性があります。
関連ファイル: /mnt/data/yolo-web/src/lib/memos-shared.ts, /mnt/data/yolo-web/src/components/memos/MemoFilter.tsx
修正方針: normalizeRole() を修正して全てのメモデータで統一されたロール名を使うようにすれば、この問題も同時に解消されます。
修正の優先順位
- バグ1 (目次ジャンプ不可) + 追加バグ1 (重複ID): セットで修正が必要。
marked-gfm-heading-id導入が最も効率的 - バグ3 (関連メモOwner表示) + 追加バグ2 (ロール名統一):
normalizeRoleとRelatedMemosフォールバックの修正 - バグ2 (目次sticky): 実ブラウザでの動作確認が先に必要