🌟

各ホスティングサービスでNext.jsを静的ファイルとして配信できるか検証してみた

に公開

こんにちは!CastingONEの大沼です。

始めに

Next.jsは基本SSRですが、サーバーの運用コストを考えて静的ファイルとして出力して配信することもできます。しかしこの静的ファイルは動的パラメータが入っている場合は厄介で、単純にoutput: 'export'を設定しただけだと[id].tsxというページファイルは[id]/index.htmlという形でHTMLファイルが出力され、このファイルを返すように適切にURLとマッピングする必要があります。
こういったURLのマッピング設定をリライトと言いますが、各ホスティングサービスでこの設定ができればNext.jsでもSPAっぽく動かすことができるため、それぞれ検証してみました。検証したサービスは以下になります。

検証内容

検証に使ったNext.jsアプリは以下のリポジトリに書きました。こちらに各サービスのホスティング設定も書かれています。

https://github.com/TakanoriOnuma/trial-nextjs-pages-ssg

各ホスティングサービスのデプロイ先は以下になりますので、動作を確認したい方はご参照ください(アプリの内容は全く同じです)

各サービスでNext.jsを静的ファイルとして配信するための設定

Next.jsアプリを静的ファイルとして配信する場合は配信側のリライト設定が重要になるので、その設定を各サービスではどのように行うかを重点的に説明します。それ以外のデプロイするための設定については割愛します。

Netlify

Netlifyのリライト設定は非常にシンプルで、_redirectsというファイルをルートディレクトリに配置するとリライトします。

_redirects
/dynamic/:id/ /dynamic/[id]/index.html 200

Vercel

Vercelはvercel.jsonrewritesでリライト設定します。またframeworkはnullにしておくのも重要です。nextjsを設定してしまうとNext.jsのrewritesの設定を見ようとするのかvercel.jsonのリライト設定を見てくれなくなったのでnullにしておきます。

vercel.json(リライト設定で重要な部分をハイライト)
 {
   "$schema": "https://openapi.vercel.sh/vercel.json",
+  "framework": null,
   "installCommand": "",
   "buildCommand": "",
   "outputDirectory": "out",
+  "rewrites": [
+    {
+      "source": "/dynamic/:id/",
+      "destination": "/dynamic/[id]/index.html"
+    }
+  ]
 }

Cloudflare

Cloudflareは最初Cloudflare Pagesで_redirectsファイルを置いてリライトさせるつもりでしたが何故か動かなかったので、Cloudflare Workersの方でリライトを実装しました。
まずはWorkerの設定ファイルを以下のように用意します。workerは自前でリクエストをかなり自由にハンドリングできますが、今回は動的パラメータのリライト部分だけハンドリングしたいので、run_worker_firstで動的パラメータとなるパス部分だけ指定し、それ以外のルーティングはデフォルトで用意されているnot_found_handring: '404-page'html_handling: 'auto-trailing-slash'を設定しました。
run_worker_firstは未指定でも動作はしますが、全てのファイルで必ず実行することになってコストがかかってしまうので基本的には絞っておいた方が良いと思います。

wrangler.jsonc(リライト設定で重要な部分をハイライト)
 {
   "$schema": "node_modules/wrangler/config-schema.json",
   "name": "trial-nextjs-pages-ssg-workers",
+  "main": "worker.ts",
   "compatibility_date": "2025-08-11",
+  "assets": {
+    "directory": "out",
+    "binding": "ASSETS",
+    "not_found_handling": "404-page",
+    "html_handling": "auto-trailing-slash",
+    "run_worker_first": ["/dynamic/*/"]
+  },
   "observability": {
     "logs": {
       "enabled": true
     }
   }
 }

workerの処理は以下のようにリクエストのURLを正規表現でチェックしてマッチした時はURLを書き換えて静的ファイルへの取得に繋げています。この時重要なのはリライトしたURLはそのままURLオブジェクトを渡すのではなく、.toString()して文字列にして渡します。これをやらないとリクエスト元とURLが変わったと判断されてリダイレクトされてしまいます。

worker.ts
import { WorkerEntrypoint } from "cloudflare:workers";

class Worker extends WorkerEntrypoint<Env> {
  async fetch(request: Request) {
    // リライトの設定
    const url = new URL(request.url);
    const match = url.pathname.match(/^\/dynamic\/([^/]+)\/?$/);

    if (match) {
      const assetUrl = new URL("/dynamic/[id]/index.html", request.url);
      // toStringで渡すことでリクエスト元と比較されなくなり、リダイレクトせずに静的アセットを返してくれる
      return this.env.ASSETS.fetch(assetUrl.toString());
    }

    // デフォルトは静的アセット返す
    return this.env.ASSETS.fetch(request);
  }
}

export default Worker;

Firebase

FirebaseはFirebase Hostingを使ってVercelと同じようにrewritesの部分にリライトを設定しました。

firebase.json(リライト設定で重要な部分をハイライト)
 {
   "hosting": {
     "public": "out",
     "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
+    "rewrites": [
+      {
+        "source": "/dynamic/:id/",
+        "destination": "/dynamic/[id]/index.html"
+      }
+    ]
   }
 }

終わりに

以上が各ホスティングサービスでNext.jsを静的ファイルとして配信できるかの検証結果でした。Cloudflareだけかなり苦戦しましたが、今回試したサービスは全て動かすことができました。
Next.jsを静的ファイルとして配信したい人の参考になれば幸いです。

Discussion