JavaScriptでWebサイトの外部リンクを自動判定する方法

Webサイトを運営していると、外部リンクには target="_blank" rel="noopener noreferrer" を必ずつけなければならない。セキュリティ上の理由(opener経由の攻撃対策)とSEO上の理由(リンク先ドメインとの関係性を明示)の両面からだ。

問題は、記事を大量に書いているとリンクの属性設定を忘れがちなことだ。

このサイト(obaba-win.com)でも同じ課題があった。WordPressの記事エディタで書いているとき、外部リンクのたびにいちいち属性を手動で設定するのが面倒だった。そこで、JavaScriptで外部リンクを自動判定して属性を付与する仕組みを作った。

外部リンク判定のロジック

「外部リンク」の定義はシンプルだ。現在のサイトのドメインと異なるドメインを指すリンクが外部リンク。

function isExternalLink(href, currentHost) {
  try {
    const url = new URL(href);
    return url.hostname !== currentHost;
  } catch {
    // 相対パス(/about, ../page など)は内部リンク
    return false;
  }
}

new URL(href) はhrefが相対パスの場合に例外を投げる。相対パスは定義上、同一オリジン内のリンクなので内部リンクとして扱う。

実装コード全体

(function () {
  'use strict';

  const currentHost = window.location.hostname;

  function processLinks() {
    const links = document.querySelectorAll('a[href]');

    links.forEach(function (link) {
      const href = link.getAttribute('href');

      // javascript:, mailto:, tel: などは除外
      if (/^(javascript|mailto|tel):/.test(href)) {
        return;
      }

      if (isExternalLink(href, currentHost)) {
        // すでに設定済みの場合は上書きしない
        if (!link.getAttribute('target')) {
          link.setAttribute('target', '_blank');
        }
        // rel は noopener noreferrer が含まれるよう確実にセット
        const rel = link.getAttribute('rel') || '';
        const relValues = rel.split(' ').filter(Boolean);
        if (!relValues.includes('noopener')) relValues.push('noopener');
        if (!relValues.includes('noreferrer')) relValues.push('noreferrer');
        link.setAttribute('rel', relValues.join(' '));
      }
    });
  }

  function isExternalLink(href, host) {
    try {
      const url = new URL(href, window.location.href);
      return url.hostname !== host;
    } catch {
      return false;
    }
  }

  // DOM構築完了後に実行
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', processLinks);
  } else {
    processLinks();
  }

  // 動的に追加されたリンクにも対応(MutationObserver)
  const observer = new MutationObserver(function (mutations) {
    mutations.forEach(function (mutation) {
      mutation.addedNodes.forEach(function (node) {
        if (node.nodeType !== Node.ELEMENT_NODE) return;
        const links = node.querySelectorAll
          ? node.querySelectorAll('a[href]')
          : [];
        links.forEach(function (link) {
          const href = link.getAttribute('href');
          if (href && isExternalLink(href, currentHost)) {
            if (!link.getAttribute('target')) {
              link.setAttribute('target', '_blank');
            }
            const rel = (link.getAttribute('rel') || '').split(' ').filter(Boolean);
            if (!rel.includes('noopener')) rel.push('noopener');
            if (!rel.includes('noreferrer')) rel.push('noreferrer');
            link.setAttribute('rel', rel.join(' '));
          }
        });
      });
    });
  });

  observer.observe(document.body, { childList: true, subtree: true });
})();

ポイント解説

`new URL(href, window.location.href)` の第2引数

第1引数だけで new URL(href) とすると、相対パス(/aboutなど)がパースエラーになる。第2引数にベースURLを渡すことで、相対パスを絶対URLに展開してからホスト名を比較できる。

// 第2引数なし → エラー
new URL('/about');  // TypeError: Failed to construct 'URL'

// 第2引数あり → 正常
new URL('/about', 'https://obaba-win.com');
// → URL { hostname: 'obaba-win.com', ... }

すでに設定済みの属性を上書きしない

target が設定済みの場合(例: target="_self" で意図的に同タブ遷移させているリンク)は上書きしない。ただし rel については安全性のために noopener noreferrer を追記する。

MutationObserverで動的コンテンツに対応

SPAやWordPressのブロックエディタは、JavaScriptでDOMを動的に追加することがある。MutationObserverを使うと、後から追加されたリンクも自動処理できる。

WordPressへの適用方法

このスクリプトはWordPressのフッターに仕込むのが最適だ。プラグイン(wp_footerフック)やカスタムテーマのfunctions.phpに追加する。

このサイトでは、カスタムRESTプラグイン経由でfooter-scriptとして登録している。

// wp-theme-mods-api.php のfooter-script機能を使う場合
// REST API: POST /wp-json/custom/v1/footer-script
// body: {"script": "...上記のJSコード..."}

Cocoon等のテーマを使っている場合は、テーマの「フッター」設定にそのまま貼り付けてもよい。

よくある落とし穴

javascript:void(0) のようなリンク: 正規表現でプレフィックスを除外しているが、javascript: から始まるhrefは実行させてはいけないので確実にスキップする。

サブドメインの扱い: blog.example.comshop.example.com を同一サイトとみなすかどうかはケースバイケース。現在のコードはサブドメインも外部リンク扱いになる。必要なら hostname.endsWith(rootDomain) で判定を変えられる。

プリレンダリング環境: SSR/SSGで動作させる場合は window の存在確認が必要。

まとめ

外部リンクの自動判定は、URL APIと MutationObserver の組み合わせで実装できる。

重要なポイントは3つだ。

1. new URL(href, baseURL) で相対パスも安全に処理する

2. mailto: / tel: などのスキームはスキップする

3. MutationObserver で動的追加リンクにも対応する

記事を書くたびに属性を手動で設定する手間がなくなり、設定忘れのリスクもゼロになる。WordPressだけでなく、任意のWebサイトにそのまま適用できるので汎用性も高い。