絶対パス、ルート相対パス、相対パスが混在するHTMLをなんとかする（暫定処置）

## 前書き

サイト制作および保守運用対応の最中で、現場より以下のような相談をいただきました。

**「Aタグにテストドメインへのリンクと、本番ドメインへのリンクが混在しているのを何とかできないか？」**

ん・・・どういうこと？
確認したところ、静的ページのHTML内で、
次のような実装が行われている場所がチラホラあるようです。

`````
<a href="https://honban_web_site.xxxxx.yyyyy.zzzzz/aaaaa.html">Aページ</a>
<a href="./aaaaa.html">Bページ</a>
<a href="/aaaaa.html">Cページ</a>
`````

パスの書き方が混在しています。これだと確かにリンクが混在してしまいますね。
制作者やその時の状況によって書き方が異なっていた、という状態のようです。
（動的生成しているページはそのあたりを考慮しているので、上記事象は起きていません。）

パスの書き方は、どこかで統一するよう舵きりしていただきたいですが、
それとは別アプローチで暫定対処できる方法は無いものかと考えてみました。


## 何がしたいのか

テストドメイン上でのアクセス時限定で、
**「テストドメインへのリンクと、本番ドメインへのリンクが混在している」**
のを、**HTMLは直接修正せず**に暫定対処したい。


なお、想定されるリンクの種類と修正対応は次の通り。

### Aタグ内で定義されたリンクの種類と修正対応

パス | ドメイン | 詳細 | 修正対応
|:--|:--|:--|:--
絶対パス | 本番ドメイン | 末尾がファイル名 | 本番ドメインをテストドメインに書き換える
絶対パス | 本番ドメイン | 末尾が半角スラッシュ | 本番ドメインをテストドメインに書き換える
絶対パス | 本番ドメイン | 末尾がドメイン末尾文字 | 本番ドメインをテストドメインに書き換える
絶対パス | 外部ドメイン | 考慮しない | 何もしない
ルート相対パス | アクセス中のドメイン | 半角スラッシュとファイル名 | テストドメインの絶対パスに書き換える
相対パス | アクセス中のドメイン | 基点が同じ階層 (ファイル名) | テストドメインの絶対パスに書き換える
相対パス | アクセス中のドメイン | 基点が同じ階層 (./) | テストドメインの絶対パスに書き換える
相対パス | アクセス中のドメイン | 基点が上の階層 (../) | テストドメインの絶対パスに書き換える


## やったこと

テストドメイン上でのアクセス時限定で、
A タグ の href を「テスト環境ドメイン」を示す絶対パスに書き換える処理を、
読み込ませるようにしてみました。


### 構成

````
sample.html (該当のHTMLファイルです)
┗ jquery-x.x.x.min.js (jQueryファイルです)
┗ domainchk.js (ホスト名がテスト環境の場合のみ「href_replace.js」を読み込む)
　┗ href_replace.js (A タグ の href を「テスト環境ドメイン」に書き換える処理)
````


まずはHTMLの構成。
上述の「リンクの種類と修正対応」を参考に、現場でみたHTMLのサンプルを準備しました。
HTML内では、絶対パス、ルート相対パス、相対パスが、混在した状態になっています。

### sample.html

````
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="https://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>HOST書き換えテスト</title>
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<script type="text/javascript" src="/js/jquery-x.x.x.min.js"></script>
<script type="text/javascript" src="/js/domainchk.js"></script>
</head>
<body>
<b>テストドメインでアクセスした時だけ A タグ のリンクURLを「テストドメインの絶対パス」に書き換えます</b>
<hr>
<a href="https://honban-xxxxx.yyyyy.zzzzz/index.html">絶対パス 本番ドメイン 末尾がファイル名</a><br>
<a href="https://honban-xxxxx.yyyyy.zzzzz/">絶対パス 本番ドメイン 末尾が半角スラッシュ）</a><br>
<a href="https://honban-xxxxx.yyyyy.zzzzz">絶対パス 本番ドメイン 末尾がドメイン末尾文字</a><br>
<a href="https://www.google.co.jp">絶対パス 外部ドメイン</a><br>
<a href="/index.html">ルート相対パス（/index.html）</a><br>
<a href="index.html">相対パス 基点が同じ階層 (ファイル名)（index.html）</a><br>
<a href="./index.html">相対パス 基点が同じ階層 (./)（./index.html）</a><br>
<a href="../index.html">相対パス 基点が上の階層 (../)（../index.html）</a>
</body>
</html>
````

上記HTML内から、以下のjsを読み込ませました。
実際には、各ページが共通で読み込むHTMLファイル上でjsを読みこませることで、
ファイルごとでjs読み込みをせずに済むよう処置しています。

### domainchk.js

````
$(function() {
    var domain_now = $(location).attr('protocol') + '//' + $(location).attr('host') + '/';
    if (domain_now.match('/test.')) {
        $.getScript(domain_now + "/js/href_replace.js");
    }
});
````

### domainchk.js の処理概要

$(location)を使うと、現在アクセスしているページURL等の情報を取得できます。
**Location - Web API | MDN - Mozilla**
https://developer.mozilla.org/ja/docs/Web/API/Location

domain_now には、現在アクセスしているページURLが入ってきます。

````
    var domain_now = $(location).attr('protocol') + '//' + $(location).attr('host') + '/';
````

次に、現在アクセスしているページURLに、
「テストドメインを示す文字が含まれるか」を確認してから、
別のjsファイルに書いた「hrefの書き換え処理」を読み込ませます。
（テストドメインを直書きすると世に存在が割れてしまうので、URLの部分一致による判断で済ませています。）

これらは、本番へリリースした時に本対応を動作させないための処置となります。

````
    if (domain_now.match('/test.')) {
        $.getScript(domain_now + "/js/href_replace.js");
    }
````


そして「hrefの書き換え処理」は、以下のとおりです。


### href_replace.js

````
$(function() {
    $('a').each(function() {
        var obj = jQuery(this);
        var link = obj.attr("href");
        var url_now = $(location).attr('href');
        var domain_now = $(location).attr('protocol') + '//' + $(location).attr('host') + '/';
        var origin_url = 'https://honban-xxxxx.yyyyy.zzzzz';

        // 絶対パス の対処
        if (link.startsWith('http')) {
            // パス内に本番URLが含まれる
            if (link.startsWith(domain_org)) {
                // 本番URLの後に「/」があるかないか
                if (link.slice(domain_org.length, domain_org.length + 1) == '/') {
                    obj.attr("href", link.replace(domain_org + '/', domain_now));
                } else {
                    obj.attr("href", link.replace(domain_org, domain_now));
                }
            }
            return true;
        }

        // ルート相対パス (/) の対処
        if (link.startsWith('/')) {
            obj.attr("href", domain_now + link.replace('/', ''));
            return true;
        }

        // 相対パス 基点が同じ階層 (ファイル名 or ./) および 上の階層 (../) の対処
        obj.attr("href", new URL(link, url_now).href);
    });
});
````


### href_replace.jsの処理概要

まず、該当ページ内のAタグを精査していきます。

````
    $('a').each(function() {
````

次に現在アクセスしているページURL等の情報を取得や、本番URLの定義など。
（呼び出し前の処理で済ませた方が良かったかもしれません。）

````
        var obj = jQuery(this);
        var link = obj.attr("href");
        var url_now = $(location).attr('href');
        var domain_now = $(location).attr('protocol') + '//' + $(location).attr('host') + '/';
        var origin_url = 'https://honban-xxxxx.yyyyy.zzzzz';
````

続いて、絶対パスの対処。
パス内に本番ドメインが含まれる場合のみ、
現在アクセスしているドメイン（テストドメインと同義）に書き換えます。

なお、外部ドメインは書き換えることなくそのままとしています。


````
        // 絶対パス の対処
        if (link.startsWith('http')) {
            // パス内に本番URLが含まれる
            if (link.startsWith(domain_org)) {
                // 本番URLの後に「/」があるかないか
                if (link.slice(domain_org.length, domain_org.length + 1) == '/') {
                    obj.attr("href", link.replace(domain_org + '/', domain_now));
                } else {
                    obj.attr("href", link.replace(domain_org, domain_now));
                }
            }
            return true;
        }
````

続いて、ルート相対パスの対処。
こちらはリンクの先端に、現在アクセスしているドメイン（テストドメインと同義）を追記すればOK。
そのまま絶対パスとして成立するはずです。

````
        // ルート相対パス (/) の対処
        if (link.startsWith('/')) {
            obj.attr("href", domain_now + link.replace('/', ''));
            return true;
        }
````

最後に、相対パスの対処。上記のどこにも該当しなかったものを処置対象としています。

URL()を使うと、相対パスを絶対パスに置換することができます。
**URL() - Web API | MDN - Mozilla**
https://developer.mozilla.org/ja/docs/Web/API/URL/URL

第１引数に相対パスを、第２引数に絶対パスのベースになるURL、
を指定すれば、絶対パスに変換してくれるスグレモノです。


````
        // 相対パス 基点が同じ階層 (ファイル名 or ./) および 上の階層 (../) の対処
        obj.attr("href", new URL(link, url_now).href);
````

これで、とりあえず完成。


## 実際に使ってみた感想

本番環境へ影響を与えない、かつ、HTML上のパスに手を入れることなく、
Aタグのリンクをテストドメインへ向けることができます。

パスの書き方が乱立している環境では、暫定処置としては中々役立つのではないでしょうか。


## 本当にやりたかったこと

以上は自分なりに空気を読んだ暫定処置でした。
本当にやりたかったことは「Aタグを全て絶対パスかルート相対パスに書き換える」です。
~~（だってね、バラバラじゃわかりにくいし管理でけへry:angel_tone1:）~~

ただ、制作者サイドにはそれなりの都合や理由があるわけです。
それゆえに、責任範囲外の場所で見つかった課題とどう向き合うかはいつも考えさせられるものです。

まぁ勉強がてら、今度そういう処理をPythonで書いてみようかな。


## おまけ：HTML上のリンクはどう書くべきかの所感

適当にググった所感なのですが、なんだか色々と意見が割れている模様。
「絶対パス派」「ルート相対パス派」「相対パス派」とそれぞれに筋だった見解があるようです。

その上で、私なりの意見は「絶対パス」が良いかなと考えています。

### 理由
Google  Search Console で絶対パスが推奨されている
https://support.google.com/webmasters/answer/35156?hl=ja

上記では、
**「可能な場合は、相対リンクではなく絶対リンクを使用します（たとえば、サイト内の別のページにリンクするときに、mypage.html だけではなく https://www.example.com/mypage.html とします）」**
と書かれているようです。

推奨の理由までは書かれていないようですが、
推察するに「相対パス」でリンク切れ（制作側のヒューマンエラー）が起きやすいことを、
Google側が体感しているから、なのかもしれません。

単にGoogleがそう言っているから、ではなくて、
その背景にあるものを考えると「絶対パス」がベターなのかなと考えるようになりました。
（上記を知るまでは「ルート相対パス」が良いかなと思ってました。）


## まとめ

HTML上のパスはなるべく統一したルールで書いてもらいたいなぁと思った次第:flushed: