はてなブログのURLを変更したときに、新しいページへリダイレクトしたいのです！

#はじめに
はてなブログって、記事個別ページのURLを後から変更すると、リダイレクトせずにリンクが切れちゃうのですね。まぁ、仕方ない仕様かもしれませんけど、やっぱり変えたくなることありますね。

- 昔作った気に入らないURLを直そうにも、リンク切れちゃうと流入減ってしまうなぁ
- でも、何も考えず付けてしまったURLを今後も使い続けるのも嫌だなぁ

という問題を解決するために、**URLを変えても困らないようにする仕組み**を考えました。

特定のサービスのワークアラウンドをQiita書くかどうか、悩みましたけど。

##お断り
:warning: この方法では、SEO的なページ価値については考えていません。
本来はステータスコード404を返すのではなく301や302で転送したいところですが、はてなブログの仕様上、いったん404とHTMLが戻ってしまいます。その状況を受け入れた上でできる最善の策を実装してあります。
一番いいのは、URLを変えないことです。大事に使いましょう。

ということを含め、お使いになる場合はご自身の判断でお願いします:pray:


##対象の読者
- はてなブログを使っていてURL変えたい人、もう変えちゃった人
- Javascriptを理解できる人

##使ったもの
- Javascript 100行以内
- [microCMS](https://microcms.io/) （フリーライセンス）
- はてなブログ（無料版）

##解決したいこと整理
プログラムを作って解決したいことは、`URLを変更するハードルを下げたい` これだけです。

このためにやることは、この2点です

- 変更前と変更後のURLをペアリングしておいて、自動でリダイレクトする方法を実装する。
- ペアリングしたものを編集する方法を簡単にする。1年後でも簡単に実行できるレベルで作る。

Javascriptの中に、変数（配列）として新旧のURLのマッピングを持つのも考えましたが、URLが増えるたびにScript自体を改変することになるので辞めました。
どこで何してるか分からなくなるのと、編集ミスが起きるのが想像できました。

##仕掛けの流れ

:zero: まず、microCMSというヘッドレスCMSに、新旧のURLデータのマッピングを用意しておきます。

:one:（はてなブログの機能を使って）いつでも表示されるHTMLのパーツに、自作のJavascriptを仕込みます。普通のページでも404のページでも常に落ちてくるJavascriptです。

:two:Javascriptがロードされると関するが実行されます。関数の中で、今表示しているHTMLが期待したものか判断します。

:three: 404のページの場合、Javascriptの中でmicroCMSのAPIを叩き、旧URLに対応する新しいURLを探します。

:four: 新しいページがあった場合は、window.location.hrefを変更して、ブラウザを新しいリソースへ移動させます。

##仕組化したとしても、逃れられないこと
**旧URLと新URLの対応表を作ること**
こればっかりは、人間しか分からないです。URLを変更する前にメモ帳に残しておいてください。忘れてしまったら、もう分からないです。

**microCMSの契約と準備**
メンテナンスビリティを上げるためにmicroCMSを使うことにしました。
契約してデータを登録することは自分でやる必要があります。
あと、無料で使える範囲で十分だと思いますが、お金が発生する事態になったらお支払いお願いします。

**はてなブログにjavascriptを設置すること**
はてなブログのスクリプトが書けるエリアにJavascriptを登録するのは、自分でやってください。


#microCMSの準備
`microCMS`に旧URLと新URLのデータベースとデータを作ります。（microCMSの記事じゃないので、ところどころ端折ります）

**サービスの登録（xxx.microcms.ioのxxxの単位を作る作業）**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/a408dec1-3de1-cf47-78d7-9125bb4656d8.png)

**サービスの下にコンテンツの作成(xxx.microcms.io/api/v1/yyyのyyyの単位を作る作業）**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/ced45aac-45d2-6dac-1759-50485ca72e0d.png)

**コンテンツのスキーマ作成（テーブル定義のようなもの）**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/5e572c89-f96a-c6b5-0e7c-c3650a73e199.png)
`motoPathName`と`newPathName`の2カラムのデータ構造

**データのインポート**
作ったコンテンツに、実データを入れます。
データが空っぽのときはCSVインポートができるので、最初はCSV作ってアップロードします。contentIdはアップロード後に勝手につくので空でよいそうです。

```csv
contentId,motoPathName,newPathName
,/entry/old-aaa,/entry/new-aaa
,/entry/old-bbb,/entry/old-bbb
```

インポート完了するとこんな感じです。23件入れました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/bc527498-f756-d480-cd95-db5f6b3483a4.png)

##APIのURL組み立てる
APIを呼び出す側は、`motoPathName`が分かっているのを前提にして新しいURLを1件だけ取り出したいので、このようなクエリーにしました。

```
https\://xxx.microcms.io/api/v1/yyy?limit=1&fields=motoPathName,newPathName&filters=motoPathName[equals]old-aaa
```

**fields=**
余計なフィールドを戻さないような指示です。カンマで区切ったフィールドだけ返ってきます。フリープランだと転送量の上限が100Gバイトなので転送量の節約目的です。

**filters=**
APIが戻すデータの条件指定です。microCMDのAPIだとこのように書きます。
SQLでいうと、`WHERE motoPathName = 'old-aaa'` という条件を指定しているのと同じです。

さらに詳しく知りたい場合は、こちらへどうぞ。
https://microcms.io/docs/content-api/get-list-contents


これで、データベース側は準備完了です。

#Javascript側
Javascriptはちょっと厄介でした。

##作戦会議
404のときだけ新しいURLにリダイレクトしたいのですが、まず、表示中のコンテンツが404であるということをjavascript側で捕らえるのができないです。404のときだけ表示されるHTMLを設定でできるなら、発動のタイミングはわかりやすいですが、そういうことはできないらしい。

考えた末、全てのコンテンツ共通で`作成したjavascript`を引き込み、関数を実行してHTMLの特徴から**404っぽい**か判定し、**404っぽい**ときだけAPIを実行し、新しい行先があるときだけリダイレクトするという流れにしました。

:sunflower: **404っぽい**というあいまいな表現なのは、表示されたHTMLコンテンツとそこに\<script>タグで吐き出されたscriptからは、HTMLがサーバーから戻されたときのレスポンスコードを取得することができないからです。
画面が表示されたあとに同じURLにHTTP HEADを投げなおすと正確に分かると思いますけど、アクセス数が2倍になる可能性があるので、止めておきました。

**404っぽいなの判断基準**
2020年5月時点のはてなブログの個別ページでは、404のときには`<link rel="canonical">`が入っていなかったので、canonicalがない=404っというゆるい作戦で行かせていただきます。404なのにcanonical突っ込んできたら破綻します。

##Javascriptの実装
Javascriptこんな感じになりました。
はてなブログのHTMLが書ける領域に、Scriptタグ込みで書き込めばよいです。

```html
<script>

const myEndpoint = 'https://xxx.microcms.io/api/v1/yyy';
const myKey = 'input your key';
const doRedirect = true;

redirectWhen404(myEndpoint,myKey, doRedirect);

async function redirectWhen404(microCmsURL, microCmsKey, doRedirect){

    let pathName = window.location.pathname;
    console.info('Hatena 404 checker, %s', pathName);

    if( !pathName.startsWith('/entry/') && !pathName.startsWith('/archive/category/') ){
        console.log('This page is not supported this script.');
        return;
    }

    let notFoundPage = isThisPage404();
    console.log('404=>' + notFoundPage);
    if( !notFoundPage ){
        return;
    }

    let apiResult = await findRedirectPath(pathName);
    console.log(apiResult);

    if(!apiResult){
        //microCMS returns 200

    }else if(apiResult.totalCount !=1){
        console.log('new Pathname is not found from api = %s', pathName);
        //行先のない純粋な404のとき
        //カスタム404っぽくするために、DOMをいじるならここでどうぞ

    }else{
        let newPathName = apiResult.contents[0].newPathName;
        console.info('* Redirect to %s', newPathName);
        if(doRedirect){
            window.location.href=window.location.origin + newPathName;
        }
    }


    async function findRedirectPath(pathName){
        let parameter = `?limit=1&fields=motoPathName,newPathName&filters=motoPathName[equals]${pathName}`;
        let url = microCmsURL + parameter;
        let config = {
            method: "GET",
            headers: {
                "Content-Type": "application/json; charset=utf-8",
                "X-API-KEY": microCmsKey,
            },
        };

        let response = await fetch(url, config);
        if( response.status != 200){
            console.error('microCMS returns status %s',response.status);
            return null;
        }
        let json = await response.json();
        return json;
    }

    function getCanonical(){
        var links = document.getElementsByTagName("link");
        for ( i in links) {
            if (links[i].rel && links[i].rel.toLowerCase() == "canonical") {
                return links[i].href;
            }
        }
        return "";
    }

    function isThisPage404(){
        if( getCanonical() == "" ){
            return true;
        }
        return false;
    }
}

</script>
```

コードはGitにあげてました。
https://github.com/kanaxx/hatenablog

##ソースの解説
先頭の2つの変数は、microCMSの定義情報なので各自の環境に合うように変えてください。

**スクリプトの実行判断**
URLのpathnameが`/entry/xxx` か`/archive/category/xxx`でない場合には、この仕組みを作動させないようにしています。個別ページとカテゴリページのリダイレクトが目的だからです。APIのコール回数を減らす目的です。

**404っぽい判断**
実行条件内のときは、HTMLのデータから`getCanonical`関数で`canonical URL`を取っています。複数個のcanonicalがあることはないですが、最初の1個目をcanonicalとします。
canonicalが取れたら正常なページ、取れなかったら404っぽいページとして、止め時の判断に使います。

**404っぽいとき**
対象ページかつ404っぽい画面のときは、microCMSのAPIを実行して問い合わせをします。HTTPSのアクセスに`fetch`を使いましたので、古いブラウザでは動かないかもしれません。
`fetch`の非同期ハンドリングが面倒なので、全て`await`にしました。HTMLページが表示されてから1回だけ動くので、同期も非同期も大した違いはないです。

**エラーハンドリング**
APIが200な反応しなかったり、旧URLから新URLが1件に定まらないときは、リダイレクトはせずにそのページにとどまります。（こうなると、microCMSのクエリーに付けている`limit=1`は余計かもしれない。）

**リダイレクト部分**
最後にリダイレクト部分。変なところには飛ばないように`window.location.origin`からドメイン名を取ってます。リダイレクト先の`pathname`は、microCMSに登録したデータを信用するので、あまり余計ことは考えずに文字列結合して飛ばすだけです。

スクリプト起動時の第3引数の`doRedirect`がtrueのときだけリダイレクトします。
コードを直しているときにリダイレクトされると困る場合とか、logを見たいときは`false`にすれば、リダイレクトしません。（URLパラメータで制御するのがよさそうですけど）


#スクリプトの設置
Qiitaに書くことじゃないのでさらっと。

デザイン設定のフッタあたりに\<script>タグで囲って入れればよさげです。それっぽいところにそれっぽく置いてください。`myEndpoint`と`myKey`は自分のものに置き換えるのを忘れずに。

こっちに書きました。設置するための作業中心なので、
https://kanaxx.hatenablog.jp/entry/hatenablog/entry-redirect

##注意点
:warning: このスクリプトを実際のブログに置く場合、`microCMSのURL(endpoint)`と`APIのキー`がHTML内に露出します。HTMLソースを表示すると誰でも見つけられます。第三者がAPIを自由に使うことができる状態で、いたずら可能ということですね。
考えられるリスクは、いたずら行為によりmicroCMSの転送量を使い果たすことです。

今回はGET APIのキーだけを使いましたので、キーが知られても起きる問題は小さいですが、POST用のAPIキーはデータを消すなどの破壊的ないたずらもできます。ちゃんとmicroCMSの説明書を読んで理解したうえで使うようにしてください。


#ミステイク
~~`/archive/category/*`のパターンも、旧→新のリダイレクトすれば良かった。あとで直すか。~~
カテゴリ名を変更したときも対応できるように直し済み。最初の条件必要かな。。