任意のサイトの閲覧履歴をmicroCMSへ記録するChrome拡張機能をつくった

任意のサイトの閲覧履歴をmicroCMSのデータベースへ記録するChrome拡張機能をつくったので、簡単な概要を共有したいと思います。
（拡張機能としては一般公開していませんが、ソースコードは本記事に書いているので再現は可能だと思います）

使用した技術キーワードは、「Chrome拡張機能」「JavaScript」「microCMS」です。

:::note
microCMSに関してはAPIを叩いて、データをPOSTしているだけなので、さらっとしか書いていませんが、APIで気軽にアクセスできるデータストアとしての利用、という形でアドベンドカレンダーに参加させていただきます。

これくらい、気軽に扱えるよ！という例として読んでいただければと思います。
:::

「任意のサイト」は、今回は『pixiv』[https://www.pixiv.net/](https://www.pixiv.net/)ということにしましょう。

こちらのサイトはイラスト・小説作品向けの展示サイトで、各登録ユーザーが作品を公開できます。

今回は作品の中でも、小説作品に着目し、アクセスした小説作品のページのURLを記録するツールをChrome拡張機能で作成しました。

単純にページタイトルとURLを記録するだけですので、複雑なことはしていませんが、わたしはChrome拡張機能をつくったのははじめてですので、覚書として記事にします。

興味のある方は、話半分にお付き合いください。

:::note
サイトと自作ツールというと、スクレイピングの可否とかの話題が思い浮かびますが、今回やることを利用規約と照らし合わせた限り、今回のケースではわたし（記事の筆者）は問題ないと判断しましたので実施します。「あなた」のケースが大丈夫かはわかりませんし、それを判断する責任はわたしにはありませんので、あしからず。
:::

# ツールの基本的なつくり

今回つくるツールは、簡単に言えば以下のようなツールです。

Webページ閲覧の裏側で動作するChrome拡張機能で、URLが一定の条件に一致するページの、URLとページタイトルをmicroCMSへAPIで送信する。

こんな感じで、行っていることはシンプルですので、別のツールのベースとして役立つかも知れませんし、役立たないかもしれません。

では、作成していきます。

# Chrome拡張機能について

Chromeのパソコン版では、拡張機能を追加できます。

細かい説明をここでしたところで、どこそこから聞いた話しかできないため、説明はかっ飛ばし、拡張機能を開発するところから始めます。

https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world

まずは、上記のチュートリアルにしたがって、HelloWorld拡張機能を作成します。

作成したら、手元のChromeで作成した拡張機能を有効にして、動作を確認します。

開発環境としてこれを導入する、みたいな話がないため、すんなりできました。

では、実際のものをつくりはじめます。

# content scriptsでの実装と課題

対象サイトの遷移をコンソールで観察すると、どうやら単純なページ遷移でだけではなく、JSなどによるコンテンツ書き換えのような遷移も起きているようです。なんか、たぶん、SPAとかそんなですね。（ふわふわの説明）

試しに、以下のcontent scriptsのタイプを試したところ、ページの書き換えによる簡易的な遷移だと、コードのリロードが起きないことがわかりました。

https://developer.chrome.com/docs/extensions/get-started/tutorial/scripts-on-every-tab

どうしたらいいかなーと適当なワード「chrome extension detect page change with js」で調べてみると、以下がヒットしました。

https://stackoverflow.com/questions/67727261/how-do-you-detect-changes-in-url-with-a-chrome-extension

これがよさそうに見えるので、background scriptsやservice workerをキーワードにして、これが何をしているのか調べてみます。

https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/basics?hl=ja

どうやら、ブラウザの裏側でスクリプトを動かす機能らしいので、ちょうどいいなと思いながら、実際に試して見ます。

# service workerでの実装

まずは`manifest.json`を書き換えます。

```json:manifest.json
{
    "name": "pi-bookmarks",
    "version": "1.0",
    "manifest_version": 3,
    "permissions": [
        "tabs"
    ],
    "background": {
        "service_worker": "service_worker.js"
    }
}
```

続いて、`service_worker.js`を準備します。

```js:service_worker.js
const TOOL_NAME = "[pi-bookmarks] ";
console.log(TOOL_NAME + "loaded and working!!!!");
```

これで動かします。

service workerの出力は拡張機能のページに「`ビューを検証 Service Worker （無効）`」と出てくるので、それを押すと開発者ツールが立ちあがります。

ここに`[pi-bookmarks] loaded and working!!!!`と出ているので、OKそうです。

そして、さらにstackoverflowで紹介されている「`chrome.tabs.onUpdated`」について調べます。

https://developer.chrome.com/docs/extensions/reference/api/tabs?hl=ja#event-onUpdated

「タブが更新されたとき」イベントらしいので、確かに使えそう。

ドキュメントを参照して、以下のコードを付け足します。

```js:service_worker.js
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
    console.log(tabId, changeInfo, tab);
});
```

これで動かすと次のようなログが取得できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821654/6135ca13-174d-328d-37d5-4c960c10b979.png)

このログを見ると、`changeInfo`の`status`が`complete`になったとき、`tab`の`titie`と`url`を参照すればよさそうです。

```service_worker.js
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
    if (changeInfo.status === "complete") {
        const sendData = {
            title: tab.title,
            url: tab.url
        };
        console.log(TOOL_NAME + JSON.stringify(sendData));
    }
});
```

これだと、あらゆるサイトを閲覧したときに動作してしまうので、URLでパターンマッチします。

```js:service_worker.js
if (changeInfo.status === "complete") {
    const patt = /https:\/\/www.pixiv.net\/novel\/show.php\?id=\d+/;
    if (tab.url.match(patt) === null) {
        return;
    }
    const sendData = {
        title: tab.title,
        url: tab.url
    };
    console.log(TOOL_NAME + JSON.stringify(sendData));
}
```

よしよし、これでよし……と思って動作を確認すると、ログが以下のようになっていました。

```
[pi-bookmarks] {"title":"{前のページのタイトル},"url":"{今のページのURL}"}
```

どうやら、statusがcompleteになった時点では、まだタイトルの書き換えが行われておらず、後ほど更新されるようですね。

動作確認のため、以下を書き加えます。

```js
console.log("======onUpdated======");
console.log("tabId: " + tabId);
console.log("changeInfo: " + JSON.stringify(changeInfo));
console.log("tab: " + JSON.stringify(tab));
```

これで動きは見えました。（ちなみにこの不良が起こるのは、アーカイブページから個別ページへ遷移したときです）

動きとしては、`status: success`のonUpdatedの後に、faviconの更新のonUpdated、タイトルの更新のonUpdatedが来るようです。

なら、お手軽に、もう`changeInfo.title`をトリガーにすることにします。

```js
if (changeInfo.title) {
    const patt = /https:\/\/www.pixiv.net\/novel\/show.php\?id=\d+/;
    if (tab.url.match(patt) === null) {
        return;
    }
    const sendData = {
        title: tab.title,
        url: tab.url
    };
    console.log(TOOL_NAME + JSON.stringify(sendData));
}
```

サイト側の動作が変わったら、対応できなくなる書き方ですが、よしとしましょう。

さて、これで安定して下記のログが取れるようになりました。

```
[pi-bookmarks] {"title":"タイトル - pixiv","url":"https://www.pixiv.net/novel/show.php?id=xxxx"}
```

「アーカイブページから個別ページへ」、「個別ページから個別ページへ」の動作を確認しました。

ちなみに、「個別ページのリロード」では上記のtitleの書き換えが起こらず、console.log処理は動作しません。

# microCMS APIへPOST

これで動作は完成したので、後はちょちょいとmicroCMSのAPIを叩きます。

（microCMS管理画面の操作は割愛します。日本語ドキュメントがありますし、シンプルなので直感的に操作できます）

https://document.microcms.io/manual/create-api


```js
function sendToMicroCms(sendData) {
    const apiUrl = "https://xxxx.microcms.io/api/v1/xxxx";
    const apiKey = "xxxx";

    fetch(apiUrl, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "X-MICROCMS-API-KEY": apiKey
        },
        body: JSON.stringify(sendData)
    })
        .then(response => {
            if (!response.ok) {
                console.error(TOOL_NAME + "Error:", response);
            }
            else {
                console.log(TOOL_NAME + "Success:", response);
            }
        })
}
```

これで、閲覧履歴がmicroCMSへ記録される状態になりました。

microCMSの管理画面からも、POSTされたデータが確認できました。

# APIキーの扱い

でも、APIキーをベタ打ちしているのはよろしくないですね。

配布するつもりがないツールなので、自分用に運用するにはいいですが、GitHubのプライベートリポジトリで保存するのであっても、キーは保存しないことが推奨されています。

よって、うっかりAPIキー公開を避けるため、なんとかしたほうがよさそうです。

ベストではないと思いますが、今はAPIキーを`config.js`へ隔離し、このファイルを`.gitignore`でGit管理から取り除く手法にします。

```js:config.js
const CONFIG = {
    API_URL: "https://xxxx.microcms.io/api/v1/xxxx",
    API_KEY: "xxxx"
};
```

```js:service_worker.js
importScripts('config.js');
(略)
const apiUrl = CONFIG.API_URL;
const apiKey =  CONFIG.API_KEY;
```

# 終わりに

ということで、ひとまずツールは完成です。

今後蓄積するデータを閲覧履歴ツールとして利用していくには、フロントエンド側が必要になりますが、それはまた今後の宿題にしておきます。

ここまで、読んでいただきありがとうございました。