目次の刷新(tocbot 撤去)+ 見出しアンカー(# リンクコピー)追加
起票日: 2026-07-22
対象:
apps/web,apps/blog-api/markdown,tools/content-html-backfillステータス: 対応済み(prd の backfill のみ残)
起票理由
目次まわりに複数の問題が重なっていた。
目次のインデントが分かりにくい: tocbot が生成する入れ子
olの左パディングを Tailwind preflight がリセットしており、階層の違いがマーカーサイズ(8px/6px/4px)でしか表現されていなかったhash 付き URL での自動スクロールが機能しない:
ArticleContentの初期 hash スクロール処理がlocation.hash(パーセントエンコード済み)をデコードせずにgetElementByIdへ渡しており、日本語見出し ID では常にnull。普段はブラウザネイティブのフラグメントスクロールが隠していたが、ロード完了後にコンテンツがストリーミングされるケースで露見する目次クリック後にハイライトが外れる: tocbot の
updateTocがlocation.hash(エンコード済み)と DOM のhref(未エンコード)を比較するため、日本語 ID では照合に失敗し、クリック直後のスクロールイベントでアクティブ表示が全部剥がれる(本番で再現確認済み)モバイルで目次が出ない: 1024px 以下は
right-sidebarごとdisplay: noneだったローカル(dev DB)で目次リンクが
#heading-Nになる: dev DB のcontent_htmlが旧コンバータ(見出し ID 生成なし)時代の保存値のままで、フロントのフォールバック ID が振られていた。コード差ではなく保存データの世代差見出しリンクのコピー手段がない: 見出し先頭に
#を置き、クリックで共有用 URL をコピーしたい
設計方針
論点 |
決定 |
|---|---|
tocbot の扱い |
撤去して自前実装(約 120 行)。日本語 ID の encode/decode バグ・singleton 制約(モバイル用に DOM 複製が必要)・ |
アクティブ追従 |
rAF スロットルの scroll リスナーで「オフセット 100px を越えた最後の見出し」を選択。ページ最下部ではデコード済み hash の見出しを優先(スクロール到達不能な見出し対策) |
モバイル目次 |
1024px 以下は右上に sticky 固定した「目次」ボタン( |
階層の表現 |
ファイルツリー風の連続罫線(角丸 L 字コネクタ)を CSS で描画。 |
見出しアンカーの実装場所 |
markdown クレート(Rust)の後処理で |
コピーの挙動 |
クリックで |
既存記事への反映 |
|
実装フェーズ
[x] Phase A: 目次インデントの CSS 改善(階層ごとの段下げ)
[x] Phase B:
ArticleContentの hash デコード修正[x] Phase C: tocbot 撤去 → 自前 TOC(デスクトップ追従 + モバイルアコーディオン)
[x] Phase D: 見出しアンカー(Rust 後処理 + フロントのコピー処理 + CSS)
[x] Phase E: Storybook フィクスチャ再生成・検証
[x] Phase F: dev DB へ backfill 実行
[ ] Phase G: API デプロイ後、prd へ backfill 実行
backfill 手順
markdown クレート変更を含む API をデプロイした後に実行する(保存値と API 生成値の世代を揃えるため)。
cd tools/content-html-backfill
# wasm を最新のコンバータでビルド
bun run build:wasm
TAILNET=$(tailscale status --json | jq -r '.MagicDNSSuffix')
# dry-run で差分件数を確認(新規 / 一致 / 差分あり)
bun run backfill -- --endpoint "mysql://root@tidb.${TAILNET}:4000/blog_dev" --all --dry-run
# 実行(一致はスキップされる)
bun run backfill -- --endpoint "mysql://root@tidb.${TAILNET}:4000/blog_dev" --all
prd は endpoint の database を blog_prd に変えて同じ手順。
作業ログ
2026-07-22
目次インデント改善:
.toc ol ol a/.toc ol ol ol aに段下げを追加(Tailwind preflight がolの padding を消すため明示指定)hash スクロール不全の原因特定:
location.hashはエンコード済みで返る。decodeURIComponentを挟んで修正tocbot のハイライト剥がれを本番で再現(下部見出しクリック → 150ms 後に
is-active-liが全消失)。tocbot 内部のupdateTocがエンコード済み hash と未エンコード href を比較しているのが原因で、修正不能と判断し撤去TableOfContentsを自前実装に置換。tocbot をpackage.jsonから削除見出しアンカーを
add_heading_anchors(markdown クレートの後処理)として実装。comrak が見出し末尾に付ける空のa.anchorはそのまま残している(不可視・無害)Storybook フィクスチャを
cargo run -p markdown --example storybook_fixtureで再生成dev DB へ backfill 実行: 134 件処理(更新 100 / 一致スキップ 34 / 失敗 0)。dev で目次リンクが
#heading-Nになっていた問題はこれで解消(旧コンバータ世代の保存値に見出し ID が無かったのが原因)モバイル目次を上部 sticky 固定に変更。
position: stickyの基準を記事全体を含む.article-bodyに取らせるため、ラッパー.right-sidebarをdisplay: contentsで消す方式にしたモバイル目次をアコーディオンから「右上固定の目次ボタン + ボタン直下のモーダル(native
<dialog>)」へ変更。showModal()が最初のリンクへ自動フォーカスしてリングが全幅の下線に見えるバグは、tabindex="-1"のリスト全体へフォーカスを移して解消目次の階層表現を四角マーカー + 縦レールからファイルツリー風の連続罫線(角丸 L 字コネクタ、CSS 描画)へ変更。
├──文字案は行間で縦線が途切れるため不採用。レールが最後の項目より下へはみ出す問題もレール廃止で解消