Notion APIを使ってデータベースからページの内容を得る

Notion APIを使ってデータベースにアクセスして、ウェブサイトを表示する方法が、WordPressにとってかわるという内容の話をXの投稿で見かけました。
投稿ではBolt.newを使って作っていましたが、私はBolt.newの無料版のせいか上手くできませんでした。
それでNotion APIでデータベースの内容を得る方法や、記事の内容を得る方法を勉強しようと思いました。
今回は、備忘録の意味も込めて記事にしました。
コードは、GPT-4oを使って制作しました。

このコードで、Notion APIの動作確認ができます。

## データベースから記事IDを得て記事の内容を得る

### 必要なパラメーター

こちらのページで、インテグレーションの作成をします。
https://www.notion.so/profile/integrations

データベースのIDの確認方法
下記の記事などを参考されると、分かりやすいです。
https://note.com/amatyrain/n/ncf7111f74ffa


### コマンド
npmを使います。
Notion API SDKを使います。
```bash
#Notion API　SDK　for JavaScript
npm install @notionhq/client
```

特にフレームワークを使っていませんので、index.jsなどのファイル名で実行できます。
```bash
node index.js
```


```Javascript

const { Client } = require("@notionhq/client");
const fs = require("fs");

//インテグレーション
const notion = new Client({
  auth: "******************************",
});

//データベースID
const DATABASE_ID = "*****************";

// ページの本文（ブロック内容）を取得する関数（再帰的に子ブロックも取得）
async function getPageContent(pageId) {
    const response = await notion.blocks.children.list({
        block_id: pageId,
    });

    console.log("Fetching content for pageId:", pageId); // デバッグ用ログ
    console.log("Response:", response); // APIレスポンスを確認

    // 各ブロックが子ブロックを持つ場合、再帰的にその子ブロックも取得
    for (let block of response.results) {
        if (block.has_children) {
            block.children = await getPageContent(block.id);
        }
    }

    return response.results;
}

// ブロックからテキストを取得する関数（再帰的に子ブロックも処理）
function getTextFromBlock(block) {
    let textContent = "";

    console.log(`Processing block type: ${block.type}`); // ブロックタイプのログ出力

    // 各ブロックタイプに応じて `rich_text` フィールドから内容を抽出
    switch (block.type) {
        case "paragraph":
            textContent = block.paragraph.rich_text.map(textItem => textItem.plain_text).join("");
            break;
        case "heading_1":
            textContent = `<h1>${block.heading_1.rich_text.map(textItem => textItem.plain_text).join("")}</h1>`;
            break;
        case "heading_2":
            textContent = `<h2>${block.heading_2.rich_text.map(textItem => textItem.plain_text).join("")}</h2>`;
            break;
        case "heading_3":
            textContent = `<h3>${block.heading_3.rich_text.map(textItem => textItem.plain_text).join("")}</h3>`;
            break;
        case "to_do":
            const checked = block.to_do.checked ? "[x]" : "[ ]";
            textContent = `${checked} ${block.to_do.rich_text.map(textItem => textItem.plain_text).join("")}`;
            break;
        case "bulleted_list_item":
            textContent = `• ${block.bulleted_list_item.rich_text.map(textItem => textItem.plain_text).join("")}`;
            break;
        case "numbered_list_item":
            textContent = `${block.numbered_list_item.rich_text.map(textItem => textItem.plain_text).join("")}`;
            break;
        case "toggle":
            textContent = `<details><summary>${block.toggle.rich_text.map(textItem => textItem.plain_text).join("")}</summary>`;
            break;
        case "quote":
            textContent = `<blockquote>${block.quote.rich_text.map(textItem => textItem.plain_text).join("")}</blockquote>`;
            break;
        case "code":
            const language = block.code.language;
            textContent = `<pre><code class="${language}">${block.code.rich_text.map(textItem => textItem.plain_text).join("")}</code></pre>`;
            break;
        case "callout":
            textContent = `<div class="callout">${block.callout.rich_text.map(textItem => textItem.plain_text).join("")}</div>`;
            break;
        default:
            textContent = ""; // 未知のブロックタイプには対応せず、空白に設定
            break;
    }

    console.log(`Extracted text for block type ${block.type}: ${textContent}`); // 抽出されたテキストのログ出力

    // 子ブロックがある場合は再帰的に処理
    if (block.has_children && block.children) {
        textContent += "<ul>";
        for (const child of block.children) {
            textContent += `<li>${getTextFromBlock(child)}</li>`;
        }
        textContent += "</ul>";
    }

    // トグルブロックの場合、閉じタグを追加
    if (block.type === "toggle") {
        textContent += "</details>";
    }

    return textContent;
}

// データベースの内容を取得する関数
async function getDatabaseContent() {
    try {
        const response = await notion.databases.query({
            database_id: DATABASE_ID,
        });

        let htmlContent = `
            <html>
            <head>
                <title>Notion Database Content</title>
                <style>
                    table { width: 100%; border-collapse: collapse; }
                    th, td { padding: 8px; border: 1px solid #ccc; }
                    th { background-color: #f4f4f4; }
                </style>
            </head>
            <body>
                <h1>Notionデータベースの内容</h1>
                <table>
                    <tr>
                        <th>ページID</th>
                        <th>タイトル</th>
                        <th>内容</th>
                    </tr>
        `;

        for (const page of response.results) {
            const pageId = page.id;
            const title = page.properties["名前"]?.title?.[0]?.text?.content || "タイトル未設定";

            const contentBlocks = await getPageContent(pageId);
            let content = contentBlocks.map(block => getTextFromBlock(block)).join("");

            htmlContent += `
                <tr>
                    <td>${pageId}</td>
                    <td>${title}</td>
                    <td>${content}</td>
                </tr>
            `;
        }

        htmlContent += `
                </table>
            </body>
            </html>
        `;

        fs.writeFileSync("notion_database_content.html", htmlContent);
        console.log("HTMLファイル 'notion_database_content.html' が作成されました");

    } catch (error) {
        console.error('エラーが発生しました:', error);
    }
}

getDatabaseContent();


```


### 生成されたHTML
************** は、ページIDです。
```html

            <html>
            <head>
                <title>Notion Database Content</title>
                <style>
                    table { width: 100%; border-collapse: collapse; }
                    th, td { padding: 8px; border: 1px solid #ccc; }
                    th { background-color: #f4f4f4; }
                </style>
            </head>
            <body>
                <h1>Notionデータベースの内容</h1>
                <table>
                    <tr>
                        <th>ページID</th>
                        <th>タイトル</th>
                        <th>内容</th>
                    </tr>
        
                <tr>
                    <td>************************************</td>
                    <td>page1</td>
                    <td></td>
                </tr>
            
                <tr>
                    <td>************************************</td>
                    <td>テスト</td>
                    <td>2024年11月9日</td>
                </tr>
            
                <tr>
                    <td>************************************</td>
                    <td>page2</td>
                    <td>1234</td>
                </tr>
            
                </table>
            </body>
            </html>
        

```


### 表示例

![スクリーンショット 2024-11-10 185730-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20977/191e57f9-99a1-a9ee-67c0-d3e9466ecaf1.jpeg)


## 主要なブロックの一覧

### Paragraph（段落）

内容の取り出し: block.paragraph.rich_textからテキストを取得します。
```bash
const text = block.paragraph.rich_text.map(textItem => textItem.plain_text).join("");
```

### Heading 1, 2, 3（見出し1, 2, 3）

内容の取り出し: block.heading_1.rich_text（またはheading_2, heading_3）からテキストを取得します。
```bash
const text = block.heading_1.rich_text.map(textItem => textItem.plain_text).join("");
```

### To-do（チェックリスト）

内容の取り出し: block.to_do.rich_textからテキストを取得し、block.to_do.checkedでチェック状態を確認します。
```bash
const text = block.to_do.rich_text.map(textItem => textItem.plain_text).join("");
const checked = block.to_do.checked ? "[x]" : "[ ]";

```


### Bulleted List Item（箇条書き）

内容の取り出し: block.bulleted_list_item.rich_textからテキストを取得します。
```bash
const text = block.bulleted_list_item.rich_text.map(textItem => textItem.plain_text).join("");
```

### Numbered List Item（番号付きリスト）

内容の取り出し: block.numbered_list_item.rich_textからテキストを取得します。
```bash
const text = block.numbered_list_item.rich_text.map(textItem => textItem.plain_text).join("");
```

### Toggle（トグル）

内容の取り出し: block.toggle.rich_textからテキストを取得します。
```bash
const text = block.toggle.rich_text.map(textItem => textItem.plain_text).join("");
```

### Quote（引用）

内容の取り出し: block.quote.rich_textからテキストを取得します。
```bash
const text = block.quote.rich_text.map(textItem => textItem.plain_text).join("");
```

### Code（コード）

内容の取り出し: block.code.rich_textからテキストを取得し、block.code.languageで言語を確認します。
```bash
const text = block.code.rich_text.map(textItem => textItem.plain_text).join("");
const language = block.code.language;
```

### Callout（コールアウト）

内容の取り出し: block.callout.rich_textからテキストを取得します。
```bash
const text = block.callout.rich_text.map(textItem => textItem.plain_text).join("");
```

## まとめ
今回は、Notion APIからデータの取り出しができるように、コードを作りました。
今回のコードでページのタイトルやコンテンツのブロックの抽出ができましたので、ウェブサイトの表示ができるように加工も可能と思います。

Bolt.newなどのAIを使ったコーディング支援を使ってコードを書くのも良いのですが、仕組みを理解するためには、こうした方法で作っていくのも良いと思います。