javascriptでwordpress記事内の目次を作る

2015.2.8 Wordpress


ブログに見出しを上手に使うと可読性が上がります。

そして検索エンジンにも文書構造を的確に伝えることができ、それはクローラー(サイトを巡回するやつ)にも、「これから、このテーマについて語るよ!」と、伝えることに繋がり、一見どうでもよさそうなことなのですが実はとても重要です。

まぁなにより読みやすくなることが大事だったりしますが…

で、ブログ記事の冒頭で見出しへと直接ジャンプできたら便利ですよね。というわけで、記事中の見出しを判断して、自動でリンク付きの目次をつくるスクリプトを紹介しますね。注意点さえ解ればコピペでペロっと持っていけます。

jQueryを使わずに、Javascriptのみで見出しを作ってみる

ブログの見出しへのリンクを作る前の注意点

ここでいう「見出し」は、HTML要素のことです。

h1~h6まである、文言の重要度を表すあれなのですが、「かならず連番であること、飛び番号は基本的にNG」という規則があります。

「h1(大見出し)」の見出しに続く文章の次の見出しは「h2」、同じように「h3」…話題が変わるところで見出しがh1(大見出し)に戻り、また大見出しにそってh2以下(小見出し)が続く。

といった具合です。HTMLのh1~h6までのタグはそもそも、そういうものなのです。
なので、そういう前提で進めていきます。

ブログの見出しへのリンクを作るHTMLとJavascriptのコード

まず、サンプルのHTMLファイルを用意します。

<div id="article-content">
<nav id="moukji"></nav><!--ここに目次が入る-->
	<h1>h1h1h1h1</h1>
	<h2>h2h2h2h2</h2>
	<h3>h3h3h3</h3>
	<h4>h4h4h4</h4>
</div>

div id=”article-content”に、ブログ記事の本文が入ると想定します。

ここは各々で違うので、「ブログ本文」の要素にidを振って指定を変えてください。

そしてその下のnav要素が今回目次を表示するターゲットです!

ではスクリプトを書きます。

window.onload = function(){
		
		var top = document.getElementById('article-content'); //id article-contentがあるか?
		var mokuji = document.getElementById('mokuji'); //目次挿入箇所があるか?

		if(top && mokuji){
		
				var walker = document.createTreeWalker(
					top,
					NodeFilter.SHOW_ELEMENT,
					//ノードの判定。h1~6の要素を取得したか?
					function(node){
					   if(/^H[1-6]$/.test(node.tagName)){
					     return NodeFilter.FILTER_ACCEPT;
					   }else{
					     return NodeFilter.FILTER_SKIP;
					   }
					 },
					false
					);
		
					var node;
					var ol = document.createElement("ol"); //目次用
					var number = 1;
					var idNumber = number; //リンク用のid番号
		
				while(node = walker.nextNode()){
				  var result = node.tagName.match(/^H([1-6])$/); ///処理中のノード(h)
				  var nowHnumber = parseInt(result[1]);//処理中のh要素の数字
				  var linkId;
				  
				  while(true){
					linkId = "h" + idNumber; //リンク用id
				  	idNumber++;
		
				   var t = document.getElementsByTagName("a");
		
				   if(t.length == 0){
				  	break;
				   }
				  }
		
				  node.id = linkId; //処理中のhにIDを設定
		
				  if(number < nowHnumber){
				    //処理中のh要素の数字が大きい(見出しとしては小さい)場合
				    var newol = document.createElement("ol");
				    ol.lastChild.appendChild(newol);
				    ol = newol;	//追加先を新しいolにする
		
				  }else if(number > nowHnumber){
				    //処理中のh要素の数字が小さい(見出しとしては大きい場合)
				    for(var i=0;i<number-nowHnumber;i++){
				      ol = ol.parentNode.parentNode;
				    }
				  }
		
				  number = nowHnumber;	//「現在の番号」を更新
		
				  var newli = document.createElement("li");
				  var newA = document.createElement("a");
				  var text = node.textContent; //処理中のh要素のテキスト
				  
				  newA.href = "#" + linkId;//a要素に、処理中のh要素に対応したアンカーを設定
				  newA.textContent = text; //処理中のh要素のテキストを挿入
				  newli.appendChild(newA); //liへ挿入
				  ol.appendChild(newli); //olへ挿入
				}//while
		
				while(ol.parentNode){
				  ol=ol.parentNode;
				}
				document.getElementById('mokuji').appendChild(ol);
			}//if top
		}//onload

こんな感じです。

何をやっているか?

ムダなエラーを省くために、要素がある場合のみ処理をします。

window.onload = function(){		
var top = document.getElementById('article-content'); //id article-contentがあるか?
		var mokuji = document.getElementById('mokuji'); //目次挿入箇所があるか?

		if(top && mokuji){
//~~処理
			}//if top
		}//onload

なおかつ、window.onload=function()~とすることで、ページ読み込み終了後に処理を行うことが出来ます。scriptファイル読み込みの場所によっては要素が取得できない状態になってしまうことを防ぎます。

document.createTreeWalker()

				var walker = document.createTreeWalker(
					top,
					NodeFilter.SHOW_ELEMENT,
					//ノードの判定。h1~6の要素を取得したか?
					function(node){
					   if(/^H[1-6]$/.test(node.tagName)){
					     return NodeFilter.FILTER_ACCEPT;
					   }else{
					     return NodeFilter.FILTER_SKIP;
					   }
					 },
					false
					);

document.createTreeWalkerは、条件に合った要素を巡回して取得します。その条件ですが、4つの引数で指定します。

document.createTreeWalker(
 巡回範囲の親要素 ,
要素の種類による絞り込み,
絞り込んだ要素を更に限定する条件 ,
true / false 基本的にfalseで良い。
)
巡回範囲の親要素

top変数です。最初に取得したこの部分です。

var top = document.getElementById('article-content'); //id article-contentがあるか?

つまり、このtopを基準にその中にある要素をすべて調査するということです。

要素の種類による絞り込み

NodeFilter.SHOW_ELEMENTの場合、「要素ノード(html)のみ」取得します。
他にもSHOW_TEXT(テキストノードのみ)などがあります。
七章第三回 条件を満たすノードをまとめて処理する: TreeWalker — JavaScript初級者から中級者になろう — uhyohyo.net

絞り込んだ条件を更に限定する条件

処理中の要素(node)を判断する関数を実行しています。

if(/^H[1-6]$/.test(node.tagName))のところで、処理中の要素のタグ名がh1~6の中のどれかであるかを判断しています。
test メソッド (Regular Expression) (JavaScript)

条件に合った場合は「nodeFilter.FILTER_ACCEPT」を返します。このように返すことで「この要素条件を満たした」という返事をしていることになります。

逆に、else{}内には「nodeFilter.FILTER_REJECT」とありますが、これは「この要素は条件を満たさなかった」という返事をしています。

以上の条件で要素の調査を終えて、適合したh要素の取得ができました。結果は、変数walkerに代入しておきます。

処理に必要な変数の準備

本格的に準備を進めます。

					var node;
					var ol = document.createElement("ol"); //目次用
					var number = 1; //hタグ判断用
					var idNumber = number; //リンク用のid番号

変数nodeをつくります。先ほどのcreateTreeWalker内のnodeとは別です。

変数olには空のol要素を作って入れておきます。最終的にはここに見出しのリンクをまとめることにします。

その下のnumberは、連続で処理していくh要素が1ならどうするか、2ならどうするかを判断するために使います。

この変数numberがまた重要で、ブログ本文中で一番最初にくる見出しの数値を入れておく必要があります。

どういうことかというと、この記事だと「jQueryを使わずに、Javascriptのみで見出しを作ってみる」という見出しが「h2」です。

僕のブログ本文は必ずh2から始まります。なのでこの場合は、numberとidNumberは「2」でなくてはいけません。でもサンプルではHTMLでh1から始まっているので「1」としておきます。

そうして分けられたid要素へのリンクを貼るのにidNumber変数を使います。

連続処理による見出しの作成

createTreeWalkerで、article-contents内(つまりブログ本文)の中のhタグの取得は終わりました。while処理で判断しましょう。

 

while(node = walker.nextNode()){
				  var result = node.tagName.match(/^H([1-6])$/); ///処理中のノード(h)
				  var nowHnumber = parseInt(result[1]);//処理中のh要素の数字
				  var linkId;
				  
				  while(true){
					linkId = "h" + idNumber; //id
				  	idNumber++;
		
				   var t = document.getElementsByTagName("a");
		
				   if(t.length == 0){
				  	 break;
				   }
				  }
		
				  node.id = linkId; //処理中のhにIDを設定
		
				  if(number < nowHnumber){
				    //処理中のh要素の数字が大きい(見出しとしては小さい)場合
				    var newol = document.createElement("ol");
				    ol.lastChild.appendChild(newol);
				    ol = newol;	//追加先を新しいolにする
		
				  }else if(number > nowHnumber){
				    //処理中のh要素の数字が小さい(見出しとしては大きい場合)
				    for(var i=0;i<number-nowHnumber;i++){
				      ol = ol.parentNode.parentNode;
				    }
				  }
		
				  number = nowHnumber;	//「現在の番号」を更新
		
				  var newli = document.createElement("li");
				  var newA = document.createElement("a");
				  var text = node.textContent; //処理中のh要素のテキスト
				  
				  newA.href = "#" + linkId;//a要素に、処理中のh要素に対応したアンカーを設定
				  newA.textContent = text; //処理中のh要素のテキストを挿入
				  newli.appendChild(newA); //liへ挿入
				  ol.appendChild(newli); //olへ挿入
				}//while
walker.nextNode()

walkerは条件に適合したh要素が入っています。.nextNode()メソッドでwalkerの中のh要素の「次のh要素」を取得し、変数nodeに代入することで、順繰りな処理が始まります。逆に、次のh要素が代入できなくなった時点でループは終了します。

処理中のh要素のレベルをチェック
				  var result = node.tagName.match(/^H([1-6])$/); ///処理中のノード(h)
				  var nowHnumber = parseInt(result[1]);//処理中のh要素の数字

処理中のh要素が入っているnodeですが、.tagName.matchを使って、h1~6までのどれにあたるかを判断します。
String.prototype.match() – JavaScript | MDN

.testは条件に当てはまってるかどうかを判断するのみでしたが、.matchの場合は条件に合った要素を配列で返します。

その下のnowHnumberに.matchで条件に合った要素を代入しているのですが、result[1]とすることで、.match内の正規表現で「()」で囲んだ箇所のみを取り出すことができます。

これから自動で振るidに重複がないか調べる

目次からのリンクのための「自動で振るid番号」は、h1なら「id=”h1”」ぐらいにとても簡素です。ここで、他のすでにあるHTMLと重複しないかチェックしておきましょう。

				  while(true){
					linkId = "h" + idNumber; //id
				  	idNumber++;
		
				   var t = document.getElementsById("a");
		
				   if(t.length == 0){
				  	 break;
				   }
				  }

while(true)は見慣れない書き方ですね。こう書くことで無限ループになります。
ただそのままでは処理が止まらずとても危険なので、if(t.length==0)に合った時点でbreak;とし、ループを終了させます。

処理中のh要素のレベルに応じてolの階層を変える

見出しの重要度を解りやすい形で目次にするために重要な作業です。

if(number < nowHnumber){
				    //処理中のh要素の数字が大きい(見出しとしては小さい)場合
				    var newol = document.createElement("ol");
				    ol.lastChild.appendChild(newol);
				    ol = newol;	//追加先を新しいolにする
		
				  }else if(number > nowHnumber){
				    //処理中のh要素の数字が小さい(見出しとしては大きい場合)
				    for(var i=0;i<number-nowHnumber;i++){
				      ol = ol.parentNode.parentNode;
				    }
				  }
number = nowHnumber;	//「現在の番号」を更新

変数numberと、処理中のh要素のレベルが入った変数nowHnumberを比べます。

nowHnumberがnumberよりも大きかった(見出しレベルとしては小さい)場合は、新しいol要素をつくり、既存のolの子要素として見出しリンクを入れます。

逆にそうでない場合は、親要素を遡って、階層のトップからまた見出しリンクを作りはじめます。

さいごに、numberにnowHnumberを代入して、次の処理に移りましょう。

見出しリンクのol要素を完成させる
		  var newli = document.createElement("li");
		  var newA = document.createElement("a");
		  var text = node.textContent; //処理中のh要素のテキスト
		  
		  newA.href = "#" + linkId;//a要素に、処理中のh要素に対応したアンカーを設定
		  newA.textContent = text; //処理中のh要素のテキストを挿入
		  newli.appendChild(newA); //liへ挿入
		  ol.appendChild(newli); //olへ挿入

冒頭の2行で見出しリスト用のli要素と、そこに挿入するa要素を作っています。

次の変数textは、処理中のh要素のテキストを毎処理ごとに保存しておく変数です。

変数newAには見出しへのリンクを設定するわけなのですが、リンク先には変数linkIdが設定されていますね。

nodeのidにもlinkIdが設定されていたので、

 node.id = linkId; //処理中のhにIDを設定

正しく処理がすすんでいれば、ここで結びつくことになります。

で、そのnewAのtextContetメソッドを介してテキストを設定します。対応した見出しのテキストが代入されることになりますね。それらをol要素に溜めていきます。
Node.textContent – Web API インターフェイス | MDN

ちょっと長めですがここまでがh要素の分だけ繰り返されるループです。

見出しリンクを表示させる

さて、見出しリンクを表示させるわけなのですが、最後にすこし状況を整えます。

		while(ol.parentNode){
		  ol=ol.parentNode;
		}

olのparentNodeを最上層まで遡ります。
万が一、見出しリンク目次を内包した変数olが万が一何かの拍子でolの中に存在してしまうと目次の階層がずれ込んでしまうことを防ぐためのループです。
Node.parentNode – Web API インターフェイス | MDN

ここまでやって、最終行のappendChildで対象のnav要素に目次をぶち込みます。
以上までを実践したデモが以下です。リンクが解りやすいように、本文が追記されています。

目次を表示するデモ

wordpressブログでプラグインを使わずに記事の目次を表示してみる

では、これをwordpressのブログでかんたんに使えるように応用してみましょう。

先ほどのサンプルファイルでは

<div id="article-content">
<nav id="moukji"></nav>
	<h1>h1h1h1h1</h1>
	<h2>h2h2h2h2</h2>
	<h3>h3h3h3</h3>
	<h4>h4h4h4</h4>
</div>

idが「article-content」の要素を基準として、h1~6の順番になっている場合にそれをリスト化して連番を振り、idが「mokuji」の要素の中に表示するというものでした。

上記のスクリプトが準備してあるのなら、あとはこのid=”mokuji”にあたる要素を記事のどこかに出せばいいだけですね。

でも、そんなのをいちいちコーディングしていたら面倒なのでショートコードを作りましょう。

functions.phpに以下のように記述します。

function mokujiCode($prefix){
  extract(
    shortcode_atts(
      array(
        'wrapperHtml' => 'nav' ,
        'id' => 'mokuji' ,
      )
      ,$prefix
    )
  );

  $mokujiHtml = "<${wrapperHtml} id='$id'></${wrapperHtml}>";

  return $mokujiHtml;
}

add_shortcode('mokuji' , 'mokujiCode');

これでショートコードの完成です。
今回はショートコードを書く時に、「wrapperHtml=””」で、出力するHTML要素(デフォルトではnav要素)と、「id=””」そのid(デフォルトではmokuji)が変えられるようにしてあります。

ちなみにショートコードについては以前に記事を書いたのでそちらも見てみてくださいね^^
【wordpress】ショートコードで投稿画面でかんたんにボタンリンクを作る方法

ショートコード1行で目次を表示してみる

どうぞ!

見出しの目次リンクを出す上での注意

説明の途中でも述べましたが、注意事項です。

ブログの見出しの付け方に規則を設けてください

今後のためです。あと、今回のスクリプトが正常に動作するためです。

その際に、以下のソースに変更を加えてください。

					var number = 1;
					var idNumber = number; //リンク用のid番号

変数numberには、ブログの本文中で一番最初に来る見出しのh要素の数字を入れてください。

僕のブログの場合、本文中の見出しは必ず「h2」が最初に来ます。

スクリプト処理に時間がかかりまくる

デモでUPしたファイルは大丈夫だったのですが、当ブログで出力してみたところ、スクリプトの無限ループが影響してMozillaクラッシュをくらってしまいました。

そのループが、ここです。

				  while(true){
					linkId = "h" + idNumber; //id
				  	idNumber++;
		
				   var t = document.getElementsByTagName("a");
		
				   if(t.length == 0){
				  	 break;
				   }
				  }

もし実際に使ってみて、スクリプト処理に時間がかかっています的なダイアログが表示されることがあったら

index

					linkId = "h" + idNumber; //id
				  	idNumber++;

whileまわりの表記を削除し、こうしてください。

 


 

 

長くなりましたが、やっていることは意外と単純です。

ただ、いちいちブログの最初に登場する見出しを決めなくては行けないのは面倒ですね。

h1が来ようがh3が最初に来ようが処理が適切に行われるようにしたいです。でも面倒でした。なので、我こそは!というjavascriptプレイヤーはsatohmsysまでお願い致します。

そして今回の記事ですが、uhyoohyo.netさまを参考にさせていただきました。

七章第五回 サンプル:見出しのリスト — JavaScript初級者から中級者になろう — uhyohyo.net
七章第六回 サンプルの改良 — JavaScript初級者から中級者になろう — uhyohyo.net

特にTreeWalkerなどとても解りやすく説明してくださっています。javascript初心者は是非ともuhyoohyo.netで中級者まで成り上がりましょう!

 

 

でわ!

プロフィール

東京でWebデザイナー・コーダーとして、フロントエンド的なことからデザイン思考的なことを考えたりして、ごにょごにょと活動中。(ポートフォリオ)Webクリエイターでは珍しい(?) HIPHOP, R&B好き。休日はよくカフェや漫画喫茶に出向いたりパン屋に行ったりコーヒ豆買いに行ったり、主に散歩しています。。デザインのトレンドやデザイン思考、HTML、CSSからSASS、Javascript、Wordpress構築などコーディングのTIPSなどをブログにアップしていきます。その他のことはtwitter( @satohmsys )まで。コーヒー友達募集中。