

Cloudflare PagesでNext.jsサイトを運用していたが、Edge Runtime制約が厳しくなった
microCMSからNotion APIへの移行に伴い、Node.jsランタイムが必要になった
@cloudflare/next-on-pagesはEdge Runtime必須で制約が多い
OpenNext(@opennextjs/cloudflare)を使えばNode.js互換でCloudflare Workers上でNext.jsが動く
OpenNextのセットアップ手順
SSG(Static Site Generation)対応の設定
wrangler.toml の詳細設定
GitHub ActionsによるCI/CD構築
Workers有料プランが必要な理由
Cloudflare WorkersでNext.jsを動かしたい方
Cloudflare PagesのEdge Runtime制約に困っている方
Next.jsのSSGサイトをCloudflareにデプロイしたい方
@cloudflare/next-on-pagesはCloudflare Pages向けのアダプターだが、以下の制約がある:
Edge Runtime必須: すべてのルートがexport const runtime = 'edge'を要求される
Node.js APIが使えない: crypto, fs, path等のNode.jsモジュールが利用不可
npm パッケージの制約: Node.js APIに依存するパッケージ(@notionhq/client, cheerio等)が動かない
@opennextjs/cloudflareはCloudflare WorkersのNode.js互換モードを活用し、Next.jsをほぼそのまま動かす。
Node.js APIが使える(crypto, Buffer等)
npm パッケージの互換性が高い
ISR / SSG / SSR すべてサポート
npm install @opennextjs/cloudflare
npm install -D wrangler
# esbuildの明示的インストールが必要
npm install -D esbuild
import { defineCloudflareConfig } from '@opennextjs/cloudflare';
export default defineCloudflareConfig({});
#:schema node_modules/wrangler/config-schema.json
name = "kt-tech-blog"
main = ".open-next/worker.js"
compatibility_date = "2025-03-14"
compatibility_flags = ["nodejs_compat"]
[assets]
directory = ".open-next/assets"
binding = "ASSETS"
# KVはISRキャッシュに使用
[[kv_namespaces]]
binding = "NEXT_CACHE_WORKERS_KV"
id = "your-kv-namespace-id"
{
"scripts": {
"build": "opennextjs-cloudflare",
"dev": "next dev",
"deploy": "opennextjs-cloudflare && wrangler deploy",
"preview": "opennextjs-cloudflare && wrangler dev"
}
}
SSGでは、ビルド時に全ページのパスを静的に生成する必要がある。
// src/app/blogs/[blogId]/page.tsx
export async function generateStaticParams() {
const allBlogs = await getAllBlogs(); // Notion APIから全記事取得
return allBlogs.map((blog) => ({
blogId: blog.id,
}));
}
Notion APIにはレート制限(3リクエスト/秒)があるため、ビルド時の並列度を制限する。
// next.config.js
module.exports = {
experimental: {
workerThreads: false,
cpus: 1, // ビルドワーカー数を1に制限
},
};
// libs/notion.ts
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
export async function getAllBlogs(): Promise<Blog[]> {
const pages = [];
let cursor: string | undefined;
do {
const response = await notion.dataSources.query({
data_source_id: DATABASE_ID,
start_cursor: cursor,
});
pages.push(...response.results);
cursor = response.next_cursor ?? undefined;
await delay(350); // 350ms間隔でAPI呼び出し
} while (cursor);
return pages.map(pageToBlog);
}
# .github/workflows/deploy.yml
name: Deploy to Cloudflare Workers
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Build with OpenNext
run: npx opennextjs-cloudflare
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}
- name: Deploy to Cloudflare Workers
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
ランタイムで使用する環境変数はWorker Secretsに設定する必要がある。
# Worker Secretsに環境変数を設定
echo "your-notion-token" | npx wrangler secret put NOTION_TOKEN
echo "your-database-id" | npx wrangler secret put NOTION_DATABASE_ID
echo "your-ga-id" | npx wrangler secret put NEXT_PUBLIC_GA_ID
Cloudflare Workers無料プランにはCPU時間10msの制限がある。Next.jsのSSRレンダリングはこの制限を大幅に超えるため、無料プランでは動作しない。
Error 1102: Worker exceeded CPU time limit
Workers Paid プラン($5/月)にアップグレードすると:
CPU時間: 10ms → 30秒
リクエスト数: 10万/日 → 1000万/月
Workers KV: 読み取り10万/日 → 1000万/月
個人ブログの規模であれば$5/月で十分運用可能。
esbuildの明示的インストールが必要。OpenNextのビルドプロセスで使用されるが、peer dependencyとして自動インストールされないことがあるgetAllBlogsで全記事取得する際はdelay関数を挟むwrangler devでローカルプレビューする際は.dev.varsファイルに環境変数を設定するOpenNextを使えばCloudflare Workers上でNext.jsがNode.js互換で動作する
SSG対応はgenerateStaticParamsで全パスを生成し、ビルドワーカー数を制限する
CI/CDはGitHub Actions + wrangler deployで簡単に構築可能
Workers有料プラン($5/月) が必須。無料プランのCPU 10ms制限では動かない
Worker Secretsの設定を忘れると503エラーが大量発生するので注意