NotionページをChrome拡張機能で操作する方法：初心者向け

## 1. はじめに

今回は、HTML、CSS、JavaScriptのスキルはあるものの、Chrome拡張機能の開発は初めてという方向けに、NotionのAPIを使用してページを管理するChrome拡張機能の作成方法を解説します。この記事では、特定のNotionページ配下にサブページを追加・表示する機能を持つ拡張機能「QuickNotion」の開発プロセスを通じて、拡張機能開発の基礎を説明します。

## 2. なぜChrome拡張機能なのか？

最初は、単純なHTMLページからNotionのAPIを呼び出そうとしました。しかし、ブラウザのセキュリティ機能であるCORS（Cross-Origin Resource Sharing）の制約により、直接APIを呼び出すことができませんでした。

### 2.1 CORSとは？

CORSは、Webブラウザにおけるセキュリティ機能の一つで、異なるオリジン（ドメイン、プロトコル、ポート）間でのリソース共有を制限します。これは、悪意のあるスクリプトが他のドメインの情報を勝手に読み取ることを防ぐために重要です。

### 2.2 なぜChrome拡張機能がCORS制約を回避できるのか？

Chrome拡張機能は、ブラウザに統合された特別な環境で動作します。この環境では、適切な権限設定を行うことで、通常のWebページでは制限されるCROSの制約を回避することができます。具体的には、拡張機能のマニフェストファイル（manifest.json）で`host_permissions`を設定することで、特定のドメインへのアクセスを許可できます。

```json
{
  "manifest_version": 3,
  "name": "QuickNotion",
  "version": "1.0",
  "description": "Manage Notion pages from Chrome",
  "permissions": ["storage"],
  "host_permissions": [
    "https://api.notion.com/*"
  ],
  "action": {
    "default_popup": "popup.html"
  }
}
```

この設定により、拡張機能からNotionのAPIに直接アクセスすることが可能になります。

## 3. Notion APIの設定

### 3.1 インテグレーションの作成

NotionのAPIを使用するには、まずインテグレーションを作成する必要があります。Notionの開発者ポータル（ https://www.notion.so/my-integrations ）にアクセスし、新しいインテグレーションを作成します。

ここで注意が必要なのは、「内部インテグレーション」と「外部インテグレーション」の選択です。今回の拡張機能開発では「内部インテグレーション」を選択します。

- 内部インテグレーション：個人的な使用や、組織内での限定的な使用に適しています。APIトークンの管理が比較的簡単で、セキュリティリスクも低くなります。
- 外部インテグレーション：公開アプリケーションや、多数のユーザーが利用するサービスを開発する場合に適しています。OAuth認証が必要となり、セットアップがやや複雑になります。

今回は個人使用の拡張機能を作成するため、内部インテグレーションを選択します。

### 3.2 アクセス権の設定

インテグレーションを作成したら、次に重要なのがアクセス権の設定です。これは多くの初心者が躓くポイントです。

1. Notionのワークスペースを開きます。
2. 拡張機能でアクセスしたい親ページを選択します。
3. ページの右上にある「...」メニューをクリックし、「コネクトの追加」を選択します。
4. 先ほど作成したインテグレーションを検索し、選択します。

この手順を踏まないと、APIがページにアクセスできず、エラーが発生します。「コネクト」という言葉がやや分かりにくいですが、要するにこのページへのアクセス権を付与する操作です。

## 4. QuickNotionの機能説明

QuickNotionのテスト動画：https://www.youtube.com/watch?v=d0F43O1TmTY

QuickNotionは以下の機能を提供します：

1. APIトークンとページIDの入力と保存：
   - ユーザーはNotionのAPIトークンと親ページIDを入力する必要があります。
   - これらの値はChrome.storage.local（ローカルストレージ）に保存され、次回の使用時に自動的に読み込まれます。

2. Notionページの作成：
   - タイトルと内容を入力し、指定した親ページの下にサブページを作成します。
   - ページ作成にはAPIトークンと親ページIDが必要です。

3. メモ一覧の表示：
   - 親ページ配下のサブページ（メモ）の一覧を表示します。
   - 一覧表示にもAPIトークンと親ページIDが必要です。

4. メモの読み込みと編集：
   - 一覧から選択したメモの内容を表示し、編集可能にします。
   - 選択したメモの内容をAPIを通じて取得し、表示します。


## 5. 拡張機能の実装

それでは、実際の拡張機能の実装に移りましょう。

### 5.1 manifest.json

まず、manifest.jsonファイルを作成します。これは拡張機能の設定ファイルです。

```json
{
  "manifest_version": 3,
  "name": "QuickNotion",
  "version": "1.0",
  "description": "Manage Notion pages from Chrome",
  "permissions": ["storage"],
  "host_permissions": [
    "https://api.notion.com/*"
  ],
  "action": {
    "default_popup": "popup.html"
  }
}
```

### 5.2 popup.html

次に、拡張機能のポップアップUIを定義するHTMLファイルを作成します。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>QuickNotion</title>
    <style>
        body {
            font-family: 'Roboto', Arial, sans-serif;
            width: 300px;
            padding: 20px;
            background-color: #ecf0f1;
            color: #34495e;
        }
        h2 {
            margin-top: 0;
            color: #2c3e50;
        }
        input, textarea, button {
            width: 100%;
            margin-bottom: 15px;
            padding: 10px;
            border: none;
            border-radius: 4px;
            font-size: 14px;
        }
        input, textarea {
            background-color: #fff;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
        }
        textarea {
            height: 150px;
            resize: vertical;
        }
        button {
            background-color: #3498db;
            color: #fff;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #2980b9;
        }
        #memoList {
            max-height: 300px;
            overflow-y: auto;
            margin-top: 15px;
        }
        #memoList p {
            cursor: pointer;
            margin: 5px 0;
            padding: 10px;
            background-color: #fff;
            border-radius: 4px;
            transition: background-color 0.3s;
        }
        #memoList p:hover {
            background-color: #e0e0e0;
        }
    </style>
</head>
<body>
    <h2>QuickNotion</h2>
    
    <input type="password" id="integrationToken" placeholder="Integration Token">
    <input type="password" id="parentPageId" placeholder="Parent Page ID">
    <input type="text" id="title" placeholder="タイトル">
    <textarea id="content" placeholder="内容"></textarea>
    <button id="createButton">ページ作成</button>
    <button id="listButton">メモ一覧</button>

    <div id="memoList"></div>

    <script src="popup.js"></script>
</body>
</html>
```

### 5.3 popup.js

最後に、拡張機能の主要な機能を実装するJavaScriptファイルを作成します。

```javascript
document.addEventListener('DOMContentLoaded', function() {
    const integrationTokenInput = document.getElementById('integrationToken');
    const parentPageIdInput = document.getElementById('parentPageId');
    const titleInput = document.getElementById('title');
    const contentInput = document.getElementById('content');
    const createButton = document.getElementById('createButton');
    const listButton = document.getElementById('listButton');
    const memoList = document.getElementById('memoList');

    // Chrome.storage.localからAPIトークンとページIDを読み込む
    chrome.storage.local.get(['notionIntegrationToken', 'notionParentPageId'], function(result) {
        if (result.notionIntegrationToken) {
            integrationTokenInput.value = result.notionIntegrationToken;
        }
        if (result.notionParentPageId) {
            parentPageIdInput.value = result.notionParentPageId;
        }
    });

    createButton.addEventListener('click', createPage);
    listButton.addEventListener('click', listMemos);

    function createPage() {
        const integrationToken = integrationTokenInput.value;
        const parentPageId = parentPageIdInput.value;
        const title = titleInput.value;
        const content = contentInput.value;

        // APIトークンとページIDをChrome.storage.localに保存
        chrome.storage.local.set({
            notionIntegrationToken: integrationToken,
            notionParentPageId: parentPageId
        });

        // NotionのAPIを呼び出してページを作成
        fetch('https://api.notion.com/v1/pages', {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${integrationToken}`,
                'Content-Type': 'application/json',
                'Notion-Version': '2022-06-28'
            },
            body: JSON.stringify({
                parent: { page_id: parentPageId },
                properties: {
                    title: [
                        {
                            text: {
                                content: title
                            }
                        }
                    ]
                },
                children: [
                    {
                        object: 'block',
                        type: 'paragraph',
                        paragraph: {
                            rich_text: [{ type: 'text', text: { content: content } }]
                        }
                    }
                ]
            })
        })
        .then(response => response.json())
        .then(data => {
            console.log('ページが作成されました:', data);
            alert('ページが作成されました');
        })
        .catch(error => {
            console.error('エラーが発生しました:', error);
            alert('ページの作成に失敗しました');
        });
    }

    function listMemos() {
        const integrationToken = integrationTokenInput.value;
        const parentPageId = parentPageIdInput.value;

        // 親ページ配下のサブページを取得
        fetch(`https://api.notion.com/v1/blocks/${parentPageId}/children?page_size=100`, {
            method: 'GET',
            headers: {
                'Authorization': `Bearer ${integrationToken}`,
                'Notion-Version': '2022-06-28'
            }
        })
        .then(response => response.json())
        .then(data => {
            memoList.innerHTML = '';
            data.results.forEach(page => {
                if (page.type === 'child_page') {
                    const p = document.createElement('p');
                    p.textContent = page.child_page.title;
                    p.addEventListener('click', () => loadMemo(page.id));
                    memoList.appendChild(p);
                }
            });
        })
        .catch(error => {
            console.error('メモ一覧の取得に失敗しました:', error);
            alert('メモ一覧の取得に失敗しました');
        });
    }

    function loadMemo(pageId) {
        const integrationToken = integrationTokenInput.value;

        // 選択されたメモの内容を取得
        fetch(`https://api.notion.com/v1/pages/${pageId}`, {
            method: 'GET',
            headers: {
                'Authorization': `Bearer ${integrationToken}`,
                'Notion-Version': '2022-06-28'
            }
        })
        .then(response => response.json())
        .then(data => {
            titleInput.value = data.properties.title.title[0].plain_text;
            
            // メモの内容を取得
            fetch(`https://api.notion.com/v1/blocks/${pageId}/children?page_size=100`, {
                method: 'GET',
                headers: {
                    'Authorization': `Bearer ${integrationToken}`,
                    'Notion-Version': '2022-06-28'
                }
            })
            .then(response => response.json())
            .then(blockData => {
                contentInput.value = blockData.results
                    .filter(block => block.type === 'paragraph')
                    .map(block => block.paragraph.rich_text[0]?.plain_text || '')
                    .join('\n');
            });
        })
        .catch(error => {
            console.error('メモの読み込みに失敗しました:', error);
            alert('メモの読み込みに失敗しました');
        });
    }
});
```

## 6. エラー対処法

拡張機能の開発中、以下のようなエラーに遭遇する可能性があります：

1. CORS エラー：manifest.jsonの`host_permissions`が正しく設定されているか確認してください。

2. 認証エラー：Integration Tokenが正しいか、有効期限が切れていないか確認してください。

3. ページが見つからないエラー：Parent Page IDが正しいか、そのページにインテグレーションのアクセス権（コネクト）が付与されているか確認してください。

4. API関連のエラー：Chrome拡張機能のポップアップ画面（popup.html）を表示させた状態で、画面を右クリックし「検証」を選択します。これにより開発者ツール（DevTools）が表示され、Consoleタブでエラーメッセージを確認できます。この方法は、NotionのAPIへのリクエスト時に発生するエラーを含む、様々なJavaScriptエラーを確認するのに非常に効果的です[1][4]。

これらの方法を使用することで、拡張機能の様々な部分でのエラーを効果的に特定し、デバッグすることができます。

## 7. セキュリティ上の注意点

1. Integration TokenとParent Page IDは機密情報です。安全に保管し、公開しないよう注意してください。
2. 拡張機能のコードを公開する場合、これらの情報を含めないようにしてください。

## 8. まとめ

この記事では、NotionのAPIを使用してページを操作するChrome拡張機能の作成方法を解説しました。HTML、CSS、JavaScriptの基本的なスキルがあれば、拡張機能の開発は思ったより簡単です。ただし、APIの設定やセキュリティには十分注意が必要です。