外部リンク用クッションページを HTML + JavaScript のみで作成する
Web サービスによっては、外部リンク(そのサービスのドメインと違うドメインのリンク)をクリックした際、クッションページが挟まれることがあります。
このクッションページは、
- リンク先のページが外部リンクであることを明示する
- リンク先のページが安全であるかどうかをユーザに確認して貰う
- 機密情報をリファラ情報としてリンク先に送信しないようにする
- どの外部リンクがクリックされたかをトラッキングする
などの目的で利用されます。
多くの場合はサーバーサイドでクッションページを生成していますが、実は HTML + JavaScript のみで作成できるのではないか? と思い立ち、作ってみました。

実際のコード(リンクが設置されているページ用)
HTML は適当で良いのですが、リンクが設置されているページとは別にクッションページが必要です。
リンクが設置されているページ用の JavaScript は以下です。
// 元ページ用の JavaScript // クッションページの URL を定義 const cushionPageUrl = 'blog/cushion?target='; /** * 指定された要素内の全ての a 要素における href 属性が外部リンクの場合、クッションページへのリンクに置換する * @param links {Array<HTMLAnchorElement>} a 要素の配列 */ const rewriteLinksToCushionLinks = (links) => { // links が HTMLCollection or NodeList でない場合は何もしない if ( ! (links instanceof HTMLCollection || links instanceof NodeList)) { return; } // 内部リンク判定用の正規表現を定義 const internalLinkPattern = new RegExp(`^${location.origin}`); // 全ての a 要素に対して判定、置換処理を実行 links.forEach(link => { // a 要素でない場合は何もしない if ( ! (link instanceof HTMLAnchorElement)) { return; } // href 属性が空の場合は何もしない if ( ! link.href) { return; } // 既に変換済みのリンクの場合は何もしない if (link.href.indexOf(cushionPageUrl) === 0) { return; } // href 属性がページ内リンク or 内部リンクの場合は何もしない if (internalLinkPattern.test(link.href)) { return; } // href 属性が外部リンクの場合はクッションページへのリンクに置換 link.href = cushionPageUrl+encodeURIComponent(link.href); // target 属性を _blank に設定 link.target = '_blank'; }); }; // ページ読み込み完了時に実行 document.addEventListener('DOMContentLoaded', () => { // 全ての a 要素に対して処理を実行 // rewriteLinksToCushionLinks(document.querySelectorAll('a')); // class="target-links" を持つ最初の要素内の全ての a 要素に対して処理を実行 rewriteLinksToCushionLinks(document.querySelector('.target-links').querySelectorAll('a')); });
const cushionPageUrl = 'blog/cushion?target=';
の部分は、クッションページの URL を指定します。
?target=
という部分はそのまま固定です。
実際にこの記事にも仕込まれているので、動作を確認してみて下さい。
実際のコード(クッションページ用)
一方で、クッションページ側には以下の JavaScript を設置します。
// クッションページ用の JavaScript document.addEventListener('DOMContentLoaded', () => { // クエリパラメータからリダイレクト先 URL を取得 const url = new URLSearchParams(location.search).get('target'); // リダイレクト先 URL が取得できない場合は何もしない if ( ! url) { return; } // javascript: が含まれる場合は何もしない if (url.indexOf('javascript:') !== -1) { return; } // リダイレクト先 URL にリダイレクト const anchor = document.createElement('a'); anchor.setAttribute('href', decodeURIComponent(url) .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'")); anchor.textContent = url; document.getElementById('link-url').appendChild(anchor); });
リンク先として a タグを生成したい親要素に id="link-url"
を設定しておきます。
以上で実際に動作するかと思います。
コードの解説

元ページ用の JavaScript
rewriteLinksToCushionLinks
関数で受け取った a 要素の href 属性を置換しているのですが、
- ページ内リンク(同じページに存在する id が指定されているリンク)
- 内部リンク(同じドメイン内のリンク)
は無視するようにしています。
HTMLAnchorElement は
.href
でリンク先の URL を取得できるのですが、これは実際に href 属性に設定されている値ではなく、常に絶対 URL で取得できるという特徴があります。
location.origin
で、ページのドメインを取得できるので、これが先頭に含まれるリンクはページ内リンク or 内部リンクとして無視しています。
そして、リンク先の URL をクッションページの URL のうち、 target の値として設定した URL に置換しています。
クッションページ用の JavaScript
クエリパラメータ(target)から元 URL を取得して、その URL に遷移する a タグを生成しています。
XSS 対策として、href 属性に対する URL に含まれる特殊文字をエスケープしていますが、画面に表示するテキストは .textContent に設定しているため、そのままで問題ありません。
別途、「javascript:」から始まる href 属性は JavaScript を実行してしまうため、これを無効化しています。
まとめ
意外と HTML + JavaScript だけでクッションページを作成できることが分かりました。 この記事自体はサーバサイドでプログラムが動いていますが、実際に上記 JavaScript を使用する場合は素の HTML ファイルで問題ないはずです。
もちろん、セキュリティ面などを考慮して、実際に運用する際は十分な検証を行ってください。
プログラマー/N.Go