Bloggerテンプレート自作 #16:自動目次機能を追加する

前回の「記事内にアドセンス表示欄を作る」に引き続き、今回は「自動目次機能を追加」を実践してみようと思う。

要領は前回同様、JavaScriptで記事内にどこかに目次を自動生成させるというもので、「記事のどの部分に表示させるか」と「どういう条件で表示させるか」というのがポイントになる。今回はその辺の点を踏まえて、自動目次生成の方法をまとめていくことにする。


作業工程


自動目次生成のコードを探す



ベースになるコードは「トーマスイッチ」さんからお借りすることにした。いろいろ試してみたが、結局Moreタグの上に表示されるという点が分かりやすくていい(Vaster2で使い慣れてるってのもあるし)。

で、コードをそのまま使っても良いのだが、使ってきて以下の点が問題だと思っていた。

・すべての記事で表示されてしまう(CSSでcontentを使った場合、というか必ず使うことになる)
・moreタグ無しで記事をアップすると変な位置に表示されてしまう

なので、この問題を解決すべく、元のコードをやや変更していくことにする。

元コードを分析する


元コードで必要とされるコードの説明は以下の通り。

・jQueryの読み込み(CDNで外部から読み込めるようにする)
 → 元コードの「<script src='http://ajax.googleapis.com/ajax/…」に該当
・moreタグの上に目次表示部分のHTMLを表示させる
 → 元コードの「<script type='text/javascript'> $(document).ready(function()…」に該当
・目次表示部分のHTMLタグを追加する
 → 元コードの「<b:if cond='data:blog.pageType ==…」に該当
・自動目次生成のJavaScriptコード
 → 元コードの「<script> //<![CDATA[$(function(){…」に該当

上記4つが元コードで必要とされている。でも、以前のコードを転用できたりするので、そいうった点を踏まえて自分が必要な部分だけを取り出し、あとは自作することにした。

自動目次生成のコードを作る(実際にやったこと)


1.jQueryのCDNを追加する


元コードはjQuery(JavaScriptのライブラリ)で書かれているので、これを使うにはjQueryを読み込ませなければならない。jQueryはCDNでも使うことができるので、読み込むためのコードを記述すれば使えるようになる。元コードはバージョンが古いので、今回は以下のCDNを使うことにした。

<!-- jQuery CDN -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js" />

なお、これは別のjQueryのコードを使う場合も共通していることがあるので、一つ書いておけば、他のjQueryのコードを動かすことができる(バージョンがあっていれば)。

ちなみにjQueryのCDNは<head>タグ内に記述することが望ましい。その理由は、</body>の上にjQueryのCDNを記述した場合、テンプレートのxmlに直接入力したscriptコードは動くものの、ガジェットや記事内に記述したコードは動かなくなるため。<head>タグ内に記述すると動くようになる。

2.jQueryで呼び出すHTMLを追加する


<data:post.body />
<!-- 目次 -->
<b:if cond='data:blog.pageType == &quot;item&quot;'>
  <div id='Table-of-Content'>
    <div id="toc"></div>
  </div>
</b:if>

上記はjQueryで呼び出すHTML部分になる。とりあえず、<data:post.body />(投稿本文)の直下に追加した。

この追加位置について説明すると、自動目次は個別記事ページ(item)でのみで必要になるので、個別記事ページ部分の<data:post.body />が読み込まれる範囲内に追加する必要がある。

で、<b:if cond='data:blog.pageType == &quot;item&quot;'>という条件分岐は、それが個別記事ページかどうかを判別するものなので、テンプレートによってはトップページなどに<data:post.body />と指定しているものがあるから、そういった場合のものに追加した場合のエラー回避に使われるものである。よって、追加部分が個別記事ページであれば、この条件分岐は記述する必要はない(ハズ)。

3.目次を構成するjQueryコードを追加する


<script>
//<![CDATA[
$(function(){
    var idcount = 1;
    var toc = '';
    var currentlevel = 0;
    $(".post-body h2,.post-body h3,.post-body h4",this).each(function(){
        this.id = "chapter-" + idcount;
        idcount++;

// 以下略(コード全文はリンク先から取得してください)

//]]>
</script>

</body>

とりあえず、元コードを改変すること無く </body> の上に記述した。元コードを全文載せるのは気が引けるので、全文は「トーマスイッチ」さんから取得してほしい。

一応説明すると、JavaScript(jQuery)のコードはどこに入れても動くことは動く。一説に</body>の上に置くと読み込みが早いといわれているので、</body>の上に記述するように指示されていることが多い。

このコードは、記事から見出しを繰り返し取得して、それを<ol>に入れて、<li>に見出しリンクを付与する。もし記事内にh2タグがあれば<ol>(目次)の入った<div id="toc"></div>を表示、h2がなければ<div id="toc"></div>に"no-toc"というクラス名を付与するといった内容だと思う(詳しくは検証してない)。

なので、元コードをCSSなしで実行すると、h2タグを設けた記事では「目次リストが表示」され、そうでない場合は「何も表示されない」という結果になる。

4.moreタグの上に目次を表示させるコードを追加する


<!-- 記事中に目次・アドセンスを配置する -->
<b:if cond='data:blog.pageType == &quot;item&quot;'>
  <script>
    //<![CDATA[
      var parent = document.getElementsByClassName('post-content')[0];
      var ad = document.getElementById('ad1');
      var more = document.getElementsByName('more')[0];
      var toc = document.getElementById('Table-of-Content');
      var h2 = document.getElementsByTagName('h2')[0];
      // 目次の読み込み(h2とmoreがあるときのみ)
      if (typeof h2 !== 'undefined' && typeof more !== 'undefined') {
      parent.insertBefore(toc, more);
      } else {
      toc.hidden = true;
      }
      // アドセンスの読み込み
      parent.insertBefore(ad, more);
    //]]>
  </script>
</b:if>

<!-- 3の目次を構成するjQueryコード(省略) -->

</body>

これは、前回の「記事中アドセンス」のコードを変更し、目次を表示させるコード追加したもの。なお、記述位置は</body>の上辺り、3のコードの上あたりで問題ない。

なので、前回の記事を参考にしている方は「<!-- 記事中にアドセンスを配置する -->」の部分(<b:if>~</b:if>まで)を上記のコードに上書きして使ってほしい。

これを追加した場合、元コードの「<script type='text/javascript'> $(document).ready(function()…」は必要なくなる。なお、元コードでは<head>タグ内に記述するよう指示されているが、</body>の上でも問題なく動く。

で、上記のコードは何をしているのかというと、まずJavaScriptで「目次の表示位置をmoreタグの上になるよう」に指定し、その指示を「記事内にh2タグがある かつ 記事内にmoreタグがある」場合にのみ実行し、そうでない場合は「目次表示のHTML(<div id="Table-of-Content"></div>)を非表示にする」という処理を行うもの。

元コードは「moreタグの上に目次を表示させる」という処理だけだが、これを条件分岐で制御することによって

・h2タグが無い記事の場合は非表示
・moreタグが無い記事の場合は非表示

という処理ができ、転じて「h2タグがある場合のみ目次を表示」という条件を課すことができるので、実際にブログを運用する際に指定ミスによるデザイン崩れを防げるようになる(と思う)。

5.CSSを追加する


Bloggerテンプレート自作 #16:自動目次機能を追加する

/* ----------------------------------------------------------------------------
    目次
---------------------------------------------------------------------------- */

/* 目次タイトル */

#toc:before {
  display: block;
  text-align: center;
  content: "\3010\76EE\a0\6B21\3011";
  font-weight: bold;
  font-size: 15px;
  color: #555;
  letter-spacing: 2px;
  padding-bottom: 20px;
}

/* 目次全体 */

#toc {
  display: block;
  width: 60%;
  background: #f9f9f9;
  padding: 10px 10px 20px;
  font-size: 14px;
  margin: 0 auto 30px;
  border: 1px solid #bbb;
}

#toc ol li {
  margin-left: 8%;
}

#toc ol {
  margin: 2px 0 2px 15px;
}

#toc li {
  color: #666;
}

#toc a {
  color: #666;
}

#toc a:hover {
  opacity: 0.6;
}

/* レスポンシブ(タブレット縦) */

@media screen and (max-width: 960px) {
  #toc {
    width: 80%;
  }
}

/* レスポンシブ(スマホ縦) */

@media screen and (max-width: 560px) {
  #toc {
    width: 90%;
    margin: 0 auto 20px;
  }
  #toc ol li {
    margin-left: 0;
  }
}

とりあえず、上記のようなCSSを記述すると画像のようなデザインになる。

追記:画像にはカウンターを付けているが、リストの順番が不正確だったので該当するCSSを削除した。

余談


元コードにno-tocを消すCSSを追加する


元コードをそのまま使う場合、一部のCSSが欠けていると思われる。というのも、目次を自動生成のjQueryコードは、h2タグが無い時に中身のない<div id="toc">にno-tocというクラス名を追加するという記述になっている。具体的に言えば、h2タグが無い場合は以下のような処理になる。

<!-- h2が無い場合の目次HTML -->
<div id='Table-of-Content'>
  <div id="toc" class="no-toc"></div>
</div>

CSSを当てないのであればこのままで問題ないのだが、元コードにあるCSSでは#tocに「記事の目次」というタイトルがbeforeで付いているので、中身のないHTMLができてもCSSの付いた残骸が残ってしまう。

そこで、以下のような処理をCSSに書き加えると、no-tocの場合に目次HTMLをすべて非表示にすることができる。

/* h2タグがない場合に目次を非表示にする */
#Table-of-Content .no-toc{
  display:none;
}

これはno-tocというクラス名のついた <div id="toc"> を、それを囲う <div id='Table-of-Content'> ごと非表示にするというもの。.no-tocのみにdisplay:none;を当てても、元々中身が無いので表示に変化はない。

そこで.no-tocを持っている#Table-of-Contentごとdisplay:none;にすることで目次の全要素を非表示させるというもの。ハンバーガーで例えるならば、#toc(.no-toc)がハンバーガーで、#Table-of-Contentが包み紙に当たる。display:none;でハンバーガーを無くしても、包み紙は残るので、それが残骸としてページに表示されるという感じ(わかりにくい?)。