🌏

開いている全てのタブのリンクをNotionのページに保存するChrome拡張を作った

2023/08/11に公開
2

はじめに

NotionのAPIとGoogle ChromeのAPIを連携し、Google Chromeで開いているタブのリンクを全てNotionの特定のページに置いておけるChrome拡張機能を作成しました。

作成したもの紹介

拡張機能を有効にし、ブラウザ上部に表示されるアイコンをクリックすると、以下のようなポップアップが表示されます。

フォームにタブのリンクを書き込んでおきたいNotionのページのURLを入力し、「Save Tabs to Notion」と書かれたボタンを押します。

すると、以下のようにURLで指定したNotionのページの最下部に今ブラウザで開いているタブのリンクを含むpageが作成されます。

作成した動機

仕事やプライベートでインターネットを使っているといつの間にかタブを開きすぎてしまいます。

その日のうちに整理すれば良いのですが、「後で参考にするかも」という思いからかついつい放置してしまい、ひどい時には300個くらいタブを開いているといったこともありました。

実際はそんなに頻繁に参考にすることもないし、タブを開きっぱなしにしてもメモリを無駄に消費してしまっているだけです。でも利用していたサイトが近くにまとまっているのが便利な時もあり、如何ともし難いなあと思っていたところでした。

OneTabではダメな理由

こうした問題の解決のためにはOneTabというChrome拡張機能が使えるということは知っていました。

OneTab

しかし、OneTabだと保存しておいたページを探すときにOneTabのページをブラウザから開いて遡らなければいけないので、使いづらいと感じていました。

日記として使っているNotionが使えるのでは?と思い立つ

私は日記/日報代わりにNotionを使っており、日付をタイトルにしたページを毎日1ページずつ作っています。

NotionのAPIを使って、この日記ページにその日に開いていたタブを全て記録しておけば良いのでは?と閃きました。

Notionの日記ページ内に置いてしまえば日付ごとに開いていたページを確認することが容易に出来ます。

Notionの検索にも引っかかるようになるので、後で「あんな記事なかったっけなあ」と探すこともだいぶ楽になります。

作成方法

拡張機能は公開しておらずローカルでのみ動作させています。
この拡張機能を作成する方法を紹介します。

以下のような流れで作成することができます。

1.Chrome拡張機能本体のコードを書く

2.NotionAPIと連携する

3.拡張機能を開発者モードで利用する

1.Chrome拡張機能本体のコードを書く

まずはChrome拡張機能のコードを書きます。

任意のディレクトリを作り、以下の通りファイルを配置する

.
├── background.js
├── icons
│   ├── icon128.png
│   ├── icon16.png
│   └── icon48.png
├── manifest.json
├── popup.html
└── popup.js

この時、iconsディレクトリの中身は任意の画像で大丈夫です。そこまで大きくないサイズ(数十kbまで?)のpng形式か、おそらくjpg形式でもいけると思います。

各ファイルの中身を以下のように編集する

manifest.json
{
  "manifest_version": 3,
  "host_permissions": [
      "https://api.notion.com/*"
  ],
  "name": "Notion Tab Saver",
  "version": "1.0",
  "description": "Save all open tabs to Notion",
  "permissions": [
    "tabs"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

popup.html
<!DOCTYPE html>
<html>
<head>
    <title>Save to Notion</title>
    <style>
        body {
            width: 300px;
            font-family: Arial, sans-serif;
        }
    </style>
</head>
<body>
    <label for="notionUrl">Notion Page URL:</label>
    <input type="text" id="notionUrl" placeholder="Enter Notion page URL">
    <button id="saveButton">Save Tabs to Notion</button>
    <script src="popup.js"></script>
</body>
</html>
popup.js
document.getElementById('saveButton').addEventListener('click', function() {
    const notionUrl = document.getElementById('notionUrl').value;
    const pageId = extractPageIdFromUrl(notionUrl);

    chrome.runtime.sendMessage({ action: "saveTabsToNotion", pageId: pageId });
});

function extractPageIdFromUrl(url) {
    // Notion URLからページIDを抽出するロジック
    const match = url.match(/([a-f0-9]{32})/);
    return match ? match[0] : null;
}
background.js
const NOTION_API_KEY = "secret_***************"; // NotionのAPIキーを書く

function createNotionPage(title, url, parentPageId) {
    const headers = new Headers({
        "Authorization": `Bearer ${NOTION_API_KEY}`,
        "Content-Type": "application/json",
        "Notion-Version": "2021-05-13"
    });

    const body = JSON.stringify({
        "parent": {"page_id": parentPageId }, 
        "properties": {
            "title": { "title": [{ "text": { "content": title } }] }
        },
        "children": [
            {
                "object": "block",
                "type": "bookmark",
                "bookmark": {
                    "url": url,
                    "caption": []
                }
            }
        ]
    });

    fetch("https://api.notion.com/v1/pages", {
        method: "POST",
        headers: headers,
        body: body
    })
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error("Error:", error));
}

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.action === "saveTabsToNotion" && message.pageId) {
        console.log("Received Page ID in background:", message.pageId); 
        chrome.tabs.query({currentWindow: true}, function(tabs) {
            for (let i = 0; i < tabs.length; i++) {
                // 500ミリ秒間隔で保存
                setTimeout(() => {
                    createNotionPage(tabs[i].title, tabs[i].url, message.pageId); // ページIDを関数に渡す
                }, i * 500);
            }
        });
    }
});

2.NotionAPIと連携する

続いて、NotionのAPIを利用するための作業を行います。

NotionのAPIキーを発行する

Notionで自身のワークスペースを持っていればAPIキーを発行することができます。
まず以下のページに遷移してください。
Notionのapiキー発行ページ

次に「新しいインテグレーションを作成する」をクリックします。

適当な名前をつけてインテグレーションを作成すると以下のように「シークレット」と呼ばれているAPIキーを取得することができます。

ちなみにインテグレーションとはAPIを使ってプログラムからNotionを操作するための仕組みです。

こちらを表示したのちコピーします。

NotionのAPIキーをコードに記載する

作成したbackground.jsの1行目にあるAPIキー記載部分に取得したAPIキーを書きます。

background.js
const NOTION_API_KEY = "secret_***************"; // NotionのAPIキーを書く

// 以下省略

注: 本来はAPIキーをソースコード中に含めるべきではありません。簡易的に実装するためにこのようにしています。

保存先のページが含まれるページにてインテグレーションを有効にする

この拡張機能では利用するたびにタブのリンクを保存したいNotionのページを指定します。これに伴いNotionの特定のページ内にて作成したインテグレーションからの操作を許可する設定を行う必要があります。

まずNotionで任意のページを開きます。そして、以下の画像のように右上の「・・・」をクリック → 「コネクトの追加」 → 作成したインテグレーションをクリック、と進んでください。

ここで作成したインテグレーション名をクリックして選択すると、そのページおよびその子ページにてインテグレーションが有効になり、APIからの操作が可能になります。

3.拡張機能を開発者モードで利用する

ここまでで開発自体は完了しました。あとはChrome拡張機能としてこちらのコードを利用するだけです。

Chrome拡張機能は、自分一人で利用するだけであれば簡単な方法で実際に利用できるようになります。その手順を示します。

Chrome拡張機能一覧ページで「デベロッパーモード」をオンにする

以下のページ(Chrome拡張機能一覧)に遷移します。
Chrome拡張機能一覧

右上に「デベロッパーモード」と書かれたスイッチがあるので、ここをクリックしてオンにします。

こうすることで自身のパソコン上のディレクトリを指定してChrome拡張機能として動作させることができます。

作成したファイルたちをChrome拡張機能として読み込む

「パッケージ化されていない拡張機能を読み込む」というボタンを押すと、読み込むディレクトリを選択できます。
作成したファイルたちが入ったディレクトリを選択します。

表示されたChrome拡張機能を有効にする

Chrome拡張機能一覧に作成した「Notion Tab Saver」が表示されます。以下の画像のように右下のトグルをオンにして、有効にします。

Chromeの上部にアイコンが表示されるので、クリックする

アイコンが表示されていれば、クリックすることでこの拡張機能を利用することができます。

保存先のNotionページは、手順2でAPIの利用を許可したページの子ページでなければいけないことに注意してください。

Discussion

elmforestelmforest

興味深い拡張機能、ありがとうございました。
自分でも試してみたのですが、chrome拡張のデベロッパーモードで、以下のエラーが表示され機能しません。

Uncaught SyntaxError: Unexpected token

おそらくpopup.jsの問題だと思うのですが(popup.htmlとpopup.jsが同じ内容になっているかも)、いかがでしょうか?

eiceic

@elmforest さん、コメントありがとうございます。
確かに、popup.jsの中身がhtmlの方と同じになってしまっていました🙇
ご指摘いただきありがとうございます。修正しました。