「トップページではなく https://example.com/docs のようなサブディレクトリでアプリを公開したい」「JavaScript や CSS だけを CDN から配信して負荷を下げたい」——このどちらも next.config.js の設定で対応できます。前者が basePath、後者が assetPrefix です。名前が似ていて役割も少し重なるため混同されがちですが、変わるものがはっきり違います。この記事では2つの設定の違いと書き方、そして「サブディレクトリに置いたら CSS が当たらない・画像が404になる」というよくあるつまずきの原因を、App Router の構成を前提に解説します。
目次
basePath と assetPrefix は何が違うのか
basePath は、アプリ全体をドメインのサブパス配下で配信するための設定です。'/docs' と指定すると、これまで /about だったページは /docs/about で配信されるようになります。URL そのものが変わるので、ルーティングもリンクもアセットの位置も、まとめて接頭辞付きになります。
一方の assetPrefix は、ビルド済みアセットの配信元だけを差し替える設定です。Next.js がビルドで生成する /_next/ 以下の JavaScript と CSS を、アプリ本体とは別のドメイン(多くの場合 CDN)から読み込ませます。ページの URL は一切変わりません。あくまで HTML の中に書き出される <script src="..."> や <link href="..."> の向き先が変わるだけです。
| 観点 | basePath | assetPrefix |
|---|---|---|
| 目的 | アプリ全体をサブディレクトリ配下で配信する | ビルド済みアセットを CDN など別ドメインから配信する |
| 設定値の例 | '/docs' | 'https://cdn.example.com' |
| ページの URL | /docs/about のように変わる | 変わらない |
| ルーティング | 接頭辞込みで受け付けるようになる | 影響しない |
next/link の href | 自動で接頭辞が付く | 影響しない |
/_next/ の JS・CSS | 同じドメインの接頭辞付きパスになる | 指定したドメインから読み込まれる |
public のファイル | 配信 URL に接頭辞が付く(手書きのパスは自分で付ける) | 対象外。自分で前置する必要がある |
| 反映のタイミング | ビルド時に固定される | ビルド時に固定される |
公式ドキュメントでも、サブパスでのホスティングが目的なら assetPrefix ではなく basePath を使うよう案内されています。assetPrefix はあくまで CDN 用の設定だと考えてください。なお Vercel にデプロイする場合は CDN が自動で構成されるため、assetPrefix を自分で設定する必要はありません。
basePath でサブディレクトリ配下に配信する
設定は next.config.js に1行足すだけです。既定値は空文字列(=ドメイン直下)なので、そこにパスの接頭辞を書きます。
/** @type {import('next').NextConfig} */
const nextConfig = {
// https://example.com/docs 配下で配信する
basePath: '/docs',
};
module.exports = nextConfig;
TypeScript の next.config.ts でも書き方は同じです。設定を保存したら開発サーバーを再起動し、http://localhost:3000/docs を開いてトップページが表示されれば成功です。http://localhost:3000/ のほうは404になります。接頭辞を付けたのですから、ルート直下にはもうページが存在しないわけです。
next/link のリンクには自動で接頭辞が付く
basePath の便利なところは、next/link のリンクに接頭辞が自動で付くことです。アプリのコード側は今まで通り /about と書いておけば、出力される HTML では /docs/about になります。
import Link from 'next/link';
export default function HomePage() {
// basePath を意識せず、アプリ内のパスをそのまま書く
return <Link href="/about">会社概要</Link>;
}
<a href="/docs/about">会社概要</a>
おかげで basePath を /docs から /help に変えたくなっても、アプリ内のリンクを書き換える必要がありません。逆に言うと、気を利かせて href="/docs/about" と書いてはいけないということでもあります。そう書くと接頭辞がもう一度付いて /docs/docs/about になり、リンク先が404になります。
先頭の / は必須、末尾の / は付けない
basePath の値には書式の決まりがあります。必ず / で始めること、そして末尾に / を付けないことです。'docs' や '/docs/' と書くと、Next.js が設定を読み込む時点でエラーになって起動できません。値は「空文字列か、/ で始まるパスの接頭辞」と覚えておけば間違いません。
ドメインを含む URL(https://example.com/docs のような値)も指定できません。basePath はあくまで自分自身のパスの接頭辞であって、配信元を変える設定ではないからです。別ドメインから配信したいのであれば、それは assetPrefix の役目です。
値はビルド時に埋め込まれ、実行時には変えられない
もうひとつ重要なのが、basePath はビルド時に決まる値で、ビルドし直さない限り変更できないという点です。この値はクライアント側のバンドルにそのまま埋め込まれるため、後から環境変数を差し替えても反映されません。
実務では、これは「同じビルド成果物を、接頭辞の違う複数の環境に使い回すことはできない」という制約になります。ステージングは /preview、本番は /docs、といった構成にしたい場合は、環境ごとに別々にビルドする必要があります。Docker イメージを1つ作って複数環境に配る運用とは相性が悪いので、設計の段階で意識しておくとよいでしょう。
手書きのパスには basePath が付かない
ここが basePath でいちばんつまずくところです。接頭辞が自動で付くのは next/link のようにルーティングを Next.js が管理している部分だけで、自分で文字列として書いたパスは対象外です。
たとえば public/logo.png に置いた画像は、basePath: '/docs' の環境では /docs/logo.png で配信されます。それなのにコードには <img src="/logo.png"> と書いてある——このズレが、画像だけ404になる典型的なパターンです。同じことが CSS の url('/bg.png')、<a href="/terms"> のような素のアンカータグ、そして fetch('/api/posts') にも当てはまります。どれも Next.js の API ではないので、書いたパスがそのまま使われます。
next/image の src にも自分で書く必要がある
意外に思われるかもしれませんが、next/image の src も自動では補われません。公式ドキュメントでも、basePath を設定した場合は src の前に接頭辞を書くよう明記されています。
import Image from 'next/image';
export default function Home() {
return (
<Image
// basePath が '/docs' なら src にも /docs を付ける
src="/docs/me.png"
alt="著者の写真"
width={500}
height={500}
/>
);
}
とはいえ、こうして接頭辞を直書きしていくと、basePath を変えたときに全ファイルを grep して回るはめになります。せっかくリンクは自動化されているのに、画像だけ手作業というのも不格好です。そこで、接頭辞を1か所にまとめておく方法をとります。
NEXT_PUBLIC_BASE_PATH に切り出して1か所で管理する
実践的なのは、接頭辞を環境変数として定義し、next.config.js とコンポーネントの両方から同じ値を参照する形です。ブラウザ側のコードでも読めるように、名前は NEXT_PUBLIC_ で始めます。
# サブパスで配信しないときは空にしておく
NEXT_PUBLIC_BASE_PATH=/docs
/** @type {import('next').NextConfig} */
const nextConfig = {
// 未設定なら空文字列(=ドメイン直下)にフォールバック
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
};
module.exports = nextConfig;
この書き方は .env の値が設定ファイルの評価時に読み込まれていることが前提です。うまく反映されない環境では、ビルドコマンドに直接渡す NEXT_PUBLIC_BASE_PATH=/docs npm run build の形にすれば確実です。
あとはコンポーネント側から同じ値を参照します。毎回 process.env を書くより、小さなヘルパーを1つ用意しておくほうが読みやすくなります。
// process.env.NEXT_PUBLIC_XXX はビルド時に値へ置き換えられる。
// 分割代入や動的なキー参照にすると置換されないので、この形で書く。
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
/** public 配下のファイルへのパスに basePath を付ける */
export function assetPath(path: string): string {
return `${basePath}${path}`;
}
import Image from 'next/image';
import { assetPath } from '@/lib/asset-path';
export default function Home() {
return (
<>
{/* next/image も素の img も、同じヘルパーを通す */}
<Image
src={assetPath('/me.png')}
alt="著者の写真"
width={500}
height={500}
/>
<img src={assetPath('/logo.png')} alt="ロゴ" width={120} height={40} />
</>
);
}
コードのコメントにも書いた通り、process.env.NEXT_PUBLIC_BASE_PATH はビルド時に文字列へ置き換えられる仕組みです。const { NEXT_PUBLIC_BASE_PATH } = process.env のように分割代入したり、process.env[name] のように変数でキーを指定したりすると置換の対象外になり、ブラウザ側で undefined になります。必ず process.env.変数名 の形をそのまま書いてください。
なお、fetch で自分の Route Handlers を呼ぶ場合も同じヘルパーが使えます。fetch(assetPath('/api/posts')) のようにしておけば、サブパス配信に切り替えたときにリクエスト先がずれません。
assetPrefix で _next/static を CDN から配信する
assetPrefix にドメインを指定すると、Next.js が /_next/ 以下から読み込む JavaScript と CSS の URL がそのドメインに差し替わります。たとえば通常は次のようなリクエストになるチャンクが、
/_next/static/chunks/4b9b41aaa062cbbfeff4add70f256968c51ece5d.4d708494b3aed70c04f0.js
https://cdn.example.com/_next/static/chunks/4b9b41aaa062cbbfeff4add70f256968c51ece5d.4d708494b3aed70c04f0.js
のように、CDN 経由になります。パス部分(/_next/static/...)は変わらず、前に付くオリジンだけが差し替わるのがポイントです。CDN 側は、このドメインを Next.js がホストされているドメインに解決するオリジンとして設定します。
開発時は assetPrefix を付けない
ローカル開発中に CDN のドメインを向いていると、まだアップロードされていないファイルを取りに行ってしまい何も動きません。そこで、開発サーバーのときだけ assetPrefix を無効にする条件分岐を入れます。公式ドキュメントでは、設定を関数として書いてフェーズで判定する方法が紹介されています。
// @ts-check
import { PHASE_DEVELOPMENT_SERVER } from 'next/constants';
export default (phase) => {
// next dev で起動されたときだけ true になる
const isDev = phase === PHASE_DEVELOPMENT_SERVER;
/** @type {import('next').NextConfig} */
const nextConfig = {
// 開発時は undefined(=自分のドメインから読み込む)
assetPrefix: isDev ? undefined : 'https://cdn.example.com',
};
return nextConfig;
};
設定オブジェクトをそのまま export する代わりに、フェーズを受け取る関数を export しているのが違いです。PHASE_DEVELOPMENT_SERVER は next/constants から読み込みます。フェーズを使わず process.env.NODE_ENV !== 'production' で分岐しても同じような結果になりますが、next build と next dev をはっきり区別できる分、フェーズで判定するほうが素直です。
CDN にアップロードするのは .next/static だけ
CDN 側の準備で注意したいのが、アップロードする対象です。必要なのは .next/static/ の中身だけで、これを _next/static/ という名前で公開します。ディレクトリ名が .next から _next に変わる点に気をつけてください。
そして、.next/ フォルダの残りをアップロードしてはいけません。この中にはサーバー側のコードやビルド設定が入っており、公開すると内部構造をそのまま外に晒すことになります。同期の対象は必ず static ディレクトリに限定してください。
public フォルダのファイルは対象外
assetPrefix が面倒を見てくれるのは _next/static へのリクエストだけです。public フォルダに置いた画像やフォント、PDF などは対象外なので、これらも CDN から配信したいなら自分で接頭辞を付ける必要があります。basePath のときと同じく、値を環境変数に切り出してヘルパー関数を通す形にしておくと管理しやすくなります。
また、別ドメインから配信することで新たに出てくる問題もあります。代表的なのが Web フォントで、フォントファイルの読み込みはブラウザが CORS を要求するため、CDN 側で Access-Control-Allow-Origin を返す設定が必要になります。CSS や画像は問題なく表示されているのにフォントだけ適用されない、という場合はここを疑ってください。
サブパスに置いたら画像が404になるとき
サブディレクトリ配信に切り替えた直後は、「HTML は表示されるのに CSS が当たっておらず、画像も出ない」という状態になりがちです。原因はいくつかありますが、切り分けの手順はどれも同じで、開発者ツールのネットワークタブを開いて404になっているリクエストの URL を見ることから始めます。そこに接頭辞が付いているかどうかで、どの原因かがだいたい判別できます。
basePath を設定せずにサブディレクトリへ置いている
いちばん多いのがこれです。ビルド成果物をサーバーの /docs ディレクトリにコピーしただけで、next.config.js には何も書いていないケースです。この場合、HTML の中には /_next/static/... というルート直下を指すパスが書き出されているため、ブラウザは https://example.com/_next/static/... を取りに行って404になります。CSS も JavaScript も読み込めないので、スタイルの当たっていない素の HTML が表示されます。
404になっている URL に /docs が付いていなければ、この原因です。basePath: '/docs' を設定してビルドし直せば解決します。ファイルを置く場所と basePath の値は必ず一致させてください。/docs に置くなら basePath も /docs、というシンプルな対応関係です。
public の画像を / 始まりのまま書いている
CSS は当たっているのに画像だけ出ない、という場合はこちらです。/_next/static/... のリクエストには接頭辞が付いているのに、/logo.png だけ付いていない——ネットワークタブを見ればすぐ分かります。前述の通り、<img src="/logo.png"> や CSS の url('/bg.png')、next/image の src は自動で補われないためです。
対処は、接頭辞を付けたヘルパー経由に書き換えることです。CSS の中の url() は JavaScript から書き換えられないので、こちらは相対パス(url('../images/bg.png') のように CSS ファイルからの相対で書く)にするか、画像を public ではなくソースコード側に置いてバンドラに解決させると、接頭辞の影響を受けなくなります。
設定を変えたのにビルドし直していない
basePath も assetPrefix もビルド時に値が埋め込まれる設定です。next.config.js を書き換えただけでは、すでに出力済みの .next の中身は古いままです。本番なら next build からやり直し、開発中なら開発サーバーを一度止めて起動し直してください。設定ファイルは起動時にしか読まれないので、保存しただけでは反映されません。
CI でビルドしている場合は、環境変数がビルドのステップにきちんと渡っているかも確認しましょう。NEXT_PUBLIC_BASE_PATH を実行時のコンテナにだけ設定していて、ビルド時には空だった、というのはよくある行き違いです。
手前のプロキシがパスを削っている
Nginx などのリバースプロキシを前段に置いている構成では、プロキシ側の設定と basePath がかみ合っていないことがあります。basePath: '/docs' を設定した Next.js は /docs/about というリクエストを受け取る前提で動きます。ところがプロキシが接頭辞を取り除いて /about として転送していると、Next.js から見れば知らないパスなので404を返します。
ローカルでは正常なのに本番だけおかしい、という場合はここを疑ってください。切り分けには、プロキシを通さずアプリのポートへ直接アクセスして確認するのが手っ取り早い方法です。直接なら表示されるのであれば、原因はアプリではなく前段の転送設定にあります。
まとめ
basePath はアプリ全体をサブパス配下で配信するための設定で、値は / で始めて末尾には付けません。設定すると next/link のリンクには自動で接頭辞が付くので、アプリ内のパスは今まで通りルート起点で書けます。逆に自分で /docs/about と直書きすると二重に付いてしまうので注意してください。
自動で付かないのは、public の画像を <img src="/logo.png"> と手書きした場合や、next/image の src、CSS の url()、素の fetch のように Next.js のルーティングを経由しないパスです。接頭辞を NEXT_PUBLIC_BASE_PATH のような環境変数に切り出し、小さなヘルパー関数を通して組み立てるようにしておけば、設定を変えたときの修正が1か所で済みます。
assetPrefix は /_next/ 以下の JavaScript と CSS の配信元だけを差し替える設定で、ページの URL は変わりません。CDN にアップロードするのは .next/static/ の中身だけを _next/static/ として、それ以外の .next/ は公開しないでください。開発サーバーでは PHASE_DEVELOPMENT_SERVER で判定して無効にします。どちらの設定もビルド時に値が固定されるため、変更したら必ずビルドし直すこと。これを覚えておくだけで、無駄な調査時間をかなり減らせます。