外部リンク用クッションページを HTML + JavaScript のみで作成する
この記事は2024/08/01に作成されました。
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