Notion API を使って Notion を CMS 化する

11 min read

はじめに

僕が Notion API に期待することのメインは、

  • CMS バックエンド
  • データベース (特にカレンダー) 操作の自動化

の2点です。

前回の記事で、なんとなく API の使いかたの基本はわかりました。

https://zenn.dev/5t111111/articles/785fec8d21d82b

この記事では、一歩進んで、上記したやりたいことの1つである 「CMS バックエンド」 の可能性を探っていきたいと思います。

ちなみに先に言っておきますが、現時点の Notion API で CMS 化にはかなり難があります。

資料

https://developers.notion.com/docs/working-with-page-content

https://github.com/makenotion/notion-sdk-js

やること

  • Notion でブログポストのようなページをいくつか作る
  • そのページのリストや内容を API で取得する
  • Next.js を使ってなんだか形にする

Notion 上のページ構成

事前に Notion 上に、実際のサイト上のページのソースとなるページ構成を作っておきます。

ブログサイト全体を構成するページ (親ページ) を作り、そのページに含まれるデータベースに、子ページとして個別投稿のページを作る構成とします。

  • ブログサイト用ページ (親ページ)
    • 投稿一覧データベース
      • ブログ投稿用ページ1 (子ページ1)
      • ブログ投稿用ページ2 (子ページ2)

もちろん、親ページとなる「ブログサイト用ページ」は、API でアクセスできるように integration にシェアしておく必要があります。

ブログサイト用ページ (親ページ)

親ページには以下の情報を持つこととします。

  • サイトタイトル
  • サイト description
  • 個別投稿のリスト

ブログ記事用ページ (子ページ)

個別投稿の子ページには以下の情報を持つこととします。

  • 投稿タイトル
  • 投稿日時
  • 投稿の内容

Next.js でフロントエンドサイトの土台を作る

API を実行して Notion 記事を取得し、そして表示するためのフロントエンドサイトの土台を、Next.js で作ります。

npx create-next-app --example with-typescript korn-blog

Notion SDK もインストールしておきます。

npm install @notionhq/client

ページ API の仕様を確認する

これで、サイトのフロントエンドの土台と、SDK の準備ができたので、まずは一旦、単純に SDK からリクエストを送って、親ページのデータを取得してみます (ID はダミーです) 。

const notion = new Client({
  auth: process.env.NOTION_TOKEN,
});
const parentPageId = 'd283163cf4d04b219493xxxxxxxxxxxx';
const parentPage = await notion.pages.retrieve({
  page_id: parentPageId,
});
console.dir(parentPage, { depth: null });

こんなデータが取れました。

{
  object: 'page',
  id: 'd283163c-f4d0-4b21-9493-xxxxxxxxxxxx',
  created_time: '2021-05-14T06:50:00.000Z',
  last_edited_time: '2021-05-14T07:56:00.000Z',
  parent: { type: 'workspace', workspace: true },
  archived: false,
  properties: {
    title: {
      id: 'title',
      type: 'title',
      title: [
        {
          type: 'text',
          text: { content: 'とうもろこしの技術ブログ', link: null },
          annotations: {
            bold: false,
            italic: false,
            strikethrough: false,
            underline: false,
            code: false,
            color: 'default'
          },
          plain_text: 'とうもろこしの技術ブログ',
          href: null
        }
      ]
    }
  }
}

んん〜?なんか期待とちがくねーかこれは…

サイトのタイトルとなる「title」プロパティは取れていますが、それ以外に欲しかった本文や子ページの入ったデータベースは取得できていません。

困りました。どうすればよいのでしょうか。

Page content と page property

さきほどの問題を理解し解決するには、まず、Page content と page property の理解が必要なようです。

https://developers.notion.com/docs/working-with-page-content#page-content-versus-properties
を見ると、
  • Page property は日程やカテゴリーなど 構造化されたデータ を利用するときに都合が良い
  • Page content は より緩い自由な構造のデータ を利用するときに都合が良い

ということです。この分類自体は、普通に Notion を使っていれば、たぶん理解して普段も活用している使い分けだと思います。

しかし、今回やりたいこととして重要なのは、Page content はそれを構成するブロックの集合体であり、そのデータを取得するためにはまた別のエンドポイントを使わないといけない、ということです。

具体的には Retrieve block children という API を利用する必要があるようです。

https://developers.notion.com/reference/get-block-children

この API はカーソルベースのページネーションをサポートする API で、親となるブロックの ID を指定します。紛らわしいのですが、ページというものもブロックの一種なので、ブロック ID としてページの ID を指定すると、ブロックの子要素であるブロックのリスト、つまりは page content の内容が取得できるはずです。

ブロックの子であるブロックのリストを取得する

やってみましょう。JS SDK のどの API が「Retrieve block children」に対応しているのかパッと見わからないので、SDK の実装を追って探すと、どうやら notion.blocks.children.list でいけそうです。

const blocks = await notion.blocks.children.list({ block_id: parentPageId });
console.dir(blocks, { depth: null });

で、取れたデータは…

{
  object: 'list',
  results: [
    {
      object: 'block',
      id: 'ba052fc6-d0b9-447e-ba1b-137218f6a5fa',
      created_time: '2021-05-14T07:01:00.000Z',
      last_edited_time: '2021-05-14T07:56:00.000Z',
      has_children: false,
      type: 'unsupported',
      unsupported: {}
    },
    {
      object: 'block',
      id: 'd9b4a8f2-9379-4cb3-962e-ce784a584f47',
      created_time: '2021-05-14T06:55:00.000Z',
      last_edited_time: '2021-05-14T07:56:00.000Z',
      has_children: false,
      type: 'paragraph',
      paragraph: { text: [] }
    },
    {
      object: 'block',
      id: '932de24c-a972-4886-b160-2d0f6a1fc199',
      created_time: '2021-05-14T07:53:47.038Z',
      last_edited_time: '2021-05-14T08:25:00.000Z',
      has_children: false,
      type: 'unsupported',
      unsupported: {}
    }
  ],
  next_cursor: null,
  has_more: false
}

んんん〜〜w

やっぱり期待したものと違う!

何が違うかというと、欲しいデータ、つまり page content に設定されているはずの callout ブロックとして書かれた description と「投稿リスト」のデータベースが取れていませんね。

取れていない、というか、 unsupported となっちゃっているようです。

現時点の SDK のコード上のブロックを表現する型情報を見るとこんな union になっているので、ここにリストされていないブロックはサポートされていない、ということでしょうか。

https://github.com/makenotion/notion-sdk-js/blob/ccb2180648bdba19714c40897a6bfda1c506a71a/src/api-types.ts#L38-L48

これが JS SDK の制限なのか Notion そのものの制限なのか確認するために curl でも実行してみましたが、同じように unsupported なブロックになっていました。はい。

…どうやら今構想している構造では無理っぽいです。

データベースのブロックから直接投稿リストを取得する

残念ながら、親ページのページ ID を元に投稿リストを取得するということに失敗してしまったので、諦めて、「投稿リスト」ブロックのリストを直接取得してみましょう。

「投稿リスト」データベースの ID を Notion ページ上で取得し、それからデータを取得します。これはデータベースの query API で実現できるはずです。

特にフィルタは指定せずに PublishedAt の降順で取得してみます。

const postsDatabaseId = '932de24ca9724886b1602d0f6a1fc199';
  const posts = await notion.databases.query({
    database_id: postsDatabaseId,
    sorts: [
      {
        property: 'PublishedAt',
        direction: 'descending',
      },
    ],
  });
  console.dir(posts, { depth: null });

うん、これは期待通りとれましたね。

{
  object: 'list',
  results: [
    {
      object: 'page',
      id: 'b5ee3e28-1c46-4c4f-a09e-xxxxxxxxxxxx',
      created_time: '2021-05-14T07:53:47.038Z',
      last_edited_time: '2021-05-14T07:56:00.000Z',
      parent: {
        type: 'database_id',
        database_id: '932de24c-a972-4886-b160-xxxxxxxxxxxx'
      },
      archived: false,
      properties: {
        PublishedAt: {
          id: 'u[rY',
          type: 'date',
          date: { start: '2021-05-14T11:00:00.000+09:00', end: null }
        },
        Name: {
          id: 'title',
          type: 'title',
          title: [
            {
              type: 'text',
              text: { content: 'オーナーシップって馬の名前ですか', link: null },
              annotations: {
                bold: false,
                italic: false,
                strikethrough: false,
                underline: false,
                code: false,
                color: 'default'
              },
              plain_text: 'オーナーシップって馬の名前ですか',
              href: null
            }
          ]
        }
      }
    },
    {
      object: 'page',
      id: '41d36147-8e34-4f5a-bef3-xxxxxxxxxxxx',
      created_time: '2021-05-14T07:53:47.038Z',
      last_edited_time: '2021-05-14T07:56:00.000Z',
      parent: {
        type: 'database_id',
        database_id: '932de24c-a972-4886-b160-xxxxxxxxxxxx'
      },
      archived: false,
      properties: {
        PublishedAt: {
          id: 'u[rY',
          type: 'date',
          date: { start: '2021-05-13T09:00:00.000+09:00', end: null }
        },
        Name: {
          id: 'title',
          type: 'title',
          title: [
            {
              type: 'text',
              text: { content: 'クレートはスイーツです', link: null },
              annotations: {
                bold: false,
                italic: false,
                strikethrough: false,
                underline: false,
                code: false,
                color: 'default'
              },
              plain_text: 'クレートはスイーツです',
              href: null
            }
          ]
        }
      }
    }
  ],
  next_cursor: null,
  has_more: false
}

投稿ページの ID や property が含まれているので、これを利用すれば、投稿の一覧ページは作れそうです。

投稿ページのデータを取得する

続けて、投稿ページのデータを取得してみます。

Page property と page content をそれぞれ API で取得すればよいはずです。

Page property の取得

まずは page property から。

const postId = 'b5ee3e28-1c46-4c4f-a09e-ea19ce0ad197';
const postProperties = await notion.pages.retrieve({ page_id: postId });
console.dir(postProperties, { depth: null });
{
  object: 'page',
  id: 'b5ee3e28-1c46-4c4f-a09e-xxxxxxxxxxxx',
  created_time: '2021-05-14T07:53:47.038Z',
  last_edited_time: '2021-05-14T07:56:00.000Z',
  parent: {
    type: 'database_id',
    database_id: '932de24c-a972-4886-b160-xxxxxxxxxxxx'
  },
  archived: false,
  properties: {
    PublishedAt: {
      id: 'u[rY',
      type: 'date',
      date: { start: '2021-05-14T11:00:00.000+09:00', end: null }
    },
    Name: {
      id: 'title',
      type: 'title',
      title: [
        {
          type: 'text',
          text: { content: 'オーナーシップって馬の名前ですか', link: null },
          annotations: {
            bold: false,
            italic: false,
            strikethrough: false,
            underline: false,
            code: false,
            color: 'default'
          },
          plain_text: 'オーナーシップって馬の名前ですか',
          href: null
        }
      ]
    }
  }
}

Page property は、さっき投稿リストを取得したときにレスポンスに含まれていたデータと同じなようです。

Page content の取得

ページネーションなどを考慮せず、page content を取得してみます。

const postId = 'b5ee3e28-1c46-4c4f-a09e-ea19ce0ad197';
const postContent = await notion.blocks.children.list({ block_id: postId });
console.dir(postContent, { depth: null });

レスポンスで取得できた内容…は、かなり長いので割愛しますが、heading や paragraph といった基本的なブロックのデータは取れていました。

しかし、サンプルコードを書いていたコードブロックが unsupported になってしまいましたね…技術ブログとしてはいきなり致命傷です。

が、めげずにとりあえず作るところまではやるか…

API で取得した Notion データで、Next.js でサイトを作る

だいぶくじけてきていますが、大急ぎで作りました (デザインは何もやっていません) 。

トップページ

投稿ページ

コード

こちらです。とにかく最低限形になるようにしているので色々ひどいです。

https://github.com/5t111111/korn-blog

Notion API を使った CMS バックエンドの所感

実際に試してみた所感です。

いいところ

  • 気合いがあって制限を受け入れられれば Notion を CMS としてサイトが作れる

いまいちなところ

  • API でサポートされているブロックが少ない
  • JS SDK の型定義間違いなど SDK にまだバグが多い (特にブロック周り)
  • API limit やレスポンスタイムを考えると、SSG 化や CDN は必須
  • integration の権限が can edit だけしか選べないのホント?

使いものになるためには、ブロックのサポートなど API 側の機能の拡充に加えて、Notion のブロックと HTML の相互変換ライブラリなどが充実する必要があると感じた。ちょっと各自が自前でいい感じにレンダリングを管理するのはきつい感じ。

現時点では、Notion を CMS として使いたければ Anotion などの専用のサービスを利用する方が間違いなくよいでしょう。