はてなブログでブログ外リンクを別ウィンドウ/タブで開くように(JS解説付き)

ポイント

はてなブログで全てのリンクを別ウィンドウで開くならば <base target="_blank"> を設定しておけば済むのだけど、しかしこの状況でははてなブログ内のリンクまで別ウィンドウで開いてしまう。これはウザい。ってことで先人の知恵を借りた。けど若干のチューニングをしたのでメモっておく。

どうするの?

はてなブログの「設定>デザイン>カスタマイズ>ヘッダ」の「タイトル下」に HTML を書く欄があるので、そこに

<script>window.addEventListener("DOMContentLoaded",()=>document.querySelectorAll("a[href^='http']:not([href*='"+location.hostname+"'])").forEach(anchor=>anchor.setAttribute('target','_blank');));</script>

をコピペして「変更を保存する」ボタンを押して保存。これでおk。

スクリプトを読みやすくして解釈していく

<script>
window.addEventListener("DOMContentLoaded", () => {
  document.querySelectorAll("a[href^='http']:not([href*='" + location.hostname + "'])").forEach( (anchor) => {
    anchor.setAttribute('target', '_blank');
  })
});
</script>

1. document.querySelectorAll を解釈

document.querySelectorAll の引数は CSS セレクタで指定します。その中身である

"a[href^='http']:not([href*='" + location.hostname + "'])"

を解釈していきましょう。

まず a[href^='http'] から…との記事を書いていたのですが、先に location.hostname から。location.hostname は「現在ページURLのホスト名」の文字列です(参考:location.hostname-JavaScriptリファレンス)。ここでは本ブログのホスト名 riocampos-tech.hatenablog.com になります。次に、その前後をみると '" + location.hostname + "' とあります。シングルクオートとダブルクオートとが並んでいて読みづらい上、なぜシングルクオートとダブルクオートとが並んでいるのか?と悩んでしまいます。結論から言ってしまうと、+ location.hostname の前のダブルクオートは冒頭 a から続く文字列を閉じるものです。CSS セレクタは当然ながら文字列なので、文字列の中にさらに文字列であるURLを引用するにはシングルクオートを使う必要があります。しかし文字列の中に引用を入れるのは従来の JavaScript では不可能だった1ので、シングルクオートとダブルクオートとを並べることになるわけです。ややこしいので、先に location.hostname の部分を文字列にしましょう。すると[href*='riocampos-tech.hatenablog.com'] という文字列になります。CSS セレクタ全体を見直してみると

"a[href^='http']:not([href*='riocampos-tech.hatenablog.com'])"

となりました。では改めて解釈していきましょう。

まず a[href^='http'] は「a 要素のうち href 属性の値が文字列 http から始まる要素」という意味です(参考:E[foo^="bar"]-CSS3リファレンス)。

次に :not(〜) は「引数(〜)に該当しない要素」なので a[href^='http']:not(〜) は「a 要素のうち href 属性の値が文字列 http から始まり、且つ引数(〜)に該当しない要素」という意味になります2(参考:E:not(s)-CSS3リファレンス)。で、引数の部分は [href*='riocampos-tech.hatenablog.com'] となっています。ここは「href 属性の値が文字列 riocampos-tech.hatenablog.com を含む要素」という意味です(参考:E[foo*="bar"]-CSS3リファレンス)。

まとめると「a 要素のうち href 属性の値が文字列 http から始まり、且つhref 属性の値が文字列 riocampos-tech.hatenablog.com含まない要素」が document.querySelectorAll で選び出されることが分かります。

2. forEach 以降を解釈

(いずれ書きます)

先人の情報

この記事の内容は、基本的には一つ目のリンク先のままなのだけど、空白や改行を削除したり、jQuery のリンク先をはてな内にしたり、<script>タグの表現を変更したり jQuery から ES5 へ変更したり、ヘッダじゃなくフッタへの設定に変更したり、 function を Fat arrowにしたりしてます。

補足

じつは Document.querySelectorAll() の返値 NodeListNodeList.forEach() を使えなかったのですよ。最近のブラウザは使えるようになったようなので、それで使ってます。古いブラウザ対応は無視w (参考:NodeList.prototype.forEach() - Web APIs | MDN

はてなブログスマホビューのとき

でもね。iPhoneスマホビューで見たときには別タブで開いてくれないのです…なんでなの。 jQuery$(document).ready なのがスマホだとダメなときがあるという話を読んで「これは jQuery を止めちゃおう」と思って ES5 に書き換えたのにな。 HTML 読み込みなどの設定はスマホ用サイトだと別設定になっているし、はてなブログ PRO じゃないとスマホ用の HTML を書けないということに気付きました…うーむ。2年まとめ払いでも月額600円だから、はてなダイアリープラスの価格より大幅値上げに感じてしまうのよね…。 まあそれぞれのブログの末尾に

<script>document.querySelectorAll("a[href^='http']:not([href*='"+location.hostname+"'])").forEach(anchor=>anchor.setAttribute('target','_blank'))</script>

って毎回追記しておいてもいいのだけどね。面倒だけど3

なお、ブログ末尾に上記のスクリプトを追記するのであれば、はてなブログの設定のスクリプト

<script>window.addEventListener("DOMContentLoaded",()=>document.querySelectorAll("a[href^='http']:not([href*='"+location.hostname+"'])").forEach(anchor=>{if(anchor.getAttribute('target')!=='_blank')return;anchor.setAttribute('target','_blank');}));</script>

のように target="_blank" がある場合にはスクリプトを実行しないようにしておいたほうがいいかなと思います。


  1. いまはテンプレートリテラル``を使えば読みやすく書ける。今回の場合だと`a[href^='http']:not([href*='${location.hostname}'])`

  2. セレクタを続けて書くとandになると思われます。

  3. セコイねw