📌概要: Notion SDK v5とNext.js 13 App Routerを使って、Notionをバックエンドにしたブログシステムを構築する方法をまとめました。dataSources API、Blocks→Markdown変換、ISR設定など実装の詳細を解説します。
はじめに
NotionをヘッドレスCMSとして使い、Next.jsでブログを構築する手法が注目されている。公式SDKのv5では APIが大きく変更されたため、最新の実装パターンを解説する。
この記事でわかること
-
Notion SDK v5のセットアップと基本的な使い方
-
dataSources APIでのデータベースクエリ
-
NotionブロックからMarkdownへの変換実装
-
ISR(Incremental Static Regeneration)の設定
-
カテゴリ・タグ・アーカイブ機能の実装
対象読者
-
Notion APIを使ったWebアプリ開発に興味がある方
-
Next.js App Routerの実践的な使い方を学びたい方
-
ヘッドレスCMSの選択肢としてNotionを検討している方
前提条件
-
Node.js 18以上
-
Next.js 13(App Router)
-
@notionhq/client v5
-
Notion APIインテグレーション作成済み
1. Notion SDK v5のセットアップ
インストール
npm install @notionhq/client
クライアント初期化
import { Client } from '@notionhq/client';
const notion = new Client({ auth: process.env.NOTION_API_KEY });
v5での主要な変更点
| v4以前 |
v5 |
| notion.databases.query() |
notion.dataSources.query() |
| database_id パラメータ |
data_source_id パラメータ |
| notion.databases.retrieve() |
notion.dataSources.retrieve() |
2. データベース設計
推奨プロパティ構成
| プロパティ |
型 |
説明 |
| Title |
title |
記事タイトル |
| Slug |
rich_text |
URLパス用のスラッグ |
| Category |
select |
カテゴリ分類 |
| Tags |
multi_select |
技術タグ |
| Status |
select |
Draft / Published / Archived |
| Created |
date |
公開日 |
| Eyecatch |
url |
アイキャッチ画像URL |
3. 記事一覧の取得
export const getList = async () => {
const allPages = [];
let cursor;
do {
const response = await notion.dataSources.query({
data_source_id: DATABASE_ID,
start_cursor: cursor,
page_size: 100,
filter: {
property: 'Status',
select: { equals: 'Published' },
},
sorts: [{
property: 'Created',
direction: 'descending',
}],
});
allPages.push(...response.results);
cursor = response.has_more ? response.next_cursor : undefined;
} while (cursor);
const contents = await Promise.all(
allPages.map(page => pageToBlog(page, false))
);
return { contents, totalCount: contents.length };
};
4. Blocks → Markdown変換
Notionの記事本文はBlockオブジェクトの配列として取得される。これをMarkdownに変換する。
async function blocksToMarkdown(blockId: string): Promise<string> {
const blocks = [];
let cursor;
do {
const response = await notion.blocks.children.list({
block_id: blockId,
start_cursor: cursor,
page_size: 100,
});
blocks.push(...response.results);
cursor = response.has_more ? response.next_cursor : undefined;
} while (cursor);
const lines = [];
for (const block of blocks) {
switch (block.type) {
case 'heading_1':
lines.push('# ' + richTextToPlain(block.heading_1.rich_text));
break;
case 'heading_2':
lines.push('## ' + richTextToPlain(block.heading_2.rich_text));
break;
case 'paragraph':
lines.push(richTextToMarkdown(block.paragraph.rich_text));
break;
case 'code':
lines.push('```' + block.code.language + '\n' + richTextToPlain(block.code.rich_text) + '\n```');
break;
case 'image':
const url = block.image.type === 'external'
? block.image.external.url
: block.image.file.url;
lines.push('');
break;
}
lines.push('');
}
return lines.join('\n');
}
インライン装飾の変換
function richTextToMarkdown(richText) {
return richText.map(t => {
let text = t.plain_text;
if (t.annotations?.bold) text = '**' + text + '**';
if (t.annotations?.italic) text = '*' + text + '*';
if (t.annotations?.code) text = '`' + text + '`';
if (t.href) text = '[' + text + '](' + t.href + ')';
return text;
}).join('');
}
5. ISR設定
Notion APIにはレート制限(3リクエスト/秒)があるため、全ページをビルド時に静的生成するとタイムアウトする。ISRで解決。
export const revalidate = 3600;
export const dynamicParams = true;
export async function generateStaticParams() {
return [];
}
メモリキャッシュの追加
let listCache = null;
export const getList = async () => {
if (listCache) return listCache;
listCache = result;
return result;
};
6. カテゴリ・タグ一覧の取得
Notionのスキーマから直接selectオプションを取得できる。
export const getCategoryList = async () => {
const db = await notion.dataSources.retrieve({
data_source_id: DATABASE_ID
});
const options = db.properties.Category?.select?.options || [];
return {
contents: options.map(opt => ({
id: encodeURIComponent(opt.name.toLowerCase()),
name: opt.name
}))
};
};
Tips
📌Tip 1: Notionの内部画像URL(fileタイプ)は1時間で期限切れになる。外部URLの画像を使うか、R2等にアップロードして永続URLにすること
📌Tip 2: dataSources.retrieveでスキーマを取得すれば、カテゴリやタグの選択肢をハードコードする必要がない
📌Tip 3: ビルド時のAPI呼び出し回数を最小化するために、getListの結果をモジュールレベルでキャッシュすると効果的
参考リンク
まとめ
-
Notion SDK v5ではdataSources APIを使う
-
Blocks→Markdown変換で記事本文を取得
-
ISR + メモリキャッシュでレート制限を回避
-
スキーマからカテゴリ・タグを動的取得
-
Notionは無料でヘッドレスCMSとして十分実用的