【入門編】NotionとVercel・Next.jsでヘッドレスCMSを構築する:APIのレートリミット対策とISR(Incremental Static Regeneration)による高速ブログ運用 – プロジェクト・ナレッジ管理活用バイブル

NotionとVercel・Next.jsで、あの「数秒で反映」を実現する!レートリミットも怖くない、究極のブログ運用術

やっほー!君も最新技術にワクワクしてるかい?今日のテーマは、ちょっとマニアックだけど、知ってしまえば「なんだ、こんなに簡単だったのか!」って膝を打つような、とっておきのテクニックだよ。

「NotionをCMSとして使いたいけど、APIの制限が心配…」「ブログの更新、もっと早く反映されないかな?」そんな君のために、今回はNotion、Vercel、そしてNext.jsを組み合わせて、まさに「数秒でブログが更新される」魔法のようなシステムを構築する方法を、ゼロから丁寧に解説していくよ。

しかも、ただ動くだけじゃなく、APIのレートリミットという、多くの開発者が頭を悩ませる壁を乗り越えるための、とっておきの「キャッシュ戦略」も伝授しちゃう。これをマスターすれば、君の毎日の開発作業が、きっと劇的に楽になるはずさ。

さあ、一緒にこの冒険に旅立とう!

1. Notionデータベースをバックエンドに? アーキテクチャ全体像を掴もう!

まず最初に、「え、あのNotionがバックエンドになるの?」って思った君、鋭い! そうなんだ、Notionはその柔軟なデータベース機能と、強力なAPIのおかげで、立派なコンテンツ管理システム(CMS)として機能してくれるんだ。

なぜNotionをCMSとして使うのか? その魅力に迫る!

  • 直感的な操作性: 誰でも知ってるNotionだから、エンジニアじゃないチームメンバーでも記事の執筆や管理がラクラク! 専門知識がなくても、コンテンツ作成に集中できるのは大きなメリットだよね。
  • 豊富なデータベース機能: テーブル、ボード、ギャラリー、カレンダー… Notionのビューを駆使すれば、記事のステータス管理や公開スケジュール管理だって、視覚的で分かりやすく行えるんだ。
  • API連携の容易さ: Notion APIを使えば、外部のアプリケーションからNotionデータベースにアクセスできる。これこそが、今回のブログ構築の鍵になるんだ。
  • コストパフォーマンス: 無料プランでも十分な機能が使えるから、小規模なプロジェクトや個人ブログなら、コストを抑えながら始められるのも嬉しいポイントだ。

全体像:Notion → Vercel (Next.js) → Webサイト!

君がこれから作り上げるシステムは、こんなイメージだよ。

graph TD
A[Notionデータベース] –> B(Notion API)
B –> C{Vercel (Next.js)};
C –> D[Webサイト];
C — キャッシュ –> E[Vercel Edge Cache];

1. Notionデータベース: ここに君のブログ記事のデータ(タイトル、本文、公開日、サムネイル画像など)が保存される。
2. Notion API: Vercel上のNext.jsアプリケーションが、このAPIを通じてNotionデータベースから記事データを取得する。
3. Vercel (Next.js):

  • Notion APIから取得したデータを元に、Webサイトのページを生成する。
  • 【重要】 Notion APIのレートリミットを考慮し、取得したデータをキャッシュする仕組みを実装する。
  • 【さらに重要】 Incremental Static Regeneration (ISR) というNext.jsの強力な機能を使って、Notion側が更新されたら、数秒〜数分でWebサイト側にも自動的に反映させる。

4. Webサイト: 生成されたWebサイトが、インターネット上に公開される。

このアーキテクチャの肝は、「Notion APIへのアクセスを最小限にしつつ、最新の情報を素早くユーザーに届ける」こと。これを実現するのが、次の「キャッシュ戦略」と「ISR」なんだ。

2. Notion APIの「3リクエスト/秒」の壁を越えろ! 賢いキャッシュ戦略

さて、ここで大きな問題にぶつかる。Notion APIには「3リクエスト/秒」というレートリミットがあるんだ。つまり、1秒間に3回以上APIを叩くと、一時的にブロックされてしまう可能性がある。ブログサイトで一度にたくさんの記事を表示したり、頻繁に更新したりすると、あっという間にこの制限に引っかかってしまうんだ。

でも、心配無用! この壁を乗り越えるための、とっておきの「キャッシュ戦略」を伝授しよう。

キャッシュ戦略の基本:一度取ったデータは、しばらく「しまっておく」

一番シンプルで効果的なのは、「一度Notion APIから取得したデータを、一定時間(例えば数分間)サーバー側で保持しておく」という考え方だ。

こうすれば、同じデータを何度もAPIに問い合わせる必要がなくなり、レートリミットに引っかかるリスクを大幅に減らせる。さらに、APIからの応答を待つ時間がなくなるから、Webサイトの表示速度も劇的に向上するんだ。まさに一石二鳥!

Next.jsで実装する、効果的なキャッシュ戦略

Next.jsには、このキャッシュ戦略を簡単に実装できる機能が備わっている。今回は、その中でも特に強力な「ISR (Incremental Static Regeneration)」と組み合わせることで、リアルタイムに近い更新を実現する方法を解説していくよ。

まずは、Next.jsのAPIルートを使って、Notion APIからデータを取得し、それを返す簡単な処理を書いてみよう。

`pages/api/posts.js` (APIルートファイル)

// Notion APIクライアントをインポート
import { Client } from ‘@notionhq/client’;

// 環境変数からAPIキーとデータベースIDを取得
const notion = new Client({ auth: process.env.NOTION_API_KEY });
const databaseId = process.env.NOTION_DATABASE_ID;

// 取得した記事データをキャッシュするための変数 (簡易的な実装)
let cachedPosts = null;
let cacheTimestamp = 0;
const CACHE_DURATION = 60 1000; // 1分間キャッシュ (60秒 1000ミリ秒)

export default async function handler(req, res) {
const currentTime = Date.now();

// キャッシュが存在し、かつ有効期限内であればキャッシュを返す
if (cachedPosts && currentTime – cacheTimestamp < CACHE_DURATION) { console.log('--- Using cached posts ---'); return res.status(200).json(cachedPosts); } console.log('--- Fetching posts from Notion API ---'); try { // Notion APIからデータベースのページを取得 const response = await notion.databases.query({ database_id: databaseId, // 必要に応じてソートやフィルタリング条件を追加 // 例: 公開済みの記事のみ取得する場合 // filter: { // property: 'Status', // Notionデータベースのプロパティ名に合わせてください // select: { // equals: 'Published', // }, // }, // sort_by: { // property: 'PublishedDate', // Notionデータベースのプロパティ名に合わせてください // direction: 'descending', // }, }); // 取得したデータを加工 (必要に応じて) const posts = response.results.map((page) => {
// Notionのページオブジェクトから必要な情報を抽出
// プロパティ名は君のNotionデータベースの構造に合わせてください
return {
id: page.id,
title: page.properties.Name.title[0]?.plain_text || ‘Untitled’, // タイトル
slug: page.properties.Slug.rich_text[0]?.plain_text || ”, // URLスラッグ (別途Notionで定義)
// 他に必要なプロパティを追加 (例: 作成日、サムネイルなど)
// createdAt: page.properties.CreatedTime.created_time,
// thumbnailUrl: page.properties.Thumbnail.files[0]?.file?.url || null,
};
});

// キャッシュを更新
cachedPosts = posts;
cacheTimestamp = currentTime;

res.status(200).json(posts);
} catch (error) {
console.error(‘Error fetching posts from Notion:’, error);
res.status(500).json({ message: ‘Error fetching posts’ });
}
}

解説:

  • `@notionhq/client`: Notion公式のNode.jsクライアントライブラリだよ。これを使うと、Notion APIとのやり取りがすごく楽になる。
  • `process.env.NOTION_API_KEY`, `process.env.NOTION_DATABASE_ID`: APIキーやデータベースIDは、コードに直接書かず、環境変数で管理するのがセキュアでベストプラクティス。Vercelでは、プロジェクトの設定画面から簡単に環境変数を設定できるよ。
  • `cachedPosts`, `cacheTimestamp`, `CACHE_DURATION`: ここがキャッシュの心臓部。
  • `cachedPosts`: Notionから取得した記事データを一時的に保存しておく変数。
  • `cacheTimestamp`: 最後にキャッシュを更新した時刻を記録する。
  • `CACHE_DURATION`: キャッシュが有効な期間。今回は1分に設定。
  • `if (cachedPosts && currentTime – cacheTimestamp < CACHE_DURATION)`: ここで、キャッシュが存在して、かつ有効期限内であれば、APIに問い合わせる代わりにキャッシュからデータを返している。これにより、APIリクエスト回数を大幅に削減できるんだ。
  • `notion.databases.query()`: Notion APIを叩いて、指定したデータベースのページ一覧を取得するメソッド。
  • `response.results.map()`: 取得したデータは、Notionの構造に合わせた形式になっているから、君が使いやすいように加工する必要がある。`title`, `slug` などのプロパティ名は、君のNotionデータベースの実際のプロパティ名に合わせて調整してね。

完璧な「Hello, World!」のためのセットアップ

このコードを動かすために、いくつか準備が必要だ。

1. Notionデータベースの準備:

  • Notionで新しいページを作成し、「データベース」としてテーブルビューで作成する。
  • 最低限、「Name」(タイトル用)、「Slug」(URL用)といったプロパティを作成しておこう。必要に応じて「作成日」や「公開日」などのプロパティも追加してね。
  • 【超重要】 「Slug」プロパティは、各記事のURLに使うユニークな文字列(例: `my-first-post`)を入力しておく。これはNext.jsのファイルベースルーティングで使うから、必ず設定しよう。
  • データベースの共有設定で、「共有」→「リンクを知っている全員」に「閲覧可能」を設定する。

2. Notion APIキーの取得:

  • [NotionのMy integrationsページ](https://www.notion.so/my-integrations)にアクセス。
  • 「+ New integration」ボタンをクリック。
  • Integration name (例: `My Blog CMS`) を入力し、Associated workspace を選択。
  • Capabilitiesは「Read content」と「Insert content」にチェックを入れる(今回はReadのみでOK)。
  • 「Submit」をクリックすると、“Internal Integration Token” が表示される。これがAPIキーだよ。絶対に誰にも見せないように、大切に保管してね。

3. データベースIDの取得:

  • 君が作成したNotionデータベースのURLを見る。
  • `https://www.notion.so/your-workspace/YOUR-DATABASE-TITLE-DATABASE_ID?v=VIEW_ID` のような形式になっているはず。
  • `DATABASE_ID` の部分、32文字の英数字の文字列がデータベースIDだよ。

4. Next.jsプロジェクトの作成と環境変数の設定:

  • まだNext.jsプロジェクトがない場合は、以下のコマンドで作成しよう。

npx create-next-app@latest my-blog-app
cd my-blog-app

  • プロジェクトのルートディレクトリに `.env.local` というファイルを作成し、以下のように記述する。

# .env.local
NOTION_API_KEY=君のNotion APIキーをここに入力
NOTION_DATABASE_ID=君のNotionデータベースIDをここに入力

注意: `.env.local` ファイルは、Gitなどのバージョン管理システムに含めないように `.gitignore` に追加するのを忘れないでね!

  • Notion APIクライアントライブラリをインストールする。

npm install @notionhq/client
# または yarn
# yarn add @notionhq/client

5. NotionデータベースとAPIの連携設定:

  • Notionデータベースの右上の「…」メニューをクリック。
  • 「Connect to」→「Integrations」を選択。
  • 先ほど作成したIntegration (例: `My Blog CMS`) を選択し、「Allow access」をクリック。これで、作成したAPIキーがこのデータベースにアクセスできるようになるよ。

これで、APIルート `pages/api/posts.js` が、Notionから記事データを取得できる準備が整った!

3. 「数秒で反映」を実現! ISRと組み合わせた高速ブログ運用

さて、いよいよクライマックスだ。APIのレートリミット対策としてキャッシュ戦略を導入したけど、これだけだとNotionを更新しても、キャッシュが切れるまで反映されない。君が求めているのは、「Notionを更新したら、数秒でWebサイトにも反映される」ことだよね?

それを実現するのが、Next.jsの「ISR (Incremental Static Regeneration)」という、まさに魔法のような機能なんだ!

ISRって何? なぜ「数秒で反映」が可能なのか?

ISRは、静的サイト生成 (SSG) の進化版と言える。

  • SSG: ビルド時に全ページを生成し、高速な静的ファイルとして配信する。
  • メリット: 爆速!
  • デメリット: 更新するには再ビルドが必要。
  • ISR: SSGのメリット(高速性)はそのままに、「一定時間ごとにバックグラウンドでページを再生成し、更新する」という機能を追加したもの。

つまり、ISRを使うと、Webサイトは常に最新の状態に近い静的ファイルを提供しつつ、裏側では自動的にページを更新してくれるんだ。

Next.jsでISRを組み込む方法

ISRを実装するには、Next.jsの`getStaticProps`関数内で `revalidate` オプションを指定するだけ。これが、数秒で更新を反映させるための魔法の呪文になるんだ。

`pages/index.js` (ブログ一覧ページなどの例)

// APIルートから記事データを取得する関数
async function getPostsData() {
// 環境変数からAPIエンドポイントを取得
const apiUrl = process.env.NEXT_PUBLIC_API_URL || ‘http://localhost:3000/api/posts’;

try {
const res = await fetch(apiUrl);

// APIがエラーを返した場合の処理
if (!res.ok) {
// レートリミットなどのエラーを考慮し、エラーページにリダイレクトするなど
// ここでは一旦、空の配列を返す
console.error(`API Error: ${res.status} ${res.statusText}`);
return [];
}

const posts = await res.json();
return posts;
} catch (error) {
console.error(‘Error fetching posts:’, error);
return []; // エラー時も空の配列を返す
}
}

export default function Home({ posts }) {
return (

My Awesome Blog

);
}

// ISRを有効にするための関数
export async function getStaticProps() {
const posts = await getPostsData();

return {
props: {
posts,
},
// ここがISRの肝!
// 60秒ごとにバックグラウンドでページを再生成する
revalidate: 60, // 60秒ごとに再検証
};
}

解説:

  • `getPostsData()`: 先ほど作成したAPIルート (`/api/posts`) から記事データを取得する関数。`fetch` を使って非同期にデータを取得しているよ。
  • `process.env.NEXT_PUBLIC_API_URL`: Vercelにデプロイする際に、APIエンドポイントを動的に設定できるように、環境変数で管理するのがおすすめ。
  • `export default function Home({ posts })`: 取得した記事データを元に、ブログ一覧ページを描画するReactコンポーネント。
  • `export async function getStaticProps()`: ここがISRを設定する場所。
  • `const posts = await getPostsData();`: まず、`getPostsData()` で最新(またはキャッシュされた)記事データを取得する。
  • `props: { posts }`: 取得したデータを、`Home` コンポーネントにpropsとして渡す。
  • `revalidate: 60`: これがISRの魔法! この設定により、Next.jsは60秒ごとにこのページのバックグラウンドでの再生成を試みるようになる。ユーザーがページにアクセスした際に、最後に生成されてから60秒経過していれば、バックグラウンドで再生成が開始される。生成が完了するまでは古いページが表示されるので、ユーザー体験を損なわずに更新できるんだ。

ブログ記事詳細ページもISRで!

個別の記事ページも同様にISRで構築できるよ。

`pages/posts/[slug].js` (動的ルーティングを使った記事詳細ページ)

import { Client } from ‘@notionhq/client’;

const notion = new Client({ auth: process.env.NOTION_API_KEY });
const databaseId = process.env.NOTION_DATABASE_ID;

// 記事データを取得する関数 (APIルート経由ではなく、直接Notion APIを叩く例)
async function getPostBySlug(slug) {
try {
const response = await notion.databases.query({
database_id: databaseId,
filter: {
property: ‘Slug’, // NotionデータベースのSlugプロパティ名に合わせてください
rich_text: {
equals: slug,
},
},
});

if (response.results.length === 0) {
return null; // slugに一致する記事が見つからなかった場合
}

const page = response.results[0];
// NotionのブロックAPIを使って本文を取得する処理は別途必要
// ここでは便宜上、タイトルのみ取得
return {
id: page.id,
title: page.properties.Name.title[0]?.plain_text || ‘Untitled’,
slug: page.properties.Slug.rich_text[0]?.plain_text || ”,
// content: ‘Notion Block Content…’ // 本文は別途取得
};
} catch (error) {
console.error(`Error fetching post with slug “${slug}”:`, error);
return null;
}
}

// Notion APIから全記事のslugを取得する関数 (getStaticPaths用)
async function getAllPostSlugs() {
try {
const response = await notion.databases.query({
database_id: databaseId,
filter: {
property: ‘Slug’, // Slugプロパティが存在し、空でない記事のみを対象とする
rich_text: {
is_not_empty: true,
},
},
});
return response.results.map((page) => ({
slug: page.properties.Slug.rich_text[0]?.plain_text || ”,
}));
} catch (error) {
console.error(‘Error fetching all post slugs:’, error);
return [];
}
}

export default function Post({ post }) {
if (!post) {
return

Post not found.

;
}

return (

{post.title}

{/ 本文表示エリア (Notion Block Contentをパースして表示する処理を実装) /}

This is the content of the post.

Slug: {post.slug}

);
}

// 動的ルーティングで生成するパスを指定
export async function getStaticPaths() {
const slugs = await getAllPostSlugs();

const paths = slugs.map((item) => ({
params: { slug: item.slug },
}));

return {
paths,
fallback: ‘blocking’, // 指定されていないslugへのアクセス時は、サーバー側で生成して返す
};
}

// ISRを有効にするための関数
export async function getStaticProps({ params }) {
const post = await getPostBySlug(params.slug);

// 記事が見つからなかった場合、404ページを返す
if (!post) {
return {
notFound: true,
};
}

return {
props: {
post,
},
revalidate: 60, // 60秒ごとに再検証
};
}

解説:

  • `getPostBySlug(slug)`: 指定された`slug`に一致する記事をNotionから検索して取得する関数。
  • `getAllPostSlugs()`: `getStaticPaths`で必要となる、全ての記事の`slug`を取得する関数。
  • `getStaticPaths()`: Next.jsが、どの`slug`でページを生成すれば良いかを教えてあげるための関数。`fallback: ‘blocking’` とすることで、まだ生成されていない`slug`にアクセスがあった場合でも、サーバー側で動的に生成してくれる。
  • `getStaticProps({ params })`: 個別記事の詳細ページでも、`revalidate: 60` を設定することでISRが有効になる。

Vercelへのデプロイと、最終確認!

1. Vercelプロジェクトの作成:

  • Vercelのダッシュボードにログインし、「New Project」をクリック。
  • GitHubなどのリポジトリから、君のNext.jsプロジェクトをインポートする。
  • Vercelが自動的にフレームワークを検出し、ビルド設定をしてくれるはず。
  • 【重要】 「Environment Variables」のセクションで、`NOTION_API_KEY` と `NOTION_DATABASE_ID` を設定するのを忘れないでね! `NEXT_PUBLIC_API_URL` も、デプロイ環境に合わせて設定しよう。

2. デプロイ!

  • 設定が完了したら、「Deploy」ボタンをクリック。Vercelがコードをビルドし、インターネット上に公開してくれる。

3. 動作確認:

  • デプロイされたWebサイトにアクセスしてみよう。ブログ記事が正しく表示されるはずだ。
  • 次に、Notionデータベースで記事を新規追加したり、既存の記事を編集・公開したりしてみよう。
  • 数分後(`revalidate`で設定した時間以内) に、Webサイトをリロードしてみて! ほら、更新が反映されているはずだ!

これで君も、NotionとNext.js、Vercelを駆使した、超高速でリアルタイムに近いブログシステムを構築できるようになった!

まとめ:君はもう、次世代のブログ運用マスターだ!

どうだったかな? Notionの柔軟性と、Notion APIの強力さ、そしてNext.jsのISRという組み合わせは、まさに現代のWeb開発における「最強のタッグ」の一つと言える。

今回学んだこと:

  • NotionをCMSとして活用するメリットとアーキテクチャ
  • Notion APIのレートリミットを回避するキャッシュ戦略
  • Next.jsのISRを使った、数秒での自動更新の実装方法

これらを理解し、実践できるようになった君は、もう立派な開発者だ! これからは、コンテンツ作成に集中しつつも、最新の技術で読者に最高の体験を提供できるブログを、自信を持って運用していけるはずさ。

この知識があれば、君のプロジェクトや日々の開発が、きっとより効率的で、もっと楽しくなるはずだよ。もし分からないことがあれば、いつでも聞いてくれてOKだ! さあ、この興奮を胸に、次のレベルへと進もう!

タイトルとURLをコピーしました