メインコンテンツへスキップ
kt-tech.blog

【設定・環境構築】OpenNext でNext.js SSGサイトをCloudflare Workersにデプロイする完全ガイド

設定・環境構築9分で読めます

この記事でわかること

  • OpenNext を使って Next.js を Workers で動かすセットアップ一式
  • generateStaticParams で SSG するときにビルドがレート制限に当たらないようにする工夫
  • Workers 無料プランの CPU 10ms では動かないことと、有料プランで何が変わるか
  • そして、この構成をこの後 Pages に戻した理由

Next.js(App Router)のプロジェクトと Cloudflare アカウント。Workers 有料プラン(月 5 ドル)が実質必須です。

概要: @opennextjs/cloudflareを使ってNext.jsのSSGサイトをCloudflare Workersにデプロイする方法を解説します。セットアップからSSG対応、CI/CD構築まで、本番運用に必要な設定を網羅しています。

はじめに

Cloudflare Pages で Next.js サイトを運用していましたが、microCMS から Notion API への移行に伴って Node.js ランタイムが必要になり、Edge Runtime の制約が厳しくなりました。OpenNext(@opennextjs/cloudflare)を使えば Node.js 互換で Cloudflare Workers 上で Next.js が動きます。

メモ
先に結論を書いておくと、この後このブログは Pages に戻しています。手順自体は正しく動きますが、SSG で生成した HTML も Worker 経由で返るため TTFB が伸びました。その実測と判断は Cloudflare Workers→Pages出戻り記 にあります。アプリケーションを載せるなら Workers が向きます。

1. なぜOpenNextか

@cloudflare/next-on-pages の制約

@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等)が動かない

OpenNextのアプローチ

@opennextjs/cloudflareはCloudflare WorkersのNode.js互換モードを活用し、Next.jsをほぼそのまま動かす。

  • Node.js APIが使える(crypto, Buffer等)
  • npm パッケージの互換性が高い
  • ISR / SSG / SSR すべてサポート

2. セットアップ

インストール

Bash
npm install @opennextjs/cloudflare
npm install -D wrangler
# esbuildの明示的インストールが必要
npm install -D esbuild

open-next.config.ts

TypeScript
import { defineCloudflareConfig } from '@opennextjs/cloudflare';

export default defineCloudflareConfig({});

wrangler.toml

TOML
#: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"

package.json のスクリプト更新

JSON
{
  "scripts": {
    "build": "opennextjs-cloudflare",
    "dev": "next dev",
    "deploy": "opennextjs-cloudflare && wrangler deploy",
    "preview": "opennextjs-cloudflare && wrangler dev"
  }
}

3. SSG対応

generateStaticParamsで全ページパスを生成

SSGでは、ビルド時に全ページのパスを静的に生成する必要がある。

TypeScript
// src/app/blogs/[blogId]/page.tsx
export async function generateStaticParams() {
  const allBlogs = await getAllBlogs(); // Notion APIから全記事取得
  return allBlogs.map((blog) => ({
    blogId: blog.id,
  }));
}

ビルドワーカー数を1に制限

Notion APIにはレート制限(3リクエスト/秒)があるため、ビルド時の並列度を制限する。

JavaScript
// next.config.js
module.exports = {
  experimental: {
    workerThreads: false,
    cpus: 1, // ビルドワーカー数を1に制限
  },
};

API呼び出し間隔の制御

TypeScript
// 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);
}

4. CI/CD — GitHub Actions

ワークフロー定義

YAML
# .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に設定する必要がある。

Bash
# Worker Secrets に環境変数を設定
# 引数なしで実行し、プロンプトに値を貼る(履歴に残さない)
npx wrangler secret put NOTION_TOKEN
npx wrangler secret put NOTION_DATABASE_ID

5. Workers有料プランが必要

無料プランの制約

Cloudflare Workers無料プランにはCPU時間10msの制限がある。Next.jsのSSRレンダリングはこの制限を大幅に超えるため、無料プランでは動作しない。

エラー例

Error 1102: Worker exceeded CPU time limit

有料プラン($5/月)

Workers Paid プラン($5/月)にアップグレードすると:

  • CPU時間: 10ms → 30秒
  • リクエスト数: 10万/日 → 1000万/月
  • Workers KV: 読み取り10万/日 → 1000万/月

個人ブログの規模であれば$5/月で十分運用可能。


Tips

Tips
esbuild の明示的インストールが必要。OpenNext のビルドプロセスで使用されるが、peer dependency として自動インストールされないことがある
Tips
この delay が必要なのは「ビルド時に全記事を連続で取る」場面だけです。リクエストごとに 1、2 回叩くランタイムでは不要で、入れるとそのまま TTFB に乗ります。
Tips
wrangler dev でローカルプレビューする際は .dev.vars ファイルに環境変数を設定する(.gitignore への追加を忘れずに)
Tips
Workers KV の namespace ID は wrangler.toml に書いてよいが、API トークン等の secret は絶対に書かない(リポジトリに入る)

参考リンク

まとめ

  • OpenNextを使えばCloudflare Workers上でNext.jsがNode.js互換で動作する
  • SSG対応はgenerateStaticParamsで全パスを生成し、ビルドワーカー数を制限する
  • CI/CDはGitHub Actions + wrangler deployで簡単に構築可能
  • Workers有料プラン($5/月) が必須。無料プランのCPU 10ms制限では動かない
  • Worker Secretsの設定を忘れると503エラーが大量発生するので注意

更新履歴

  1. この後 Pages に戻したことを冒頭に明記し、判断の根拠記事へリンク。delay が必要なのはビルド時だけで、ランタイムに入れると TTFB に乗ることを補足。echo "トークン" | wrangler secret put を引数なし実行に修正(シェル履歴対策)。壊れていた目次マーカーを自動目次に差し替え。エラーのコードブロックの言語指定を text に修正。引用形式の Tips を callout に統一。参考リンクの節を新設。箸条書きだった「はじめに」を文章に整理。