ヘッドレス CMS や画像 CDN に置いた画像を next/image で表示しようとしたら、いきなりエラーで真っ白になった。Next.js を触っていると必ず一度は通る場面です。原因は next.config.js(next.config.ts)の images.remotePatterns が設定されていないことで、これは不具合ではなく意図された制限です。この記事では、なぜその設定が必要なのか、各フィールドがどこと照合されるのか、ワイルドカードの * と ** をどう使い分けるのかを整理し、ヘッドレス CMS や Cloudinary・S3 を使うときの現実的な書き方、設定したのにエラーが消えないときの確認点まで解説します。
目次
外部ドメインの画像は許可リストがないと表示できない
まずは、何も設定せずに外部 URL を src に渡したときの挙動を見てみます。
import Image from 'next/image';
export default function Page() {
return (
<Image
src="https://images.example.com/photos/cat.jpg" // 外部ドメインの画像
alt="ねこの写真"
width={800}
height={600}
/>
);
}
このコードは、images.remotePatterns に該当する設定がない状態では次のエラーになります。
Error: Invalid src prop (https://images.example.com/photos/cat.jpg) on `next/image`,
hostname "images.example.com" is not configured under images in your `next.config.js`
See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host
「わざわざ設定させないで、外部画像なら全部通してくれればいいのに」と感じるかもしれません。しかしここには理由があります。next/image は画像をそのまま貼るのではなく、自前の画像最適化 API(既定では /_next/image)を経由させ、サーバー側でリサイズやフォーマット変換をしてから配信します。つまり src に渡した URL は、あなたのサーバーが取得しにいく先になります。
もし任意の URL を無制限に受け付けたら、第三者があなたのサイトの画像最適化 API に好きな画像 URL を渡せてしまいます。関係のない巨大な画像を延々と変換させてサーバーの CPU と帯域を消費させる、いわば他人の画像配信を自分のサーバー費用で肩代わりさせられる状態になります。そのため Next.js は、あらかじめ許可した URL のパターンにマッチしないリクエストは 400 Bad Request で拒否するという設計になっています。許可リストを書くのが images.remotePatterns です。
remotePatterns の基本の書き方
remotePatterns は next.config の images の下に書く配列です。1 つの要素が 1 つの許可パターンで、どれかにマッチすれば通ります。TypeScript の next.config.ts なら次のようになります。
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
port: '', // 既定ポート(443)なら空文字
pathname: '/photos/**', // /photos/ 以下だけ許可
search: '', // クエリ文字列は付けさせない
},
],
},
};
export default nextConfig;
JavaScript の next.config.js でも中身は同じです。
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
port: '',
pathname: '/photos/**',
search: '',
},
],
},
};
この設定で許可されるのは、https://images.example.com/photos/ で始まり、かつクエリ文字列を持たない URL だけです。プロトコル・ホスト名・ポート・パスのどれか 1 つでも外れたリクエストは 400 Bad Request になります。判定は完全一致かつ大文字小文字を区別するので、なんとなく似ているだけでは通りません。
そしてもう 1 つ、最初に覚えておいてほしいことがあります。next.config を書き換えたら開発サーバーを再起動することです。この設定はサーバー起動時に読み込まれるため、ファイルを保存しただけではホットリロードで反映されません。設定は正しいのにエラーが変わらない、という相談のかなりの割合がこれです。
各フィールドは URL のどこと照合されるのか
remotePatterns の 1 要素は、URL を部品に分解したものだと考えると理解しやすくなります。たとえば https://images.example.com:8443/photos/cat.jpg?v=2 という URL は、プロトコル・ホスト名・ポート・パス・クエリ文字列の 5 つに分かれます。各フィールドはそれぞれの部品と照合されます。
| フィールド | 意味と書き方 |
|---|---|
protocol | 'http' または 'https'。src のプロトコルと厳密に一致する必要があり、https の画像を http の設定で通すことはできません。 |
hostname | ホスト名。'images.example.com' のようにプロトコルもスラッシュも含めず書きます。先頭にワイルドカードを置いてサブドメインをまとめて許可できます。 |
port | ポート番号の文字列。80 / 443 のような既定ポート、つまり URL にポートが現れない場合は空文字 '' にします。http://localhost:3000 のような URL では '3000' が必要です。 |
pathname | パス部分。'/photos/**' のように先頭のスラッシュから書き、末尾にワイルドカードを置いて範囲を決めます。 |
search | クエリ文字列。先頭の ? を含めた完全一致で、ワイルドカードは使えません。'' にするとクエリ付きの URL を拒否します。 |
ここで注意したいのが省略時の扱いです。protocol・port・pathname・search を書かないと、そのフィールドは **(何でも許可)として扱われます。動かすだけなら hostname だけ書けば済むのですが、公式ドキュメントでもこれは推奨されていません。意図しない URL まで最適化できてしまい、許可リストを書いた意味が薄れます。面倒でも protocol と pathname は明示しておくのが安全です。
search はやや特殊で、指定するなら完全一致になります。たとえばキャッシュ対策のバージョン番号が固定なら、search: '?v=1727111025337' のように書けばその URL だけを通せます。逆にクエリ文字列が画像ごとに変わる場合、search を書いてしまうと通らなくなるので、あえて省略してすべてのクエリを許可する形にします。
ワイルドカード * と ** の使い分け
ワイルドカードが使えるのは hostname と pathname の 2 つだけです。ルールは次の 2 行に集約されます。* は1 つのパスセグメントまたは 1 つのサブドメインにマッチし、** は末尾の任意の数のパスセグメント、または先頭の任意の数のサブドメインにマッチします。そして ** はパターンの途中では機能しません。
言葉だけでは分かりにくいので、具体例で見てみます。
| 書き方 | マッチするもの |
|---|---|
hostname: 'example.com' | example.com のみ。www.example.com は別物として拒否されます。 |
hostname: '*.example.com' | images.example.com や cdn.example.com。a.b.example.com は 2 階層なので対象外です。 |
hostname: '**.example.com' | images.example.com も a.b.example.com も許可されます。 |
pathname: '/photos/*' | /photos/cat.jpg。/photos/2026/cat.jpg は階層が深いので対象外です。 |
pathname: '/photos/**' | /photos/cat.jpg も /photos/2026/08/cat.jpg も許可されます。 |
pathname: '/**' | そのホストのすべてのパス。範囲を絞れないため、専用ドメイン以外では避けたい書き方です。 |
サブドメインをまとめたい例として、複数のサブドメインから画像を配信しているケースを挙げます。
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com', // 何階層のサブドメインでも許可
port: '',
pathname: '/uploads/**',
search: '',
},
],
},
};
export default nextConfig;
ただし、ワイルドカードを広く取るほど許可リストとしての意味は弱くなります。特にサブドメインをユーザーが自由に作れるサービス(画像共有系のホスティングなど)で **. を使うと、事実上そのサービス全体を許可したことになります。自社ドメインのように管理下にあるホストに限って使うのが無難です。パスも同様で、/** で全開にする前に、実際に必要なディレクトリまで絞れないか検討してみてください。
Next.js 15.3 以降は new URL() で短く書ける
フィールドを 5 つ並べるのは、慣れてくると冗長に感じます。Next.js 15.3.0 以降では remotePatterns に URL オブジェクトの配列を渡せるようになり、URL を 1 行書くだけで同じ意味になります。
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
// 上のオブジェクト形式とほぼ同じ意味
remotePatterns: [new URL('https://images.example.com/photos/**')],
},
};
export default nextConfig;
渡すのは文字列ではなく new URL(...) で作った URL オブジェクトです。プロトコル・ホスト名・パスがまとめて指定できるので、書き間違いも減ります。クエリ文字列を許可したい場合は new URL('https://images.example.com/photos/**?v=1234') のように URL に含めて書きます。
1 点だけ落とし穴があります。URL コンストラクターを使うと、クエリ文字列を書かなかった場合に search が空文字 '' として扱われます。つまりクエリ付きの画像 URL は拒否されます。CMS や CDN が ?w=800&q=75 のようなパラメーターを付けてくる場合はこの書き方では通らないので、search を省いたオブジェクト形式に戻してください。
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
port: '',
pathname: '/photos/**',
// search を書かないとすべてのクエリ文字列が許可される
},
],
},
};
export default nextConfig;
なお、オブジェクト形式は Next.js 12.3.0 で正式版になった書き方で、現在も問題なく使えます。new URL() はあくまで短縮記法なので、チーム内のバージョンがまちまちならオブジェクト形式で揃えておくのも十分に妥当な判断です。
ヘッドレス CMS や画像 CDN での設定例
実務でこの設定が必要になるのは、たいてい画像の置き場所が別サービスになっているときです。よくある組み合わせをまとめて書いてみます。
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
remotePatterns: [
// WordPress をヘッドレス CMS として使う場合
// REST API が返す画像は wp-content/uploads 以下に置かれる
{
protocol: 'https',
hostname: 'cms.example.com',
port: '',
pathname: '/wp-content/uploads/**',
},
// Cloudinary(your-cloud-name は自分のクラウド名に置き換える)
{
protocol: 'https',
hostname: 'res.cloudinary.com',
port: '',
pathname: '/your-cloud-name/image/upload/**',
},
// Amazon S3(バケット名とリージョンを含むホスト名)
{
protocol: 'https',
hostname: 'my-bucket.s3.ap-northeast-1.amazonaws.com',
port: '',
pathname: '/**',
},
],
},
};
export default nextConfig;
Cloudinary や S3 のような共有ホストで pathname を絞る意味は大きいです。res.cloudinary.com は世界中の利用者が同じホスト名を使っているので、パスを /** にすると他人のアカウントの画像まで最適化できる状態になります。自分のクラウド名やバケットのパスまで書いておけば、許可される範囲は自分のアセットだけになります。
ローカル開発では、CMS を http://localhost:8080 のようなポート付きの URL で動かしていることもあります。その場合は protocol を 'http' に、port にポート番号を入れたパターンを別に追加します。本番と開発で参照先が違うなら、環境変数を見て配列を組み立てるのも手です。
import type { NextConfig } from 'next';
const isDev = process.env.NODE_ENV === 'development';
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cms.example.com',
port: '',
pathname: '/wp-content/uploads/**',
},
// 開発時のみローカルの WordPress を許可
...(isDev
? [
{
protocol: 'http' as const,
hostname: 'localhost',
port: '8080',
pathname: '/wp-content/uploads/**',
},
]
: []),
],
},
};
export default nextConfig;
protocol に as const を付けているのは、そのままだと型が string に広がってしまい、'http' | 'https' を期待する型と合わなくなるためです。
非推奨になった images.domains からの移行
少し古い記事や既存プロジェクトでは、images.domains という設定を見かけます。ホスト名を並べるだけの、もっと単純な書き方です。
module.exports = {
images: {
domains: ['assets.example.com'],
},
};
domains は Next.js 14 で非推奨になりました。理由は書きやすさの裏返しで、ワイルドカードによるパターンマッチに対応しておらず、プロトコル・ポート・パスを制限できないからです。ホスト名を書いた瞬間に、そのホストのあらゆる画像が最適化対象になります。前の節で触れたように、画像 CDN やオブジェクトストレージは複数の利用者でホストを共有しているのが普通なので、これは想定より広い許可になりがちです。
移行はそれほど手間ではありません。domains に並べていたホスト名を remotePatterns の hostname に移し、protocol と pathname を足すだけです。
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'assets.example.com',
port: '',
pathname: '/images/**', // ここで範囲を絞れるのが最大の違い
},
],
},
};
移行のときは、実際に表示している画像 URL をブラウザの開発者ツールで確認してから pathname を決めるのが確実です。想像で書くと絞りすぎて 400 になり、原因探しに時間を取られます。
設定してもエラーが消えないとき
書いたはずなのにエラーが出続ける、というのはこの設定でいちばん多い相談です。照合は URL の各部品ごとに厳密に行われるので、ズレている部品を 1 つずつ潰していくのが早い解決策になります。
開発サーバーを再起動していない
まずこれを確認してください。next.config は起動時に読み込まれる設定ファイルなので、保存しても実行中の開発サーバーには反映されません。Ctrl + C で止めて npm run dev をやり直します。設定そのものは正しいのに何も変わらない、という状況ならほぼこれが原因です。
hostname にプロトコルやスラッシュを含めている
hostname に書くのはホスト名だけです。'https://images.example.com' や 'images.example.com/photos' と書いてしまうと、URL のホスト名部分(images.example.com)と一致しないため通りません。URL をコピーしてきてそのまま貼ると起こりがちなミスです。プロトコルは protocol、パスは pathname と、フィールドが分かれていることを思い出してください。末尾のスラッシュも不要です。
ホスト名やポートが実際の URL と微妙に違う
example.com と www.example.com は別のホスト名です。同じように assets.example.com のようなサブドメインは、example.com の設定ではカバーされません。CMS の管理画面で見た URL と、実際に API が返してくる URL が違うこともあるので、レスポンスの値をそのまま確認するのが確実です。
ポートも見落としやすいところです。http://localhost:3000 の画像を許可したいなら port: '3000' が必要で、port: '' では通りません。逆に https://example.com(ポートなし)に対して port: '443' と書くのも誤りです。URL にポートが現れないなら空文字にします。プロトコルの取り違え(src が https なのに protocol: 'http')も同じ種類のミスです。
pathname が狭すぎる、または search で弾かれている
pathname: '/images/' のようにワイルドカードを付け忘れると、その配下のファイルにはマッチしません。/images/** とする必要があります。ディレクトリが 2 階層以上ある場合に /images/* だと足りない、というのもよくあるパターンです。
パスは合っているはずなのに通らないときは、クエリ文字列を疑ってください。search: '' や new URL() の短縮記法を使っていると、クエリ付きの URL は拒否されます。CMS がサイズ指定などのパラメーターを自動で付けてくる場合はここで引っかかります。前述のとおり、search を省いたオブジェクト形式にすればすべてのクエリを許可できます。
ドメインが実行時に決まってしまう
厄介なのが、投稿者が任意の URL を貼れる仕様や、ユーザーの SNS アバターを表示する画面のように、画像のドメインが事前に分からないケースです。remotePatterns は静的な許可リストなので、この要件とは根本的に相性がよくありません。
現実的な選択肢は 2 つあります。1 つは、その画像に限って unoptimized を付け、最適化を通さずそのまま配信すること。もう 1 つは、あらかじめ画像を自分のストレージに取り込んでおき、自前のドメインだけを許可すること。後者のほうが表示速度でも安定性でも有利ですが、取り込みの仕組みが必要になります。いずれにしても、ワイルドカードを限界まで広げて事実上何でも許可する形にするのは避けたい対応です。
remotePatterns とあわせて知っておきたい設定
images の下には他にも設定があり、外部画像を扱うときに関わってくるものがいくつかあります。詳細はそれぞれの公式ドキュメントに任せますが、名前と役割を知っておくと選択肢が広がります。
| 設定・プロパティ | 役割 |
|---|---|
unoptimized | 最適化をせず src のまま配信します。<Image> の props として画像単位で指定するほか、images.unoptimized: true で全体に適用できます(Next.js 12.3.0 以降)。 |
loader / loaderFile | Next.js の最適化 API ではなく、外部の画像サービスに URL 生成を任せます。この方式では自前の最適化 API を経由しないため、remotePatterns の設定も不要になります。 |
dangerouslyAllowSVG | SVG の最適化を許可します。SVG にはスクリプトを埋め込めるため既定では無効で、有効にするなら contentSecurityPolicy とあわせて設定します。src が .svg で終わる場合は自動的に unoptimized 扱いになります。 |
formats | 変換後のフォーマットを指定します。既定は ['image/webp'] で、['image/avif'] にすると AVIF を配信できます。 |
deviceSizes | sizes と組み合わせて生成される画像幅の一覧です。既定値は [640, 750, 828, 1080, 1200, 1920, 2048, 3840] です。 |
localPatterns | remotePatterns のローカル版で、最適化を許可する自サイト内のパスを絞れます(Next.js 14.2.15 以降)。 |
認証が必要な画像を扱おうとしている場合は注意点があります。既定のローダーを使った最適化 API は、画像を取得するときにヘッダーを転送しません。そのため Cookie やトークンが必要な画像は取得に失敗します。この場合は unoptimized を使うか、認証を通したうえで画像を返す仕組みを自分で用意することになります。
まとめ
next/image で外部ドメインの画像を表示するには、next.config の images.remotePatterns に許可パターンを書きます。これは不便な制約ではなく、自分の画像最適化 API を第三者に悪用されないための仕組みで、マッチしないリクエストは 400 Bad Request になります。パターンは protocol・hostname・port・pathname・search の 5 つで URL を部品ごとに照合し、判定は完全一致かつ大文字小文字を区別します。ワイルドカードは * が 1 階層、** が任意の階層で、hostname なら先頭、pathname なら末尾に置きます。Next.js 15.3 以降は new URL() で 1 行に短縮できますが、この書き方だとクエリ文字列が拒否される点だけ覚えておいてください。images.domains は Next.js 14 で非推奨になり、パスを絞れないため remotePatterns へ移行するのが推奨です。そして設定を変えたら開発サーバーの再起動を忘れないこと。エラーが消えないときは、hostname にプロトコルやスラッシュが混ざっていないか、サブドメインやポートがズレていないか、pathname のワイルドカードが足りているかを順に確認していけば、たいていは見つかります。