外部リンクに自動でtarget=”_blank”を書き込むスクリプト

標題の件をご所望のようなのでスクリプトを書いてみました。

さくっと作ったのが以下のスクリプト

var siteDomain = location.href.split('/')[2];
var $a = $('a', '#main article .article-body');
$a.each(function(){
	var $this = $(this);
	var $href = $this.attr('href');
	var $hrefArr = $href.split('.');
	var n = $hrefArr.length;
	if((siteDomain !== $href.split('/')[2]) || $this.hasClass('download') || ($hrefArr[n-1] === 'pdf')) {
		$this.attr('target', '_blank');
	}
})

う~ん。。動くんでしょうけど、何かイケていませんねえ。
変数が多すぎる気がするのと、以前ブログにjQueryのeachメソッドは重いらしいと書いたので、その部分を減量。

すると、こうなりました。
※注:下方にさらに書き直したスクリプトを掲載

var $a = $('a', '#main .article-body, #comment article');
for(var i = -1, n = $a.length, z, $this; ++i < n;) {
	$this = $($a[i]);
	z = parseInt($this.attr('href').split('.').length, 10) - 1;
	if($this.attr('target') !== '_blank' && location.href.split('/')[2] !== $this.attr('href').split('/')[2] || $this.hasClass('download') || ($this.attr('href').split('.')[z] === 'pdf')) {
		$this.attr('target', '_blank');
	}
}

変数の削除も若干偽装しているのと、ループの制御部分が若干トリッキーですがこれで動いております。

このスクリプトは以下の条件に適用する意図で書いています。

  • ドメインが違うサイト
  • a要素にdownloadというclassがついているもの
  • リンク先の拡張子が.pdfの場合

if文の条件式の部分はもっとスマートに書けるような気がします。
よろしければリファクタリングするなり、改善点を指摘していただければありがたいです。

以下追記

var $a = $('a', '#main .article-body, #comment article');
var domain = location.href.split('/')[2];
for(var i = -1, n = $a.length, href, $this; ++i < n;) {
	$this = $($a[i]);
	href = $this.attr('href');
	if(
		$this.attr('target') !== '_blank' &&
		!href.match(/^\/{1}/) &&
		!href.match(/^#/) &&
		domain !== href.split('/')[2] ||
		(href.match(/^\/{2}/) && href.split('/')[2] !== domain) ||
		href.match(/\.pdf$/) ||
		$this.hasClass('download')
	) {
		$this.attr('target', '_blank');
	}
}

href内のURLの解析について認識が甘かったです。
条件としては以下のようになると思うのですが、当初書いてたスクリプトではサイト内のリンクにも_blankがつくようになっていました…

  • http://met.hanatoweb.jp/ -> false
  • //met.hanatoweb.jp/ -> false
  • http://www.qript.co.jp/ -> true
  • //www.qript.co.jp/ -> true
  • /archives/208/ -> false
  • #rev01 -> false

一応修正したもののもっと良い書き方があるでしょう。
また何か発見したら追記します。

さらに追記しました。
以前までのURLリストでは以下になっていましたが、

  • http://met.hanatoweb.jp/ -> false
  • //met.hanatoweb.jp/ -> false
  • http://www.qript.co.jp/ -> true
  • //www.qript.co.jp/ -> true
  • /archives/208/ -> false
  • #rev01 -> false

以下のパターンが足りなかったことに気付きました…

  • archives/208/ -> false
  • 01.html -> false
  • A.html -> false

それに対応した条件分岐が以下のとおり。。

var $a = $('a', '#main .article-body, #comment article');
var domain = location.hostname;
for(var i = -1, n = $a.length, href, $this; ++i < n;) {
	$this = $($a[i]);
	href = $this.attr('href');
	if(
		$this.attr('target') !== '_blank' &&
		!href.match(/^#/) &&
		!(href.match(/^[0-9a-zA-Z]/) && !(href.match(/^http/) || href.match(/^https/))) &&
		!href.match(/^\/{1}/) &&
		domain !== href.split('/')[2] ||
		(href.match(/^\/{2}/) && href.split('/')[2] !== domain) ||
		href.match(/\.pdf$/) ||
		$this.hasClass('download')
	) {
		$this.attr('target', '_blank');
	}
}

domain変数をlocaion.hrefを配列にしてからホスト名部分を取り出してましたが、location.hostnameで取れたようです。勉強不足!
考えれば考えるほどこんなのでいいのって気分になります。

さらにさらに追記しました。
出発点から間違っていた。基本的に以下のようなリンクはないものとして扱ったほうがいいだろうから、もっとシンプルにかけましたな…。

これはバギーな書き方

"//"から始まるものを除外するとこう書けます。

var $a = $('a', '#main .article-body, #comment article');
var domain = location.hostname;
for(var i = -1, n = $a.length, href, $this; ++i < n;) {
	$this = $($a[i]);
	href = $this.attr('href');
	if(
		$this.attr('target') !== '_blank' &&
		(domain !== href.split('/')[2] && href.match(/^http:\/\//) || href.match(/^https:\/\//)) ||
		href.match(/\.pdf$/) ||
		$this.hasClass('download')
	) {
		$this.attr('target', '_blank');
	}
}

なんとなくセレクタとかメソッドチェーンでもできそうな気がしないでもないけど。
かなりダメダメなコードについて悩んでいました。。

「外部リンクに自動でtarget=”_blank”を書き込むスクリプト」への8件のフィードバック

  1. 初回にエリア内の全a要素をループさせることになるので、クリックしてからそのリンクのみ評価してwindow.openするのもありかなと思いました。

    $a.click(function(){
    var href = $(this).attr(‘href’);
    if(hogehoge)・・・
    window.open(href,’_blank’);
    });

    みたいな形で・・・。

    1. なるほど…それを採用しましょう!

      var domain = location.hostname;
      $('a', '#main .article-body, #comment article').click(function(){
      	var href = $(this).attr('href');
      	if(
      		$(this).attr('target') !== '_blank' &&
      		(domain !== href.split('/')[2] && href.match(/^http:\/\//) || href.match(/^https:\/\//)) ||
      		href.match(/\.pdf$/) ||
      		$(this).hasClass('download')
      	) {
      		window.open(href, '_blank');
      		return false;
      	}
      });
      

      と、書き直して気付いた。
      リンクにアイコンを出すトリガーが[target=_blank]になっているのでこの仕様じゃアイコンが出ないですね。
      結局classをつけるとか運用面でカバーしないといけなくならず、「めんどくさい」って結論に達するでしょうね…。

      外部リンクにするしないは記事作成者に委ねるのが一番いいのかもしれませんな…。

      1. なるほど・・・。
        アイコンを出すためにもやはり一度a要素はループ必要ですね・・・。

        しかし実案件とかで考えた場合、「target=”_blank”」なんて書けない人もいると思うので、こういうプラグインも大事ですね。

  2. 僕ならindexOf使うかも。

    var host = location.hostname;
    $(‘a’).each(function(){
    var href = $(this).attr(‘href’);
    var isOtherHostLink = href.indexOf(host) == -1;
    var isDownloadLink = $(this).hasClass(‘download’)
    var isPdfLink = href.indexOf(‘.pdf’) == href.length – 4;
    if(isOtherHostLink || isDownloadLink || isPdfLink) $(this).attr(‘target’, ‘_blank’);
    });

    1. あ、間違い・・・
      var isOtherHostLink = href.indexOf(host) == -1;
      ↓↓↓↓
      var isOtherHostLink = href.indexOf(‘http’) == 0 && href.indexOf(host) == -1;

      1. なるほど、自分の使い慣れたメソッドばっかり使ってしまうので参考になります。
        indexOfを使った方が圧倒的にわかりやすいですね。。

        1. あ!ひらめいた!
          $(‘a[href^=”http”]:not([href*=”‘+location.host+'”]),a[href$=”.pdf”],a[class=”download”]’).attr(‘target’, ‘_blank’);
          条件怪しいけどセレクタにフィルタかけるやつ。

  3. みなさんのお力を借りまして、最終的にこんなコードになりました。

    $('a[href^="http"]:not([href*="'+location.host+'"]),a[href$=".pdf"],a.download','#main .article-body, #comment article').attr('target', '_blank');
    

    ありがとうございました!!

コメントは停止中です。