【移行ガイド】microCMSからNotion APIへブログCMSを完全移行する

この記事でわかること
- コンポーネントを触らずに、API クライアントだけを差し替える移行戦略
- Slug プロパティに旧 ID を持たせて、既存 URL を一つも変えずに移行する方法
- Notion SDK v5 で databases が dataSources に変わった件
- ビルド時にレート制限でタイムアウトしたときの逃げ方
Next.js 13(App Router)、Notion API トークン、移行元のヘッドレス CMS が稼働中であること。
概要: microCMSで運用していた技術ブログをNotionに完全移行した記録です。20記事のデータ移行、Notion APIクライアントの実装、ISR対応、URL維持まで、移行の全工程をまとめました。
はじめに
microCMS で運営していた技術ブログ(Next.js 13 App Router)を Notion に完全移行しました。理由は、記事を書く場所を Notion に統一したかったことと、開発セッションを自動記事化する仕組みと連携させたかったことです。ただし既存の URL(/blogs/{microCMS-ID})は維持する必要がありました。
1. 移行計画
全体のアプローチ
既存のmicroCMSのインターフェース(getList, getDetail, getCategoryList等)を維持したまま、バックエンドだけNotionに差し替える戦略を採用。
microCMS API ──→ libs/microcms.ts ──→ コンポーネント
↓ 置換
Notion API ──→ libs/notion.ts ──→ コンポーネント(変更なし)Notion DBの設計
Tech Articles Databaseに以下のプロパティを追加:
| プロパティ | 型 | 用途 |
|---|---|---|
| Slug | rich_text | microCMS IDを保持(URL維持) |
| Eyecatch | url | アイキャッチ画像URL |
| Source | select | microCMS / Notion(データ元の識別) |
既存プロパティ: Title, Category(select), Tags(multi_select), Status(select), Created(date)
2. データ移行
microCMSからのエクスポート
microCMS JS SDKで全記事を取得し、JSONファイルにエクスポート。
const [blogs, categories, tags] = await Promise.all([
client.getList({ endpoint: 'blogs', queries: { limit: 100 } }),
client.getList({ endpoint: 'categories', queries: { limit: 100 } }),
client.getList({ endpoint: 'tags', queries: { limit: 100 } }),
]);Notionへのインポート
Notion MCP(Model Context Protocol)経由でページを一括作成。各記事のプロパティとMarkdown本文をそのまま移行。
重要ポイント:
SlugにmicroCMSのIDを設定 → URLが変わらないSource: "microCMS"で移行元を識別- カバー画像にeyecatch画像を設定
- タグのマッピング(例: microCMSの"Zustand" → Notionの"zustand")
3. Notion APIクライアント実装
SDK v5の変更点
@notionhq/client v5ではdatabases.queryが廃止され、dataSources.queryに変更。
// v4以前
const response = await notion.databases.query({ database_id: DB_ID });
// v5
const response = await notion.dataSources.query({ data_source_id: DS_ID });型定義の互換性維持
microCMSの型(Blog, Tag, Category)をそのまま維持し、Notionのページデータから変換。
export type Blog = {
id: string; // Slug(URLパス用)
title: string;
body: string; // Blocks → Markdown変換
eyecatch?: { url: string };
category?: Category;
tags?: Tag[];
createdAt: string;
// ...MicroCMSDate互換フィールド
};Blocks → Markdown変換
Notion APIのブロックデータをMarkdownに変換するblocksToMarkdown関数を実装。heading, paragraph, code, image, list, table, bookmark, callout等に対応。
4. ISRでのレート制限対策
問題
全48記事の静的生成時にNotion APIのレート制限(3リクエスト/秒)に引っかかり、ビルドがタイムアウト。
解決策
- generateStaticParamsを空配列に — ビルド時にページを生成しない
- revalidate = 3600 — ISRで初回アクセス時に生成、1時間キャッシュ
- リクエスト内のメモ化 — 同じリクエストの中で同じ API を何度も叩かないようにする
cache() を使ってリクエスト単位に閉じること。モジュールスコープの変数に貯めると、Workers や Lambda ではインスタンスが使い回されて古い一覧が返り続けます(新記事を公開しても出てこない)。筆者はこれで一度踏んでいます。export const revalidate = 3600;
export const dynamicParams = true;
export async function generateStaticParams() {
return []; // ビルド時には生成しない
}5. URL維持の仕組み
Slugプロパティの活用
getDetailではまずSlugプロパティで検索し、見つからなければNotion Page IDで検索するフォールバック。
export const getDetail = async (slug: string) => {
const response = await notion.dataSources.query({
data_source_id: DATABASE_ID,
filter: { property: 'Slug', rich_text: { equals: slug } },
});
if (response.results.length === 0) {
const page = await notion.pages.retrieve({ page_id: slug });
return pageToBlog(page, true);
}
return pageToBlog(response.results[0], true);
};Tips
databases のメソッドが dataSources に移動。パラメータ名も database_id → data_source_id に変更されているので注意encodeURIComponent で Slug を生成すると、日本語カテゴリ名も URL セーフに扱える.next キャッシュを削除しないと dev サーバーが古いデータを返すことがある。rm -rf .next で解決参考リンク
- Notion API 公式ドキュメント
- @notionhq/client(npm)
- Next.js: ISR
- Notion APIでブログシステムを構築する(別記事)
- Cloudflare Pages → Workers 移行で遭遇したEdge Runtime問題集(別記事)
まとめ
- microCMSからNotionへの移行は、APIクライアントの差し替えが中心で、コンポーネント側の変更は最小限
- Notion SDK v5では
dataSources.queryを使う(databases.queryは廃止) - SlugプロパティでmicroCMSのIDを保持することで、既存URLを完全に維持
- ISR とリクエスト内のメモ化でNotion API のレート制限を回避(モジュールスコープには貯めない)
- 移行後はNotionで記事管理が一元化され、運用が大幅にシンプルに
更新履歴
- 「リスト取得にメモリキャッシュ」の記述を、リクエスト内のメモ化に訂正し、モジュールスコープに貯めると isolate の使い回しで古い一覧が返る事故を警告として追加。壊れていた目次マーカーを自動目次に差し替え。コードブロックの言語指定を javascript から text に修正。引用形式の Tips を callout に統一。参考リンクの URL 重複表記をタイトル付きに整理し、関連記事を追加。箸条書きだった「はじめに」を文章に整理。「この記事でわかること」「対象読者」「前提条件」を概要プロパティへ移動。

