無料公開中
公開翌日4時まで

Webサイトホスティングで使うCloudflare Workers 第3回 Next.jsのサイトをWorkersにデプロイする

第3回は、Next.jsで作ったサイトをCloudflare Workersにデプロイします。静的エクスポートによるSSGと、OpenNextのアダプターを使ったSSRの2通りを取り上げ、Astroとの違いや、開発時と本番でランタイムが異なることによる注意点を解説します。

発行

著者 渡辺 由 フロントエンド・エンジニア
Webサイトホスティングで使うCloudflare Workers シリーズの記事一覧

前回まで

前回は、Astroで作ったサイトをCloudflare Workersにデプロイしました。SSGのシンプルな構成と、アダプターを利用したSSRの2種類のホスティングに加え、GitHubリポジトリと連携させて自動デプロイとプレビューURLまで整える、という流れでした。

今回はNext.jsです。フレームワークが変わると何が変わり、何が変わらないのか見ていきましょう。

次の2つは「変わらないところ」です。フレームワークによって大きく変わることはありませんので、今回は詳しい解説はしません。

  • Cloudflare Workersに関連するwranglerコマンドの操作と、設定ファイルであるwrangler.jsoncの記法
  • GitHubリポジトリとの連携(Workers Builds)やプレビューURLの発行

一番差が出てくるのは、SSRのアダプターに関するところです。

デモリポジトリ

前回と同様、記事の手順ごとにコミットを分けています。

SSGの場合

Next.jsもAstro同様にSSGとSSRの両方が可能です。もう1つ、ISR(Incremental Static Regeneration)という独自のレンダリング手法ももっていますが、こちらについては後述します。

まずは静的エクスポートから見ていきます。Next.jsでは、next.config.tsoutput: 'export'を指定すると、各ルートをHTMLファイルとして書き出す静的エクスポートになります。あとはそれを静的アセットとして配信するだけなので、AstroのSSGとほぼ同じで、wrangler.jsoncに配信したいディレクトリを指定するだけでよいです。

create-next-appで作ったプロジェクトのnext.config.tsに、1行足します。

静的エクスポートを有効にする(next.config.ts)

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "export",  // この1行を追加する
};

export default nextConfig;

これでnpm run buildを実行すると、outディレクトリにHTMLなどのアセットが書き出されます。

あとは前回のwrangler.jsoncを流用できます。変えるのはassets.directoryで、AstroのdistをNext.jsのoutに向けるだけです。

静的エクスポートの場合のwrangler.jsonc

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "2026-workers-hosting-nextjs",
  "compatibility_date": "2026-08-28",
  "assets": {
    "directory": "./out", // Astroでは ./dist だった
    "not_found_handling": "404-page"
  }
}

not_found_handlingも前回同様です。Next.jsは静的エクスポート時にout/404.htmlを書き出してくれるので、404-pageを指定しておけば、どのアセットにも一致しなかったリクエストにはNext.js製の404.htmlが返ります。

あとはnpx wrangler deployまたはversions uploadすればWorkersで配信されるようになります。

静的エクスポートと画像最適化

Next.jsの静的エクスポートでは、いくつか注意が必要なものがあります。一番ひっかかりやすいと思われるのは、画像の最適化です。Next.jsがデフォルトで提供するnext/imageを使用する場合、静的エクスポートではうまくいきません。

Next.jsは画像を最適化して返すためにサーバー側の処理を必要としますが、Workersの静的アセット配信はただファイルを返すだけなので、処理は行えません。ビルドは成功するものの、Workersにデプロイしても、画像のURLは404になってしまいます。

対処法としては、最適化がなくても困らないのであれば、デフォルトの最適化を切ってしまうのが手っ取り早いです。

画像の最適化を無効にする(next.config.ts)

const nextConfig: NextConfig = {
  output: "export",
  images: {
    unoptimized: true,  // 無効のオプションをtrueに設定
  },
};

こうすると、画像もout/ディレクトリに書き出され静的な配信になります。

最適化を続けたい場合は、外部の画像配信サービスを使う、ビルド時に圧縮やフォーマット変換をする、などの方法もありますが、これらはコードやライブラリの追加が必要です。筆者は、SSGに徹するのであれば画像最適化も使わないのがシンプルでよいと考えます。静的エクスポートで使えない機能はほかにもありますが、そういった機能を使いたい・検討しているという場合は、回避策を講じなくても済むようなレンダリング方法やホスティング環境を選定するのがよいのではないでしょうか。

静的エクスポートで使えない機能

画像最適化のほかにも、静的なアセット配信ではNext.jsの機能に制約が出ます。公式ドキュメントが挙げているのは、たとえば次のようなものです。

  • Route Handlersのうち、リクエストの内容を読むもの
  • cookiesheadersredirectsrewrites
  • Server Actions
  • generateStaticParams()のない動的ルート

これらは画像最適化とは違ってビルド時にエラーになるので気づきやすくはありますが、いずれにしても静的なサイトとしてCloudflare Workersから配信することはできません。

SSRの場合(OpenNext)

続いてSSRの場合です。Astro同様にアダプターが必要です。AstroではAstroのチームが開発している@astrojs/cloudflareをインストールしました。Next.jsの場合は@opennextjs/cloudflareというパッケージを使います。@opennextjsが何なのか気になった方もいるかもしれません。これは、OpenNextという、Vercel以外のプラットフォームでNext.jsを動かすためのコミュニティの名前です。コミュニティベースのパッケージということになりますが、Cloudflare Workersのドキュメントでも紹介されています。

vinextとアダプターの選択肢

Cloudflare Workersのドキュメントでは、vinextというパッケージが推奨と書かれています。こちらはCloudflareが開発していて、Next.jsのAPIをViteの上に再実装するという、OpenNextとは異なるアプローチのプロジェクトです。Next.js 16のAPIの94%をカバーするとしており、冒頭で述べたNext.jsの独自のレンダリング手法のISRや、Proxyにも対応しています。専用のnpx @vinext/cloudflare deployでWorkersに配信できます。

ただし、執筆時点(2026年8月)でのバージョンは1.0.0-beta.8で、「Experimental software, use at your own risk」と明記されています。安心してプロダクション利用ができるようになるのはもう少し先、といったところでしょうか。とはいえ、OpenNextよりもできることも増え、パフォーマンスもよくなりそうですので、将来的にはこちらに乗り換えていくのがよさそうです。

もう1つ、@cloudflare/next-on-pagesというアダプターの名前を見かけることがあるかもしれません。こちらもCloudflareが開発したものですが、Cloudflare Pages向けの古いもので、現在は非推奨です。

SSRアダプターの導入

では@opennextjs/cloudflareを導入しましょう。npmでインストールして、アダプターが用意するマイグレーションコマンドを実行します。

ただ、実行する前に済ませておくことが2つあります。

1つは、CloudflareアカウントでR2を有効化しておくことです。後述しますが、R2はCloudflareのストレージサービスです。migrateはキャッシュ用のR2バケットを自動的に作りますが、有効化していないとこれができません。有効化していなくても、エラーが出るわけではなく、警告が1行出るだけなので、気づかないまま先へ進みがちです。

ダッシュボードの「ストレージとデータベース > R2 オブジェクトストレージ」から有効化できます。

もう1つは、先ほどSSGの節で作ったwrangler.jsoncを削除しておくことです。migrateがSSR用のファイルをあらためて作るので、すでに存在しているとエラーになってしまいます。

OpenNextの導入

npm i @opennextjs/cloudflare@latest
npx opennextjs-cloudflare migrate

migrateコマンドは、wrangler.jsoncのほかにもopen-next.config.ts.dev.varspublic/_headersを作ったり、package.json.gitignoreを更新してくれたりします。

一方でmigrateコマンドが面倒を見てくれないところもあります。next.config.tsです。migratenext.config.tsの末尾に開発サーバー用の1行を足すだけなので、SSGのところで書いたoutput: 'export'はそのまま残っています。静的エクスポートの指定が残ったままだと、OpenNextのビルドはエラーで止まってしまいます。SSRに切り替える際は、output: 'export'と、併せて指定したimages.unoptimizedも削除してください。

静的エクスポート用の設定を外す(next.config.ts)

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // output: "export" と images.unoptimized は削除する
};

export default nextConfig;

import('@opennextjs/cloudflare').then(m => m.initOpenNextCloudflareForDev());

SSRでのwrangler.jsonc

生成されたwrangler.jsoncを見てみましょう。コメントも生成されたままのものです。長いので、ここでは全体を眺めるだけでかまいません。このあと、要点を抜き出して見ていきます。

migrateが生成したwrangler.jsonc

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "main": ".open-next/worker.js",
  "name": "2026-workers-hosting-nextjs",
  "compatibility_date": "2026-08-30",
  "compatibility_flags": [
    "nodejs_compat",
    "global_fetch_strictly_public"
  ],
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  },
  "services": [
    {
      // Self-reference service binding, the service name must match the worker name
      // see https://opennext.js.org/cloudflare/caching
      "binding": "WORKER_SELF_REFERENCE",
      "service": "2026-workers-hosting-nextjs"
    }
  ],
  "r2_buckets": [
    // Use R2 incremental cache
    // See https://opennext.js.org/cloudflare/caching
    {
      "binding": "NEXT_INC_CACHE_R2_BUCKET",
      // Create the bucket before deploying
      // You can change the bucket name if you want
      // See https://developers.cloudflare.com/workers/wrangler/commands/#r2-bucket-create
      "bucket_name": "2026-workers-hosting-nextjs-opennext-cache"
    }
  ],
  "images": {
    // Enable image optimization
    // see https://opennext.js.org/cloudflare/howtos/image
    "binding": "IMAGES"
  }
}

Astroでの設定と比べると長いですが、前半のmaincompatibility_flagsassetsの部分はよく似ています。nodejs_compatも前回と同じで、Next.jsやReactもNode.js互換のAPIを必要とします。namepackage.jsonnameから決まります。create-next-appで作った直後はディレクトリ名と同じになっているはずです。ここでは2026-workers-hosting-nextjsにしてあります。

そして、Astroにはなかったキーが3つ増えています。servicesr2_bucketsimagesです。これらはNext.jsのキャッシュや画像最適化をWorkers上で動かすためのものです。このうち、キャッシュに関わる2つを見ておきましょう。

まずservicesです。

生成されたwrangler.jsoncのservices(再掲)

  "services": [
    {
      // Self-reference service binding, the service name must match the worker name
      // see https://opennext.js.org/cloudflare/caching
      "binding": "WORKER_SELF_REFERENCE",
      "service": "2026-workers-hosting-nextjs"
    }
  ],

Worker自身を呼び出すためのサービスバインディングです。コメントにあるとおり、serviceの値はworker名、つまりnameと一致している必要があります。nameを変更する場合は、このserviceの値も書き換えてください。

次がr2_bucketsです。ここで先ほど有効化したR2を見てみましょう。

生成されたwrangler.jsoncのr2_buckets(再掲)

  "r2_buckets": [
    // Use R2 incremental cache
    // See https://opennext.js.org/cloudflare/caching
    {
      "binding": "NEXT_INC_CACHE_R2_BUCKET",
      // Create the bucket before deploying
      // You can change the bucket name if you want
      // See https://developers.cloudflare.com/workers/wrangler/commands/#r2-bucket-create
      "bucket_name": "2026-workers-hosting-nextjs-opennext-cache"
    }
  ],

補足:R2ストレージ

R2は「バケット」からイメージできるかもしれませんが、Cloudflareが提供するストレージサービスです。AWSのS3と同様のものをイメージしていただくと近いです。10GB分の無料枠があり、データ転送はもともと無料ですので、Webサイト用の画像やキャッシュデータを置いておく程度であれば無料の範囲に収まると考えられますが、サービス自体は従量課金プランで提供されています。

2026-workers-hosting-nextjs-opennext-cacheというバケットが自動的に作成されています。r2_bucketsのコメントには「デプロイの前にバケットを作成する」とありますが、migrateが作ってくれるので、自分で作る必要はありません。

open-next.config.tsも生成されています。こちらにもキャッシュについての設定が書かれています。

open-next.config.ts

import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";

export default defineCloudflareConfig({
  incrementalCache: r2IncrementalCache,
});

package.jsonのスクリプトも書き換わります。

追加されたスクリプト

"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"

deploynpx wrangler deployではなくなっているところに注目してください。前回のnpx wrangler versions uploadに対応するのがuploadです。そして、どちらの前にもopennextjs-cloudflare buildが入っています。なぜコマンドを変更しなければならないのかは、このあとの「純正アダプターと変換レイヤー」で見ていきます。

GitHubリポジトリとCloudflareを接続している場合、Cloudflareの「設定」の「ビルド構成」も合わせます。「ビルド コマンド」は空欄に、「デプロイ コマンド」にnpm run deploy、「バージョン コマンド」にnpm run uploadを指定します。ビルドについてはdeployuploadコマンドに含まれているので不要になっています。

純正アダプターと変換レイヤー

なぜデプロイのコマンドが長くなったのでしょうか。それはnpm run deployの出力を見るとわかりやすいです。このnpmコマンドの中身はopennextjs-cloudflare build && opennextjs-cloudflare deployでしたので、ビルドとデプロイを直列につなげて行っています。出力は次のようになります。

Next.jsのビルド

> next build

 Next.js 16.3.3 (Turbopack)
...
Route (app)
 /
 ƒ /api/hello
 ƒ /ssr

┌──────────────────────────────┐
 OpenNext Generating bundle
└──────────────────────────────┘

Bundling middleware function...
Bundling static assets...
Bundling cache assets...
Building server function: default...
Applying code patches: 1.709s
Worker saved in `.open-next/worker.js` 🚀

最初に行われているのは普通のnext buildで、Next.jsは自分の標準的な成果物を.nextに書き出します。続いてOpenNextがその成果物を読み取り、workerdで動く1つのWorkerに組み直しています。ここまでがopennextjs-cloudflare buildの処理です。

前回のAstroとは構造的な違いがあります。Astroはビルドプロセスに拡張性があり、アダプターもその中でコード生成をしていました。Next.jsの場合は、いったんビルドしたものを、あとからWorkers向けに変換するので、ビルド処理が二段構えになっています。

ビルドに続いて実行されるopennextjs-cloudflare deployは、R2へのキャッシュの投入などを行ってから、wrangler deployを実行してくれます。

開発時のランタイム

デプロイされるものがworkerd向けに組み直されるのなら、手元で動かしているものは何なのでしょうか。最後に、ローカルでの開発について触れておきます。

Next.jsに限らず、多くのWebフレームワークはローカル用に2種類のコマンドを提供しています。開発サーバー(npm run dev)と、プレビュー(npm run preview)です。両者は本番との「距離」が異なり、その距離を生む要因は大きく2つあります。ビルドの有無と、ランタイムです。

1つ目はビルドの有無です。開発サーバーは、本番同様のビルドをせず、コード変更の即時反映を優先した「ゆるい」状態で動きます。もちろんビルド後のworkerd向けの組み直しも行われません。一方プレビューは、本番と同じようにビルドしたものを確認するためのサーバーです。ビルドするので変更の反映には時間がかかりますが、そのぶん本番に近いものを見られます。

2つ目はランタイムです。Next.jsの開発サーバーはNode.jsで動いており、Cloudflare Workersのランタイム(workerd)とは異なります。プレビューのほうはWranglerのサーバーが立ち上がるため、workerdにより近い環境になります。このランタイムの差は、「ローカルでは動いていたのに本番でビルドエラーになる」といった形で現れることがあります。

これらの点を踏まえると、普段は開発サーバー、デプロイ前にはプレビューでも確認する、という使い分けがよいでしょう。プレビューは変更のたびにビルドが必要で頻繁に使うのは現実的ではありません。versions uploadでWorkersのプレビューを発行してしまうのも1つの手です。

Astroの場合の開発サーバー

ではAstroの開発サーバーはどうだったのか、と思われるかもしれません。Astroのバージョン6以降に対応するSSRアダプターは、ローカルでもworkerdを動かせるようになっています。つまり、開発サーバーと本番環境のランタイムの差異はほとんどありません。実際には、依存パッケージなどの都合でローカルでのworkerd起動がうまくいかないバグも報告されていますが、いずれ解消していくと考えられます。

Astroを開発するThe Astro Technology Companyは2026年1月にCloudflareに買収されています。Astro自体はオープンソースでプラットフォーム非依存のまま続くとされていますが、Next.jsに比べ、AstroのほうがCloudflare自体との距離が近いのです。開発環境やアダプターの利便性の差は、こういった点の帰結かもしれません。

ここまでのまとめ

今回は、Next.jsで作ったサイトをCloudflare Workersにデプロイしました。アダプターの開発・対応状況が流動的であることや、現状で安定しているOpenNextでは使えない機能があるなど、Astroに比べて制約が多く感じられたかもしれません。実際、Next.jsのさまざまな機能をフルに活用するのであれば、開発元が同じであるVercelのほうに優位性があります。

ですが、Next.jsでもSSGがメインであったり、使っている機能が限定的であったりする状況であれば、Cloudflare Workersも十分選択肢になり得ると筆者は考えています。コストが安いですし、アクセス制限などのCloudflareの多様なサービスとも連携ができます。合いそうだな、というプロジェクトがあれば比較検討してみるのもよいのではないでしょうか。

次回は、アクセスログの出力・確認や、Cloudflareのほかのサービスとの連携について取り上げます。