MANIARUのカテゴリー記事では、PCとスマートフォンで目次の見せ方を変えています。PCでは本文横のサイドバー、モバイルでは本文先頭の折りたたみ目次です。
表示場所と外側のUIは違いますが、案内するのは同じ記事のH2とH3です。現在の実装では、見出しを画面ごとに取り直さず、一度作ったheadingsを共通データとして使い、同じbuildList()から二つの目次DOMを生成しています。
この記事では、完成時の構造、JavaScriptの配置場所、必要なDOM、単一HTMLで試せる最小完成例、WordPressへ反映した後の確認方法まで順番に整理します。追従表示はCODE #01、現在位置のactive切り替えはCODE #02へ分け、今回は目次を作る処理だけを扱います。
完成するとどう動くか
記事本文に「概要」「準備」「実装」という見出しがあれば、PC用とモバイル用の目次にも同じ順序で三つのリンクが作られます。H3は直前のH2の下に入り、リンク先は元の見出しIDです。
- PCでは、通常サイドバーの末尾に
asideの目次を追加する - モバイルでは、本文の先頭に
detailsの折りたたみ目次を追加する - 二つの目次は、同じ
headingsと同じ階層ルールから作る - 画面幅に応じた表示切り替えとPCのstickyはCSSへ任せる
このページ自体も同じ仕組みを使っています。PCでは右側、スマートフォンでは記事冒頭の「この記事の目次」を確認できます。
処理の全体像
- 記事本文とPCサイドバーを取得する
- 本文内のH2/H3を一つの配列へ入れる
- IDがない見出しへ連番のIDを付ける
buildList()で目次リストを作る- PC用の
asideへリストを入れ、サイドバーへ追加する - モバイル用の
detailsへ別のリストを入れ、本文先頭へ追加する
一つの目次DOMをPCからモバイルへ移動するのではありません。buildList()を2回呼び、同じ見出しデータから別々のDOMツリーを作ります。DOM要素は複数の親へ同時に置けないためです。
MANIARUではどこにJavaScriptを書いているか
現在のMANIARUでは、WordPress管理画面の「外観 → ウィジェット → サイドバー」にある「カスタムHTML」ブロックへ、カテゴリー目次の共通JavaScriptを置いています。
- WordPress管理画面で「外観」を開く
- 「ウィジェット」を開く
- 「サイドバー」を展開する
- サイドバー内の「カスタムHTML」を開く
<script>内のカテゴリー目次用の自己実行関数を確認する
記事ごとにJavaScriptを貼る構成ではありません。AI / WEB / CODE / BUILD / GROW / MONEYの記事だけを対象に、記事のDOMが存在する状態で一度実行しています。
これは現在のMANIARUで確認できた配置場所です。テーマ、子テーマ、専用プラグインなどでJavaScriptを管理しているサイトもあります。カスタムHTMLへの配置をWordPress全体の標準手順とはせず、自分の環境で共通JavaScriptを管理している場所へ合わせます。
コードを読む前に前提DOMを確認する
| 要素・変数 | MANIARUでの役割 |
|---|---|
.p-entry__content | H2/H3を取得し、モバイル目次を先頭へ入れる記事本文 |
.l-sidebar | PC用目次を末尾へ追加するサイドバー |
headings | 本文から取得したH2/H3の配列 |
heading.id | 目次リンクの移動先。ない場合は連番を付ける |
buildList() | 同じ見出し配列から新しい目次リストを返す関数 |
.maniaru-category-toc | PC用目次の外側 |
.maniaru-category-mobile | モバイル用目次の外側 |
<main class="p-entry">
<div class="p-entry__content">
<h2 id="section-1">概要</h2>
<h3 id="section-1-1">準備</h3>
</div>
</main>
<aside class="l-sidebar">
<!-- PC用目次の生成先 -->
</aside>
モバイル用目次は別の固定枠を探すのではなく、.p-entry__contentの最初の子要素として挿入します。コード中のsidebarは突然現れる変数ではなく、.l-sidebarを取得した結果です。
単一HTMLファイルで最小完成例を試す
公開中のWordPressへ直接コードを加える前に、ローカルの単一HTMLファイルで中心処理を確認します。サーバーや追加ツールは必要ありません。
- テキストエディタで新しいファイルを作る
- ファイル名を
responsive-toc-demo.htmlにする - 次の完成コードをすべてコピーして貼り付ける
- UTF-8で保存し、ブラウザで開く
- ブラウザ幅を広げた状態と狭めた状態で確認する
HTML、表示用CSS、目次を生成するJavaScriptを一つにまとめています。JavaScriptは<body>の末尾にあるため、本文と生成先が作られた後に実行されます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PC・モバイル目次の共通化デモ</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
color: #1b2638;
font-family: sans-serif;
}
.demo-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 32px;
max-width: 960px;
margin: 0 auto;
padding: 32px 20px;
}
.demo-content,
.demo-sidebar,
.demo-mobile {
padding: 16px;
border: 1px solid #e3e9f0;
}
.demo-toc-list ul {
padding-left: 20px;
}
.demo-mobile {
display: none;
margin-bottom: 24px;
}
@media (max-width: 699px) {
.demo-layout {
display: block;
}
.demo-sidebar {
display: none;
}
.demo-mobile {
display: block;
}
}
</style>
</head>
<body>
<div class="demo-layout">
<main class="demo-content">
<h2 id="overview">概要</h2>
<p>PCとモバイルで同じ見出しを使います。</p>
<h3 id="preparation">準備</h3>
<p>H3は直前のH2の下へ入ります。</p>
<h2 id="implementation">実装</h2>
<p>二つの目次を別々のDOMとして生成します。</p>
</main>
<aside class="demo-sidebar" aria-label="PC用目次の生成先"></aside>
</div>
<script>
(function () {
var content = document.querySelector('.demo-content');
var sidebar = document.querySelector('.demo-sidebar');
if (!content || !sidebar) return;
var headings = Array.prototype.slice.call(
content.querySelectorAll('h2, h3')
);
if (!headings.length) return;
headings.forEach(function (heading, index) {
if (!heading.id) heading.id = 'demo-heading-' + (index + 1);
});
function buildList() {
var root = document.createElement('ul');
root.className = 'demo-toc-list';
var currentH2 = null;
var childList = null;
headings.forEach(function (heading) {
var item = document.createElement('li');
var link = document.createElement('a');
link.href = '#' + heading.id;
link.textContent = heading.textContent.trim();
item.appendChild(link);
if (heading.tagName === 'H2') {
root.appendChild(item);
currentH2 = item;
childList = null;
} else if (currentH2) {
if (!childList) {
childList = document.createElement('ul');
currentH2.appendChild(childList);
}
childList.appendChild(item);
} else {
root.appendChild(item);
}
});
return root;
}
var desktopTitle = document.createElement('p');
desktopTitle.textContent = 'PC用目次';
sidebar.appendChild(desktopTitle);
sidebar.appendChild(buildList());
var mobile = document.createElement('details');
mobile.className = 'demo-mobile';
var summary = document.createElement('summary');
summary.textContent = 'モバイル用目次';
mobile.appendChild(summary);
mobile.appendChild(buildList());
content.insertBefore(mobile, content.firstChild);
})();
</script>
</body>
</html>
最小例の成功状態を確認する
- 700px以上では右側に「PC用目次」が表示される
- 699px以下ではPC用目次が消え、本文先頭に「モバイル用目次」が表示される
- どちらにも「概要 → 準備 → 実装」が同じ順序で並ぶ
- 「準備」は「概要」の子リストへ入る
- 各リンクを押すと、同じ名前の本文見出しへ移動する
ブラウザ幅を変えても見出しを再取得していません。ページを開いた時点でPC用とモバイル用の両方を生成し、CSSが画面幅に合う方だけを表示しています。
コードの重要部分を確認する
headingsを共通データにする
headingsには、本文内のH2/H3を上から順番に入れます。PC用とモバイル用で別々にquerySelectorAll()を実行しないため、対象見出しと順序がずれません。
buildListは呼ぶたびに新しいDOMを返す
buildList()は共通のheadingsを読みますが、呼ぶたびに新しいulを作ります。PC用のリストをモバイル側へ追加して移動させるのではなく、同じルールで二つ作ることが共通化の中心です。
IDとhrefを同じ値にする
目次リンクは#とheading.idを組み合わせます。IDがない見出しには先に連番を付けます。見出しIDが重複すると複数リンクが同じ場所を指すため、WordPressで手動アンカーを設定している場合も重複がないか確認します。
MANIARU本番の実装との違い
最小例は、目次生成の中心だけを確認するためのコードです。本番では、次の条件を追加しています。
- CATEGORY記事だけを対象にする
- PROJECT専用ナビがある記事では実行しない
- 生成済みのモバイル目次やPROJECTナビ内の見出しを除外する
- PC側は
.l-sidebar、本文側は.p-entry__contentを使う - 本番用のclass名を付け、既存CSSへ接続する
- 生成後にCODE #02のactive判定へ両方のリンクを渡す
目次DOMを作るJavaScriptと、どちらを表示するか決めるCSSは役割を分けています。PC用のsticky表示もCSSの役割です。現在位置の判定は目次生成後に行います。
WordPressへ反映して確認する
MANIARUの共通スクリプトを変更する場合は、カスタムHTMLの元コードを手元へ控えてから編集します。変更後はカスタムHTMLを更新し、公開記事を再読み込みします。
- PC幅で右側の目次が表示されることを確認する
- 本文のH2/H3と同じ項目が同じ順序で並ぶことを確認する
- H3が直前H2の子リストへ入ることを確認する
- 各リンクから対応する本文見出しへ移動できることを確認する
- スマートフォン幅でPC目次が消え、本文先頭の折りたたみ目次が表示されることを確認する
- 折りたたみ目次を開き、PCと同じ項目があることを確認する
この状態なら、見出し取得、IDとhrefの対応、共通リスト生成、PC・モバイルそれぞれへの挿入まで動いています。
目次が生成されないときに確認すること
次はMANIARUで起きた失敗談ではなく、同じ仕組みを別環境へ合わせるときの一般的な確認ポイントです。
- 本文を取得できない:
.demo-contentや.p-entry__contentが実際の本文要素と一致しているか - PC目次が出ない:
sidebarが実際の生成先を取得できているか - リンクが移動しない:見出しIDとリンクのhrefが一致し、IDが重複していないか
- H3の階層が違う:H3より前に親となるH2があるか
- 何も生成されない:スクリプト実行時点で本文、サイドバー、見出しがDOMに存在するか
- 目次項目が重複する:生成した目次内の見出しを再取得対象に含めていないか
- 変更が見えない:ウィジェットを更新し、公開記事を再読み込みしたか
実装して分かったこと
共通化する対象は、完成した目次DOMそのものではなく、元になる見出しデータと組み立て方でした。同じDOMを使い回そうとすると表示先を移動してしまいますが、同じデータから別々に作れば、PCとモバイルの両方を同じページへ置けます。
データ取得、DOM生成、表示切り替え、sticky、active判定を分けると、どこを直す処理なのかも判断しやすくなりました。画面ごとに同じ処理を複製するのではなく、共通部分とUI固有部分の境界を決めることが重要でした。
関連記事
- WordPressの記事に追従する目次を作る。H2/H3と現在位置を連動させてみた — 目次全体の設計とPC・モバイルの役割
- CSSのposition: stickyで追従サイドナビを作る。実装して分かった注意点 — PC側の追従表示
- スクロール位置に合わせて目次を切り替える。JavaScriptで現在の見出しを判定してみた — 生成後のリンクへ現在位置を反映する処理