ロゴ画像や資料の PDF、フォントファイルのように「そのままの形で配信したいファイル」は、Next.js ではプロジェクト直下の public フォルダに置きます。置いたファイルはビルドの対象にならず、URL を直接叩けばそのまま返ってくるのが特徴です。この記事では App Router を前提に、public に置いたファイルがどの URL になるのか、next/image からどう参照するのか、import した画像と何が違うのかを整理し、404 になるときやファイルを差し替えても反映されないときの確認点まで解説します。
目次
public に置いたファイルはルート起点の URL になる
public はプロジェクトのルート直下(app や package.json と同じ階層)に作るフォルダです。この中のファイルは、サイトのベース URL(/)を起点としたパスで配信されます。ポイントは、URL に public という文字列が入らないことです。public はあくまで置き場所を示すフォルダ名で、公開されるときには取り除かれます。
my-app/ ├── app/ │ ├── layout.tsx │ └── page.tsx ├── public/ │ ├── logo.png │ ├── images/ │ │ └── hero.jpg │ └── docs/ │ └── guide.pdf ├── next.config.ts └── package.json
この構成なら、それぞれのファイルは次の URL でアクセスできます。フォルダの階層はそのまま URL の階層になります。
| ファイルの置き場所 | 配信される URL |
|---|---|
public/logo.png | /logo.png |
public/images/hero.jpg | /images/hero.jpg |
public/docs/guide.pdf | /docs/guide.pdf |
public/fonts/custom.woff2 | /fonts/custom.woff2 |
初心者がまずつまずくのがここで、/public/logo.png と書いてしまうケースが非常に多いです。それでは 404 になります。開発サーバーを起動した状態でブラウザのアドレス欄に http://localhost:3000/logo.png と入力すれば、画像が単体で表示されるはずです。コードを書く前にこの確認をしておくと、パスの誤りなのか表示側の問題なのかを切り分けられます。
next/image から public の画像を表示する
画像を表示するときは next/image の Image コンポーネントを使います。src には、先ほどの表のとおり public を除いたパスを文字列で渡します。
import Image from 'next/image';
export default function Page() {
return (
<Image
src="/images/hero.jpg" // public/images/hero.jpg のこと
alt="トップページのメインビジュアル"
width={1200} // 画像そのものの幅(px)
height={630} // 画像そのものの高さ(px)
/>
);
}
パスを文字列で渡す場合、width と height の指定は省略できません。Next.js はビルド時にファイルの中身を読んでいないため、画像の縦横比を知る手段がないからです。この 2 つは表示サイズではなく画像本来のサイズ(intrinsic size)を書く場所で、ブラウザが読み込み前に領域を確保してレイアウトのガタつきを防ぐために使われます。実際の見た目の大きさは CSS で調整します。サイズが分からない画像や、親要素いっぱいに広げたい場合は width と height の代わりに fill を使います。next/image 自体の細かいオプションについては別記事で解説しています。
もちろん、素の img 要素や CSS の background-image から参照しても構いません。CSS の場合も同じく url(/images/hero.jpg) のように書きます。
import した画像との違い
Next.js では、画像を public に置かず、コンポーネントの近くに置いて import する方法もあります。この書き方は静的インポートと呼ばれ、public のパスを文字列で渡す方法とは扱いがかなり変わります。
import Image from 'next/image';
import hero from './hero.jpg'; // ファイルを直接読み込む
export default function Page() {
return (
<Image
src={hero}
alt="トップページのメインビジュアル"
// width と height は自動で入るので不要
placeholder="blur" // 読み込み中のぼかしも自動で用意される
/>
);
}
静的インポートでは、Next.js がビルド時にファイルを解析するため、幅と高さが自動で決まります。jpg・png・webp・avif であれば、ぼかしプレースホルダー用の blurDataURL も自動で生成されます(アニメーション画像は除く)。さらに、出力されるファイル名にはハッシュが付き、内容が変われば URL も変わります。そのため Next.js はこれらのファイルに Cache-Control: public, max-age=31536000, immutable という「1 年間キャッシュしてよい」ヘッダーを付けられます。
一方 public のファイルは、URL とファイル名が固定です。中身が差し替わっても URL が変わらないため、Next.js は安全に長期キャッシュを指示できず、既定では Cache-Control: public, max-age=0 が付きます。両者の違いをまとめると次のようになります。
| 項目 | public にパスで指定 | 静的インポート |
|---|---|---|
| 書き方 | src="/hero.jpg" | import hero from './hero.jpg' して src={hero} |
width / height | 自分で指定する(または fill) | 自動で決まる |
blurDataURL | 自分で用意する必要がある | 対応形式なら自動生成 |
| 出力されるファイル名 | 置いたときのまま | ハッシュ付きの名前に変わる |
| 既定のキャッシュ | max-age=0 | max-age=31536000, immutable |
| 存在しないときの挙動 | 実行時に 404 になる | ビルド時にエラーで気づける |
使い分けの目安はシンプルです。コンポーネントからしか使わない画像は静的インポートのほうが有利で、サイズ指定の手間がなく、キャッシュも効きます。逆に、og:image として外部サービスから参照されるもの、メールに貼るバナー、ダウンロード用の PDF のようにURL が変わっては困るファイルは public に置きます。
PDF などのダウンロードファイルを置く
public に置けるのは画像だけではありません。PDF、CSV、zip、動画、フォントなど、どんな形式のファイルでもそのまま配信されます。PDF を配布したいなら public/docs/ のようなフォルダを作って置き、リンクを張るだけです。
export default function DownloadPage() {
return (
<p>
{/* public/docs/guide.pdf を配布する */}
<a href="/docs/guide.pdf" download>
利用ガイド(PDF)をダウンロード
</a>
</p>
);
}
ページ内リンクではないので、next/link の Link ではなく素の a 要素を使います。download 属性を付けるとブラウザ内で開かずに保存を促せますが、付けなければ PDF はブラウザのビューアーで表示されます。どちらが親切かは配布物の性質で選んでください。
favicon や robots.txt はどちらに置くべきか
favicon.ico や robots.txt、sitemap.xml は、昔ながらの感覚だと public に置きたくなるファイルです。実際 public/robots.txt に置けば /robots.txt として配信されるので、動作自体はします。
ただし App Router には、これらを扱うためのメタデータファイル規約が用意されており、Next.js の公式ドキュメントでも public ではなく app 内の規約ファイルを使うことが推奨されています。app/favicon.ico を置けば link 要素まで自動で出力されますし、app/robots.ts や app/sitemap.ts にすればコードで内容を組み立てられ、型チェックも効きます。
| 用途 | 推奨される置き場所 | できあがる URL |
|---|---|---|
| ファビコン | app/favicon.ico | /favicon.ico |
| クローラー制御 | app/robots.ts(または robots.txt) | /robots.txt |
| サイトマップ | app/sitemap.ts(または sitemap.xml) | /sitemap.xml |
| ロゴ・写真・PDF など | public/ 以下 | 置いたパスそのまま |
ここで気をつけたいのが、同じ URL を作るファイルを両方に置かないことです。app/robots.ts と public/robots.txt はどちらも /robots.txt を生み出すため、どちらが返るかに依存した作りにするべきではありません。設定を書き換えたのに反映されない、という原因が分かりにくい不具合につながります。app 側の規約ファイルを使うと決めたら、public にある同名のファイルは削除しておきましょう。これは規約ファイルに限らず、app/about/page.tsx(/about)と public/about のようなルートとの衝突でも同じです。
public は「誰でも見られる場所」だと意識する
public に置いたファイルは、URL さえ分かれば誰でも取得できます。ログイン状態のチェックも、リファラーの確認も一切入りません。フォルダ名のとおり、文字どおり「公開」ディレクトリです。
そのため、会員限定の資料、社内向けの見積書、APIキーを含む設定ファイル、開発時に使ったダミーの個人情報 CSV といったものを置いてはいけません。「リンクを張っていないから見つからないだろう」という発想は通用しません。ファイル名は HTML やビルド後の JavaScript に残りますし、外部サービスにクロールされることもあります。認証が必要なファイルは public ではなく、ルートハンドラーなどを経由して権限を確認したうえで返す設計にしてください。秘密の値を扱うなら環境変数を使い、サーバー側だけで読むのが基本です。
画像が 404 になるときの確認点
パスに public を含めていないか
最も多い原因です。public/logo.png の URL は /logo.png であって、/public/logo.png ではありません。エディタの補完でフォルダ名まで入ってしまったときに起こりがちなので、まずここを疑ってください。また、logo.png や ./logo.png のような相対パスも避けます。表示されているページの階層によって解決先が変わるため、/blog/hello のような深いパスのページで壊れます。先頭にスラッシュを付けた絶対パスで書くのが確実です。
大文字・小文字が一致しているか
ローカルでは表示できるのに、デプロイすると画像だけ出ない、というときに疑うポイントです。macOS や Windows の標準的なファイルシステムは大文字と小文字を区別しないため、Logo.png を /logo.png と書いていても手元では動いてしまいます。しかし本番環境の Linux は区別するので、そこで初めて 404 になります。拡張子も同様で、.JPG と .jpg は別物として扱われます。
ビルドのあとに追加したファイルではないか
public の中身は next build の時点で成果物にコピーされます。つまり配信されるのはビルド時点で存在していたファイルだけです。デプロイ後にサーバー上へファイルを置いても、再ビルド・再デプロイをしない限り配信されません。ユーザーがアップロードした画像のように、実行時に増えていくファイルをここに保存する運用は成り立たないので、その用途にはオブジェクトストレージなどの外部サービスを使ってください。
開発中も、開発サーバーを起動したまま public にファイルを追加すると、うまく認識されないことがあります。パスに間違いがないのに 404 が続くようなら、いったんサーバーを停止して起動し直してみてください。
差し替えたのに古い画像が表示されるとき
同じファイル名のまま画像を新しいものに置き換えたのに、ブラウザには前の画像が出続ける。これも public でよく遭遇する場面です。URL が変わっていない以上、ブラウザや CDN からすれば「すでに持っているもの」と区別がつかないために起こります。
まずは手元のブラウザでキャッシュを無視した再読み込み(スーパーリロード)を試し、それで直るなら閲覧者側のキャッシュの問題です。Next.js 自体は public のファイルに Cache-Control: public, max-age=0 を付けるため、本来は毎回サーバーに確認が入ります。しかし前段に CDN やリバースプロキシを置いている場合、そちらの設定でより長くキャッシュされていることがあり、そうなると元ファイルを差し替えてもしばらく古い内容が配信されます。
確実な解決策は、ファイル名を変えて URL 自体を新しくすることです。logo.png を logo-v2.png にする、あるいは hero-20260805.jpg のように日付を入れる。URL が違えば別のファイルとして取得されるので、キャッシュの残り方に悩む必要がなくなります。いわゆるキャッシュバスティングと呼ばれる考え方で、静的インポートがハッシュ付きのファイル名を出力しているのも同じ理屈です。
逆に、ロゴのようにめったに変わらないファイルは、明示的に長くキャッシュさせたい場面もあります。その場合は next.config の headers で対象のパスにヘッダーを設定します。ただし長期キャッシュを指定するなら、差し替えのたびにファイル名を変える運用とセットにしてください。
module.exports = {
async headers() {
return [
{
// public/images/ 以下のファイルが対象
source: '/images/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
};
まとめ
public はプロジェクト直下に置く静的ファイル用のフォルダで、中のファイルはベース URL(/)を起点としたパスで配信されます。public/logo.png は /logo.png であり、URL に public は含まれません。next/image から使うときはこのパスを src に渡し、width と height を自分で指定します。コンポーネントからしか使わない画像なら、サイズと blurDataURL が自動で決まり、ハッシュ付きのファイル名で長期キャッシュが効く静的インポートのほうが扱いやすいでしょう。favicon.ico や robots.txt、sitemap.xml は app のメタデータファイル規約を使い、同じ URL になるファイルを両方に置かないようにします。public の中身は誰でもアクセスできるので機密情報は置かず、ビルド時にコピーされる仕組み上、ビルド後に追加したファイルは配信されない点も押さえておいてください。差し替えても反映されないときは、ファイル名を変えて URL ごと新しくするのが確実です。