1. ホーム
  2. Next.js

【Next.js】output: ‘export’ で静的サイトを書き出す方法|設定手順と使えなくなる機能を解説

Share

Next.js で作ったサイトを、Node.js のサーバーを用意せずに公開したいことがあります。そんなときに使うのが next.config.tsoutput: 'export'、いわゆる静的エクスポート(Static Exports)です。設定を1行足して next build を実行すると、サイト全体が HTML・CSS・JS のファイルとして書き出され、S3 や GitHub Pages、Netlify、レンタルサーバーなどにそのまま置けるようになります。この記事では App Router を前提に、設定手順から使えなくなる機能とその理屈、ビルドが失敗したときの対処までを解説します。

静的エクスポートは「サーバーのいらない Next.js」

通常の Next.js は、next build したあとに next start で Node.js のサーバーを立ち上げて配信します。リクエストが届くたびにサーバー側でコードが動くため、ログイン中のユーザーごとに表示を変えたり、フォームの送信を受け取ったりといったことができます。

静的エクスポートはこの前提を捨てる選択です。output: 'export' を指定すると、Next.js はルートごとに1つの HTML ファイルを生成し、必要な CSS・JS と一緒に out ディレクトリへ書き出します。あとはそのフォルダの中身を、静的ファイルを配信できるサーバーへアップロードするだけです。Node.js のプロセスを常駐させる必要がないので、運用がぐっと単純になり、ホスティング費用も抑えられます。

ここで一番大事なのが、公開後の環境には「リクエストのときに動くサーバー」が存在しないという点です。この記事で後述する制約はすべて、この一点から導かれます。逆に言えば、コーポレートサイトやドキュメント、ブログのようにコンテンツがビルド時に確定するサイトなら、静的エクスポートは非常に相性が良い構成です。

next.config.ts に output: ‘export’ を書く

設定は next.config.ts(JavaScript なら next.config.js)に output: 'export' を追加するだけです。専用のコマンドはありません。かつて存在した next export コマンドは Next.js 14 で削除され、この設定に置き換わっています。古い記事を参考にするときは注意してください。

next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  // サイト全体を静的な HTML・CSS・JS として書き出す
  output: 'export',
};

export default nextConfig;

この状態で next build を実行すると、プロジェクト直下に out ディレクトリが作られます。出力先を変えたい場合は distDir で指定できます。

ターミナル
# 静的ファイルを out ディレクトリに書き出す
npm run build

# 書き出された中身を確認する
ls out

ルートが //blog/[id] の2つだけのサイトなら、出力はおおよそ次のような形になります。ページごとに HTML ファイルが1つずつ並び、404.html も一緒に生成されます。

out ディレクトリの中身(例)
out/
├── index.html          # / のページ
├── 404.html            # 見つからないときのページ
├── blog/
│   ├── post-1.html     # /blog/post-1 のページ
│   └── post-2.html     # /blog/post-2 のページ
└── _next/              # CSS・JS などのアセット

手元で表示を確かめるときは、next start ではなく静的ファイルサーバーを使います。next start.next の成果物を配信する Next.js のサーバーなので、静的エクスポートの構成では使いません。serve のようなツールで out を配信するのが手軽です。

ターミナル
# out ディレクトリをローカルで配信して確認する
npx serve out

なお out はビルドの成果物なので、Git で管理する必要はありません。.gitignore/out/ を追加しておきましょう。

動的ルートは generateStaticParams で全パスを列挙する

app/blog/[slug]/page.tsx のような動的ルートは、URL の一部が変わるページをまとめて1つのファイルで書ける仕組みです。通常の構成ならリクエストが来た時点で slug の値を受け取ってページを組み立てられますが、静的エクスポートではそれができません。ビルドの時点で、どの URL の HTML を作るべきかが決まっていなければならないからです。

そこで generateStaticParams を使い、生成したいパスをすべて列挙します。この関数が返した配列の数だけ HTML が書き出されます。

app/blog/[slug]/page.tsx
// ビルド時に生成するパスを列挙する
export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/posts');
  const posts: { slug: string }[] = await res.json();

  // [{ slug: 'post-1' }, { slug: 'post-2' }] のような配列を返す
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;

  return <h1>{slug}</h1>;
}

ここで列挙しなかったパスの HTML は作られないため、公開後にアクセスすると 404 になります。記事が増えたら再ビルドして out を差し替える、という運用になる点は押さえておいてください。また、動的ルートで dynamicParamstrue にする(列挙外のパスを実行時に生成する)設定は、静的エクスポートでは使えません。

使えなくなる機能と、その理由

静的エクスポートで使えないのは、要するにリクエストが届いた瞬間にサーバー側で何かをする機能です。公開されているのはただのファイルなので、実行する場所がありません。公式ドキュメントが挙げている非対応機能を、理由と合わせて整理します。

機能使えない理由
Server Actionsフォーム送信などを受け取って処理するサーバーが存在しない
Route Handlers でリクエストを読むものGET は静的なファイルとして書き出されるが、リクエストの内容に応じた応答は返せない
cookies() などの動的な関数リクエストヘッダーを読む処理はビルド時には結果が決まらない
ISR(Incremental Static Regeneration)一定時間後にサーバー側でページを再生成する仕組みなので成立しない
middleware / proxyリクエストの前段で割り込む処理を実行する場所がない
redirects / rewrites / headersこれらはサーバーがリクエストを見て振り分ける設定なので反映されない
Draft Mode下書きプレビューの切り替えをサーバー側で判定できない
Intercepting Routes遷移をサーバー側で解釈して差し替える仕組みが使えない
generateStaticParams の無い動的ルート生成すべき URL がビルド時に確定しない
next/image の既定の最適化画像をリクエスト時に変換するサーバーがない(後述の設定が必要)

redirectsheaders は、代わりにホスティング側の機能で設定します。Netlify や Cloudflare Pages なら専用の設定ファイル、Nginx なら設定ファイルの記述、S3 と CloudFront なら CloudFront 側の機能、という具合です。「Next.js の設定でできないから諦める」のではなく、配信するレイヤーへ移すという考え方になります。

一方、これらの機能を使ったまま next dev で開発しようとすると、その時点でエラーになります。ルートレイアウトに export const dynamic = 'error' を書いたときと同じ扱いで、動的な処理に触れた瞬間に知らせてくれる仕組みです。デプロイ直前に大量の問題が発覚する事態を避けられるので、静的エクスポートにするなら早い段階で設定しておくのがおすすめです。

逆に、そのまま使えるもの

制約を並べると窮屈に見えますが、App Router の中心的な機能はほとんどそのまま使えます。まずServer Components はビルド中に実行されます。コンポーネントの中で fetch して CMS や API からデータを取り、それを HTML に描き込むという書き方は問題なく動きます。従来の静的サイト生成と同じイメージです。

app/page.tsx
export default async function Page() {
  // この fetch は next build のときにサーバー(ビルド環境)で実行される
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();

  return (
    <main>
      {posts.map((post: { id: string; title: string }) => (
        <h2 key={post.id}>{post.title}</h2>
      ))}
    </main>
  );
}

クライアントコンポーネントも通常どおり使えます。ビルド時に HTML へプリレンダリングされたうえで、ブラウザ側で JavaScript が動きます。表示するデータをブラウザから取得したい場合は、クライアントコンポーネントの中で SWR などを使って fetch すれば、公開後の最新データを反映できます。ただし windowlocalStorage はサーバー(ビルド時)には存在しないので、useEffect の中で触るようにしてください。

next/link によるページ遷移もそのまま機能し、遷移はクライアント側で行われます。静的な metadata の出力、sitemap.tsrobots.ts によるファイル生成、GET だけの Route Handlers(app/data.json/route.ts のように JSON ファイルを書き出す用途)も使えます。layout.tsxloading.tsx といったファイル規約も変わりません。

next/image は設定を足す必要がある

next/image は既定では、リクエストに応じてサイズや形式を変換した画像を返すサーバー側の仕組み(既定の loader)を使います。静的エクスポートではこれが動かないため、設定を変えないとビルドでエラーになります。方法は2つです。

1つは最適化をやめることです。images.unoptimizedtrue にすると、next/image は元の画像をそのまま配信します。Image コンポーネントのレイアウト面の利点は残るので、画像点数が少ないサイトならこれで十分なことも多いです。

next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  output: 'export',
  images: {
    // 画像の最適化を行わず、元のファイルをそのまま配信する
    unoptimized: true,
  },
};

export default nextConfig;

もう1つは、最適化を外部サービスに任せる方法です。images.loader'custom' にし、URL を組み立てる関数を loaderFile で指定します。Cloudinary や imgix のような画像配信サービスを使えば、静的エクスポートでもサイズごとの最適化された画像を配信できます。

next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  output: 'export',
  images: {
    loader: 'custom',
    loaderFile: './my-loader.ts',
  },
};

export default nextConfig;

loaderFile に指定するファイルでは、画像のパスと幅・品質を受け取って配信 URL を返す関数を default export します。下は Cloudinary の例です。

my-loader.ts
export default function cloudinaryLoader({
  src,
  width,
  quality,
}: {
  src: string;
  width: number;
  quality?: number;
}) {
  const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`];

  return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`;
}

trailingSlash と .html 拡張子でつまずかないために

静的エクスポートで意外にはまりやすいのが URL の形です。既定では /about というルートは out/about.html というファイルとして書き出されます。ブラウザからアクセスされる URL は /about(拡張子なし)なので、配信側が「/about というリクエストが来たら about.html を返す」と解釈してくれないと 404 になります。

この解釈をしてくれるホスティングもありますが、してくれないものもあります。Nginx で自分で配信するなら、拡張子を補って探す設定を書きます。

nginx.conf
server {
  listen 80;
  server_name example.com;

  root /var/www/out;

  # /about → about.html のように拡張子を補って探す
  location / {
    try_files $uri $uri.html $uri/ =404;
  }

  error_page 404 /404.html;
}

もう1つの解決策が trailingSlash: true です。これを有効にすると、リンクが /me ではなく /me/ の形になり、出力も me.html ではなく me/index.html になります。ディレクトリ名でアクセスされたら index.html を返すのはどのサーバーでも標準的な挙動なので、ホスティング側の設定を書かずに済むことが多いのが利点です。S3 の静的ウェブサイトホスティングのように、拡張子の補完をしてくれない環境ではこちらが素直です。

next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  output: 'export',

  // /me → /me/ になり、出力は me/index.html になる
  trailingSlash: true,

  // /me → /me/ への自動リダイレクトをやめ、href をそのまま保つ
  // skipTrailingSlashRedirect: true,
};

export default nextConfig;

末尾スラッシュの有無は URL そのものを変えるので、公開後に切り替えると既存の URL が変わってしまいます。SEO への影響を避けるため、公開前に決めておくのが安全です。GitHub Pages のようにサブディレクトリ配下で公開する場合は、アセットのパスがずれないよう basePath の設定も必要になります。公式が用意しているデプロイ用テンプレートを参照すると設定を確認しやすいです。

ビルドが失敗するとき

output: 'export' を追加した直後のビルドは、まず失敗するものと考えておくとよいです。今まで動的に処理していた部分が一気に洗い出されるためです。よくある3つのパターンを見ておきます。

動的ルートに generateStaticParams が無いと言われる

Page "/blog/[slug]" is missing "generateStaticParams()" のように、対象のルートと足りない関数名を挙げてビルドが止まります。エラーメッセージにルートのパスが出るので、そのディレクトリの page.tsxgenerateStaticParams を追加してください。データ元がまだ用意できていない段階なら、暫定的に配列を直接返す形でも先に進めます。

画像の最適化が使えないと言われる

next/image を使っているのに images の設定をしていない場合、既定の loader は静的エクスポートで使えないという内容のエラーが出ます。前の章で説明した images.unoptimized: true か、loader: 'custom'loaderFile の指定を追加すれば解決します。

動的な機能に触れているとエラーになる

cookies() を呼んでいたり、Server Actions が残っていたり、middleware のファイルが置かれていたりすると、その箇所を指してエラーになります。メッセージには対象のファイルや関数名が含まれているので、そこから順に潰していく形になります。

直し方は機能ごとに変わります。ユーザーごとの表示切り替えなら、ブラウザ側で fetch するクライアントコンポーネントに寄せます。フォーム送信は Server Actions ではなく、外部のフォームサービスや別途用意した API へ送る形にします。認証やアクセス制御は、ホスティング側の機能に任せるか、そもそも静的エクスポートの対象から外すかを検討してください。

もし置き換えが難しい機能が多いなら、無理に静的化せず通常のビルドに戻すという判断も現実的です。Docker などで自分でサーバーを動かしたい場合は output: 'standalone'、Vercel のようなプラットフォームなら設定なしのままデプロイできます。「サーバーが本当に不要か」を基準に選ぶとよいでしょう。

まとめ

next.config.tsoutput: 'export' を書いて next build を実行すると、サイトが HTML・CSS・JS として out ディレクトリに書き出され、Node.js のサーバーなしで静的ホスティングに置けるようになります。ローカル確認は next start ではなく npx serve out のような静的ファイルサーバーで行い、動的ルートは generateStaticParams で生成するパスをすべて列挙します。

使えなくなるのは、Server Actions、リクエストを読む Route Handlers、cookies()、ISR、middlewareredirectsheaders、Draft Mode、next/image の既定の最適化といった、リクエスト時にサーバーで動く機能です。理由はすべて同じで、公開後にコードを実行する場所が無いからです。逆に Server Components によるビルド時のデータ取得、クライアントコンポーネント、静的な metadatasitemap.ts などは問題なく使えます。URL の形は trailingSlash とホスティング側のルーティング設定に関わるので、公開前に決めておいてください。

参考ページ