Notion API＋PHP(MySQL)でサークルのWebサイトを構築した話

こんにちは、KadoBloG（カドブログ）のKadoです。
今回は、しばらく前に作成した[サークルのWebサイト](https://engineer-egg.com/)で使用した技術として、Notion APIとPHPを使用してブログ型サイトを構築してみた、という話を解説していきます。

:::note info
本項は[Nihon University Advent Calendar 2023](https://qiita.com/advent-calendar/2023/nihon-u)の6日目の記事になります。
:::

## 簡易自己紹介

- 日本大学工学部 情報工学科 3年
- 趣味がWeb開発 プログラミングは高校３年から
- 本格的に没頭したのは大学２年
- インターン募集しています(小声)
- 元々はHTML, CSS, JavaScript, PHPのフレームワークなし（バニラ）開発
- 現在はNext.jsも使う

## サークルのWebサイトについて

まずは簡単にWebサイトの概要をお伝えします。「起業サークル」は、本学部で2023年3月に落成されたばかりのサークルです。私は全く起業する気はないですが「お前頭ええやろ」的なノリで加入させられました。全く起業する気はないですが（二回目）。

https://engineer-egg.com/

とはいえ何もしないのはもったいないと思い、サークルのWebサイトを作ることにしました。元々CMS構築や[ブログ型サイト](https://fast5-blog.com/html-product/uni-web/)をPHPで作成したことがあり、自分の技術で1つWebサイトを作る、ということはギリギリ可能でした。

### 3ヶ月の期間で完成

![美しいWebアニメーション](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/069e27a0-6934-1428-cff9-781b7dcd3e8e.gif)
正確には2月~6月らしいが、実際活動してたのは3ヶ月。PHPとはいえ、LaravelやReactなどのフレームワークを用いない、いわゆるバニラ開発で行いました。当時は両者とも知識がなかったので。

デザインからフロントとしてCSS(SCSS)やJavaScriptで起こし、バックエンドも自分一人で開発したロング開発は多分これが初めて。3ヶ月という大きなプロダクトでしたが、今までできた技術の集大成ということもあり、モチベーションが落ちることなくやり遂げることができました。

![スクリーンショット 2023-12-01 14.52.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/de2aeaf0-0651-2879-48c0-a1fd07e37ded.png)

ようやくSEOにも恵まれ、検索にも表示がされるようになりました。

### 扱いやすいCMS付き

![スクリーンショット 2023-12-01 15.59.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/b988cdf1-e577-21f3-2a7a-6a1024ff337a.png)

もちろんCMSも充実しており、基本的なブログ設定や、Notion APIによるブログ設定ができるようになっています（こっちのほうが命かけたくらい）。

![スクリーンショット 2023-12-01 16.02.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/6ea003a5-1410-03a5-534a-ca7ee862f5f3.png)

後述で解説しますが、こちらがNotionと連携して管理できる物となっています。

### 技術スタック

- フロントエンド
    - HTML
    - CSS(SCSS)
    - JavaScript
- バックエンド
    - PHP
    - MariaDB(MySQL)
    - Notion API（今回の主役、らしい）
- デプロイ先
    - レンタルサーバー
- その他ツール
    - figma, Goodnotes, Notion

※GitHubリポジトリはセキュリティの観点でプライベートとなっています

## Notion APIとは

ここからが本題です。Notionは、**メモアプリの最強版**です。ページの中にページを作れたり、マークダウンで記述できたり、データベースを作れたり、複数人で編集、公開したりと、とりあえず最強のメモアプリです。
そんなNotionは、`Notion API`を公開しており、いわゆるCRUD（Create, Read, Update, Delete）機能として、ページのデータを取得できたり、データベースの取得、追加ができます。

https://developers.notion.com/

### なぜサークルのWebサイトで採用したか

そもそも、**Wordpressでよくね？** という人もいるかも知れません。私は[元ガジェットブロガー](http://fast5-blog.com/)なので、Wordpressも使ったことがあります。しかし、高度なアニメーションを適用する、自分なりのサイトにする、となったとき、Wordpressだとデザインに成約が出やすいのと、単純にWordpressのPHPが異様（多分Laravel系で、私が知らない分野）で、いじるのが怖かったから、というのがあります。

しかし、自分で太字、タイトルなどを含むテキストエディタを作るのは無いな、と判断し、Notionで作らせる、という判断となりました。Notionはマークダウンにフル対応しているため、ブログでよくある表現がしやすいです。

また、当時はNext.jsで私の実現したいことを実現している人がいた、というのもあり、じゃあ俺はPHPで作ろう、というノリで作ったのもあります。

https://zenn.dev/aono/articles/3a1c78ec9c6d1c

## Notion API取得の準備

一応簡単にNotion APIでのデータ取得方法を解説します。基本は一般的な認証トークン形式と一緒です。

https://www.notion.so/my-integrations

まず、上記リンクにアクセスし、新しいインテグレーションを作成してみましょう。このインテグレーションを作成することで、Notion APIを利用できるようになります。

![スクリーンショット 2023-12-01 10.41.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/910f4cd1-2e2e-ffb1-b7b5-41ca368c8fcd.png)

基本情報を設定し、適当に名前をつけてみましょう。画像はオプションです。
右下の黒いボタン（CSSが崩れています）が、`送信`です。
![スクリーンショット 2023-12-01 10.41.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/ecf77ed5-cfb3-48fc-8231-370d1bde7ec5.png)

作成が完了すると、シークレット、認証トークンが出現します。

![スクリーンショット 2023-12-01 10.41.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/231cc170-a16c-7fc4-27b8-8b00e940925a.png)

### Notion側の設定

次にNotion側の設定です。取得したいページに対して、`コネクトの追加`を探し、先ほど作成したインテグレーションを追加します。


![スクリーンショット 2023-12-01 12.37.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/aefa69eb-7f7e-2433-4d61-7666dc467194.png)

実際にNotion APIでデータを取得する際は、URLに記載されている`notion.so/`以降の部分がページIDとなります。また、データーベースのページを開いている場合、`notion.so/xxxxxxxxx?yyyyyyy`というURLになると思いますが、その際は`xxxxxxxxx`をページIDとして捉えてください。

![無題296_20231201124418.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/a0a2fb7e-5a05-ccad-9123-fd3b2da56e14.png)


## Notion APIでデータを取得する

今回は「取得」のみの解説なので、更新や挿入を実装したい方は[公式ドキュメント](https://developers.notion.com/)を参照することをおすすめします。

実際にNotion APIを取得する際は、ヘッダに以下を追記します。

```
{
  "Authorization: Bearer {自身のトークン}",
  "Notion-Version: 2021-08-16"
}
```
フォーマットは環境に合わせて変更してください。`Notion-Version`も追記で必要です。このプロパティの詳細は下記リンクを見ると記載されていますが、若干仕様が異なるらしいです。

https://developers.notion.com/reference/changes-by-version

そして、通常のページとデータベース型のページでは取得方法が少し異なります。

### 通常のページの場合
以下のURLに先程のヘッダを追記することで、データを取得できます。
`https://api.notion.com/v1/blocks/{ページID}/children/`
bash形式にするとこのような表記となります。
```
curl https://api.notion.com/v1/blocks/{ページID}/children \
  -H 'Authorization: Bearer "{自身のトークン}"' \
  -H "Notion-Version: 2022-06-28"
```
<details>
<summary>
例
</summary>

```json
{
  "object": "list",
  "results": [
    {
      "object": "block",
      "id": "xxxxxxxxxxxxxxxxxxx",
      "parent": {
        "type": "page_id",
        "page_id": "yyyyyyyyyyyyyyyyyyy"
      },
      "created_time": "2023-09-28T00:31:00.000Z",
      "last_edited_time": "2023-09-29T20:09:00.000Z",
      "created_by": {
        "object": "user",
        "id": "xxxxxxxxxxxxxxxxxxx"
      },
      "last_edited_by": {
        "object": "user",
        "id": "xxxxxxxxxxxxxxxxxxx"
      },
      "has_children": false,
      "archived": false,
      "type": "heading_1",
      "heading_1": {
        "is_toggleable": false,
        "color": "default",
        "text": [
          {
            "type": "text",
            "text": {
              "content": "あああ",
              "link": null
            },
            "annotations": {
              "bold": false,
              "italic": false,
              "strikethrough": false,
              "underline": false,
              "code": false,
              "color": "default"
            },
            "plain_text": "あああ",
            "href": null
          }
        ]
      }
    },
    {
      "object": "block",
      "id": "xxxxxxxxxxxxxxxxxxx",
      "parent": {
        "type": "page_id",
        "page_id": "yyyyyyyyyyyyyyyyyyy"
      },
      "created_time": "2023-09-28T00:31:00.000Z",
      "last_edited_time": "2023-09-28T00:32:00.000Z",
      "created_by": {
        "object": "user",
        "id": "xxxxxxxxxxxxxxxxxxx"
      },
      "last_edited_by": {
        "object": "user",
        "id": "xxxxxxxxxxxxxxxxxxx"
      },
      "has_children": false,
      "archived": false,
      "type": "heading_2",
      "heading_2": {
        "is_toggleable": false,
        "color": "default",
        "text": []
      }
    }
  ],
  "next_cursor": null,
  "has_more": false,
  "developer_survey": "https://notionup.typeform.com/to/bllBsoI4?utm_source=postman",
  "request_id": "zzzzzzzzzzzzz"
}

```
</details>

例を見てもらえると分かりますが、相当長いですね。
まず、`object: list`で、次の`results`に示すものが配列であることを示しています。
`results`配列の中に要素が複数入っており、その中の一つを見てみると、

```json
    {
      "object": "block",
      "id": "xxxxxxxxxxxxxxxxxxx",
      "parent": {
        "type": "page_id",
        "page_id": "yyyyyyyyyyyyyyyyyyy"
      },
      "created_time": "2023-09-28T00:31:00.000Z",
      "last_edited_time": "2023-09-29T20:09:00.000Z",
      "created_by": {
        "object": "user",
        "id": "xxxxxxxxxxxxxxxxxxx"
      },
      "last_edited_by": {
        "object": "user",
        "id": "xxxxxxxxxxxxxxxxxxx"
      },
      "has_children": false,
      "archived": false,
      "type": "heading_1",
      "heading_1": {
        "is_toggleable": false,
        "color": "default",
        "text": [
          {
            "type": "text",
            "text": {
              "content": "あああ",
              "link": null
            },
            "annotations": {
              "bold": false,
              "italic": false,
              "strikethrough": false,
              "underline": false,
              "code": false,
              "color": "default"
            },
            "plain_text": "あああ",
            "href": null
          }
        ]
      }
    }
```
これはたった1行の要素ですが、これだけのプロパティを持っています。
例えば、テキストだけを取得したい場合は`results[0].[results[0].type].text[0].plain_text`で取得ができます。
今回の場合、`results[0].type`は`heading_1`を示しますが、他にもたくさん種類があるため、上記のような表現となります。よく見るtypeは下記です。

- paragraph（段落）
- heading_1（見出し１）
- heading_2（見出し２）
- heading_3（見出し３）
- to_do（チェックボックス）
- bulleted_list_item（リスト）
- numbered_list_item（番号リスト）
- toggle（トグル）
- code（コード）
- child_page（子ページ）

![スクリーンショット 2023-12-01 13.17.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/d15f8f5d-0b8b-58c0-3d43-16cd622c0980.png)

また、`results[0].[results[0].type].text`も配列となっており、一部太字にしたり、リンクを挿入したりすると、テキストの中に分割されてデータが入るようになります。それを考慮してNotion APIを活用する必要があります。

### データベースのページの場合

続いて、データベースのページでもやってみます。

`https://api.notion.com/v1/databases/{ページID}/query`

これもbash形式にするとこうなります。注意点としては、データベースを取得する場合、`POST`メソッドになる点です。POSTということはbodyを含めることができ、そこでデータの検索を行うことができます。今回は解説しません。

```
curl -X POST https://api.notion.com/v1/databases/{ページID}/query \
  -H 'Authorization: Bearer "{自身のトークン}"' \
  -H "Notion-Version: 2022-06-28"
```

```json
{
  "object": "list",
  "results": [
    {
      "object": "page",
      "id": "",
      "created_time": "2023-11-22T00:27:00.000Z",
      "last_edited_time": "2023-11-22T00:37:00.000Z",
      "created_by": {
        "object": "user",
        "id": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
      },
      "last_edited_by": {
        "object": "user",
        "id": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
      },
      "cover": null,
      "icon": null,
      "parent": {
        "type": "database_id",
        "database_id": "aaaaaaaaaaaaa"
      },
      "archived": false,
      "properties": {
        "チーム領域": {
          "id": "aaaa",
          "type": "rich_text",
          "rich_text": [
            {
              "type": "text",
              "text": {
                "content": "4人(全体コーディング,レイアウト2,リーダー1,書紀1)",
                "link": null
              },
              "annotations": {
                "bold": false,
                "italic": false,
                "strikethrough": false,
                "underline": false,
                "code": false,
                "color": "default"
              },
              "plain_text": "4人(全体コーディング,レイアウト2,リーダー1,書紀1)",
              "href": null
            }
          ]
        },
        "利用言語": {
          "id": "aaaa",
          "type": "multi_select",
          "multi_select": [
            {
              "id": "yyyyyyyyyyyyyyyyyyyy",
              "name": "TypeScript",
              "color": "red"
            },
            {
              "id": "yyyyyyyyyyyyyyyyyyyy",
              "name": "CSS(SCSS)",
              "color": "gray"
            }
          ]
        },
        "日付": {
          "id": "CPc%5C",
          "type": "date",
          "date": {
            "start": "2023-09-01",
            "end": null,
            "time_zone": null
          }
        }
      }
    }
  ]
}

```
ページIDと同様に、`object: list`に続き、`results`配列の中にデータが複数ある、という形です（全部表示すると余裕で1000行行きます）。

![スクリーンショット 2023-12-01 21.18.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/92a39e3b-524e-b526-7abf-e3c13a872ce4.png)

Notion側を見ると、チーム領域と利用言語、というプロパティが存在します。こちらをJSONの中で見つけてみましょう。

```typescript
const properties = results[0].properties; // 一度プロパティのみ変数に入れる

// チーム領域
const typeTeam = properties["チーム開発"].type; // 'rich_text'が格納される
const team = properties["チーム領域"].[typeTeam][0].plain_text;
// '4人(全体コーディング,レイアウト2,リーダー1,書紀1)'

// 利用言語
const typeLangs = properties["利用言語"].type; // 'multi_select'が格納される
const langs = properties["利用言語"][typeLangs].map((v)=> v.name);
// ['TypeScript', 'CSS(SCSS)']
```

データベースの場合、列の中のデータは`properties`に格納されます。その中で更にデータベース列名である「チーム領域」と「利用言語」をそのままキーとしてJSONが持っており、さらにその型`type`があり、type名でさらに配列データが有り、、、。

**これがNotion APIです  :->**

## API経由ではマークダウン出力ができない

実はNotion単体であれば、**マークダウン出力**ができ、marked.jsを噛ませば一瞬でHTMLを生成できるツールがあります。
しかし、今回ブログを更新するのは一般の学生。私だけだったら全然それでいいですが、今後引き継ぎを行うとなったとき、毎回の`.md`ファイル作成がブログを書く障壁にならないようにしたいという思いがありました。

### Notion APIデータをマークダウンに変換するライブラリも存在

https://fand.jp/notion/the-library-converts-the-concept-body-to-markdown-format/

これは開発中に気づいたことですが、既に頭のいい人がマークダウンに変換するライブラリを作っていたらしいです。
しかし、今回はバニラPHPのためnode関連のサポートが受けられない！残念！（確かmarked.jsも試したがnode系じゃないと無理）。

そこで、PHPで無理やりデータを引っ張りHTMLを生成する、ということを当時はやっていました。

## Notionデータをブログデータとして表示する手順

![起業WebTask-02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/962633bb-c494-5019-ee6d-2b69514b69a8.jpeg)

さて、いざNotion APIを使用してブログを表示しようとなったとき、直接表示はしていないです。なぜなら、Notion APIは2〜3秒ほど遅延が発生するためです。

そこで、ブログを表示する前に予めデータをサーバ側のデータベースに格納し、普通のブログサイトと同じ高速なブラウジングを可能としました。

先程のparagraphやheadingといったプロパティから、HTML形式でマークアップテキストを生成し、それを行ごとにデータベースに入れて表示している、という形となります。

### 横並びや子リストは小ページ扱いとなる

![スクリーンショット 2023-12-02 7.50.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/fe439bbb-f5c6-3d9a-2247-7d2353a61c71.png)

Notionでは横並びをする機能やリストのネスト構造などが存在します。

しかし、この横並びの要素をNotion APIで取得しようと思うと、以下のようになります。

```json
{
  "object": "block",
  "id": "xxxxxxxxxxxxx",
  "parent": {
    "type": "page_id",
    "page_id": "aaaaaaaaaaaaa"
  },
  ... 省略 ...
  "has_children": true,
  "archived": false,
  "type": "column_list",
  "column_list": {}
}
```

`column_list`という`type`で、画像もテキストも表示されていません。この場合どうするかというと、`id`プロパティでさらにページを取得することで本来の要素を取得できます。

```json
{
  "object": "list",
  "results": [
    {
      "object": "block",
      "id": "yyyyyyyy",
      "parent": {
        "type": "block_id",
        "block_id": "xxxxxxxxxx"
      },
      "has_children": true,
      "archived": false,
      "type": "column",
      "column": {}
    },
    {
      "object": "block",
      "id": "zzzzzzzz",
      "parent": {
        "type": "block_id",
        "block_id": "xxxxxxxxxx"
      },
      "has_children": true,
      "archived": false,
      "type": "column",
      "column": {}
    }
  ]
}
```
しかし、まだ表示されません。今度は`column`という`type`でブロックが表示されています。なかなか答えに辿り着きませんが、ここで表示されている`id`を先程と同様に取得すると、ようやく2つのブロックのデータを取得することができます。

![起業WebTask-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/00c78104-5a1a-6b42-857f-3a71d3dba6dc.jpeg)

`column_list`の中に`column`、`column`の中に`list`がある、ということですね。こりゃすごい…

### データベースの中にページがある

![スクリーンショット 2023-12-02 8.13.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/df74366f-7d54-2326-a8e5-8f3eb3d3d80f.png)

データベースも同様にページという概念が存在します。実際、画像のようにサイドピーク（と言うらしい）で開き、そこからNotionの通常ページと同じようにメモをすることができます。

手順としては、

1. Notion APIでクエリ一覧を取得
2. 該当する行のページをNotion APIで取得
    a. 子要素が存在するならさらに取得
3. レンタルサーバのデータベースにそのままデータをコピー

こんな流れとなります。あまり賢くないコードなのでコードは見せませんが、これで何となくNotion APIのデータ取得方法は分かったと思います。

### 画像はサーバに保存して表示

画像も同様にデータを取得できます。

```json
{
  "type": "image",
  "image": {
    "caption": [],
    "type": "file",
    "file": {
      "url": "クソ長い画像データのパス",
      "expiry_time": "2023-12-02T00:19:59.370Z"
    }
  }
}
```

URLからPHPで画像データを取得し、データを軽量化した後に`<img src="新しいURL">`として生成する、という流れになっています。

ちなみに、Notionの画像URLは一定時間経過すると、別のURLに変更されます。そんなことをしてデータを取得しています。

## 実際の操作手順

では、実際にデータを取得する、となったときに、どのように操作をするのか解説します。

まず、Notionで記事を作成します。メタ設定もNotionのページ上で完結するようになっています。ブログが完成したら、管理者画面CMSにログインし、ブログ管理に移ります。

![スクリーンショット 2023-12-01 16.02.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538408/6ea003a5-1410-03a5-534a-ca7ee862f5f3.png)

こちらが序盤に登場したブログ管理画面です。このブログ管理画面を開く際にNotion APIのデータベースクエリを検索（いわゆるSSRで表示）し、Notionに登録されている全てのデータとサーバのデータベースに登録されている記事を表示します。

ここで、

- Notionのみにデータが有る場合は「🔵非同期」
- サーバのデータベースに登録されている場合
    - 「最新を取得」を押すと「🟠下書き」
    - エラーの場合は「🔴エラー」
    - 「公開/下書きにする」を押すと「🟢公開」
    - Notionにデータがない場合、「最新を取得」は押せない

という仕組みとなっております。アイキャッチ画像はNotion上で一番最初に上げた画像が表示されます。

そんな仕組みでサークルのWebサイトは動いています。

## 最後に

最後まで見てくださりありがとうございました。Notion APIは最初はかなり癖がありますが、慣れるとブログ作成で革命が起きるくらい便利な機能です。今回は「取得」だけでしたが、更新や挿入ももちろんできるため、可能性が本当に無限大だと感じています。
バニラのPHPで作成したコンテンツですが、今見るとかなり酷いコードになっているので、Laravelの導入やNext.jsの置き換えをしたいと思っている今日この頃です。

あと現在はブログを書く人が誰もいない（そもそもサークル自体がオワコン化してる）現状もあり、このまま維持することは困難だと考えています。就活をしていて「ミッション・ビジョン・バリュー」が当たり前のように設定されているのを見て、Webサイトのあり方、そしてサークルのあり方をもう少し深く考えるべきだと感じているところです。