Next.js Images Not Loading from public/static Folder: Fix
Which URL serves a file from public/, why next/image answers 400 instead of 404, and what basePath and a production server change.
TL;DR#
Next.js serves files from a folder named public at the project root and maps that folder to /. The file public/logo.png is served at /logo.png, never at /public/logo.png. The root static folder from the original question is the old convention, deprecated in favour of public. Put the file in public/ and reference it without the public prefix. Give a string src on next/image an explicit width and height, or import the file instead. If the app sets basePath, put that prefix in front of the next/image src too.
The error#
The Stack Overflow question asks how to load an image in a component, and whether the project must be built first. There is no stack trace: the browser just shows a broken image. On a current app the same mistake shows up in the network tab and in the dev server log. This was reproduced on Next.js 16.3.8 (Turbopack dev server) on 2 October 2026, with a minimal App Router app and a 136-byte public/logo.png. Each request was made with curl, printing the status code, content type and body size, and the body of the 400 response was printed after it:
GET /logo.png
200 image/png 136
GET /public/logo.png
404 text/html; charset=utf-8 10809
GET /_next/image?url=%2Fpublic%2Flogo.png&w=640&q=75
400 43
The requested resource isn't a valid image.The dev server logged this line for the third request:
⨯ The requested resource isn't a valid image for /public/logo.png received nullThe first request is the fix and the second is the classic mistake. The third is what the same mistake looks like through next/image, which rendered the local src as a URL on its optimiser endpoint, /_next/image.
Why it happens#
The public folder docs state the rule in one sentence: files inside public "can then be referenced by your code starting from the base URL (/)". Their example maps the file public/avatars/me.png to the path /avatars/me.png. The Pages Router version of the page says the same. public is a location on disk, not a URL segment, and /public/logo.png returned 404 in the run above while /logo.png returned the image.
A plain <img> tag shows that as a 404. next/image hides it one level deeper: the rendered src becomes /_next/image?url=%2Fpublic%2Flogo.png&..., and the optimiser endpoint answers 400 with "The requested resource isn't a valid image." In the run above, the log line names the path it was given, /public/logo.png. That path is the thing to fix, not the image file.
The status is 400 rather than 404 because of how the optimiser fetches a local src. In the Next.js 16.3.8 optimiser source, fetchInternalImage requests the path internally and checks the response only for a missing status code or an empty body. What it hands on is the body, the content type, the cache header and the ETag, not the status. The 404 page is a non-empty HTML body, so it goes through. The optimiser then sniffs the bytes, finds no image format (the received null at the end of the log line), and throws a 400 with the message printed in the response body above.
The static folder in the question title is the old convention. The Next.js error page Static directory is deprecated explains that early versions used a static directory to serve static assets, and that it has been deprecated in favour of a public directory so that assets no longer need the /static prefix. The second answer on the question gives the matching migration: place the files in public/static. The accepted answer describes public, quoting the docs, while the question title still says static.
The other half of the question, whether a build is needed, has two answers. On the dev server, no: a file created in public/ while next dev was running was served on the next request, without a restart. That matches a comment in the Next.js 16.3.8 router source, which checks the filesystem directly in development "so we don't have to wait on the watcher". A next start server behaves differently. In the same file, public/ is read into a set of known files once, inside the branch that only runs outside development. The same app, built with next build and served with next start, behaved accordingly. These are the lines from two consecutive curl runs, with the file created between the first and second request and the server restarted (not rebuilt) before the last one:
GET /late.png
404 text/html; charset=utf-8 6915
public/late.png 136 bytes
GET /late.png (added after next start)
404 text/html; charset=utf-8 6915
GET /late.png (again, 3 s later)
404 text/html; charset=utf-8 6915
GET /late.png (after restarting next start, no rebuild)
200 image/png 136An earlier version of the static assets docs put the rule in terms of build time: "Only assets that are in the public directory at build time will be served by Next.js. Files added at request time won't be available." The current public folder page no longer carries that note. In this test the cut-off was the moment next start started, not the build.
Fix#
1. Put the file in public/ at the project root#
The folder sits next to package.json, not inside app/. Projects that use a src folder keep it at the root too: the src folder docs say "the /public directory should remain in the root of your project".
my-app/
app/
page.js
public/
logo.png
package.json2. Reference it from /, without the public prefix#
export default function Logo() {
return <img src="/logo.png" alt="Company logo" width={64} height={64} />
}A subfolder keeps its name in the URL: public/avatars/me.png is /avatars/me.png.
3. With next/image and a string src, pass width and height#
The Image component docs say you must set both width and height unless the image is statically imported or uses the fill property. The values are the intrinsic size in pixels and set the aspect ratio; CSS still controls the rendered size.
import Image from 'next/image'
export default function Logo() {
return <Image src="/logo.png" alt="Company logo" width={64} height={64} />
}4. Or import the file and let Next.js read its size#
The Image Optimization guide explains that a statically imported image gets its width and height determined automatically. The third answer on the Stack Overflow question shows the same pattern.
import Image from 'next/image'
import logo from '../public/logo.png'
export default function Logo() {
return <Image src={logo} alt="Company logo" />
}In the reproduction, this rendered width="64" height="64" and an optimiser URL pointing at a hashed copy under /_next/static/media/. An imported image therefore does not need to live in public/: the docs example imports ./profile.png from the app folder. For SVG files, where the import gives an image object rather than a component, see importing SVG in Next.js, which covers next/image for display and SVGR for inline components.
5. Move a legacy static/ folder inside public/#
The deprecation page gives the migration: move static inside public, and "all URLs will stay the same as they were before".
my-app/
pages/
index.js
public/
static/
my-image.jpg/static/my-image.jpg keeps working, now served from public/static/.
6. With basePath, put the prefix in front of the src yourself#
The basePath docs say next/link applies the prefix automatically, but for next/image "you will need to add the basePath in front of src". With basePath: '/docs' in next.config.js:
import Image from 'next/image'
export default function Logo() {
return <Image src="/docs/logo.png" alt="Company logo" width={64} height={64} />
}A plain <img> needs the same prefix: it is rendered exactly as written.
Verify the fix#
Request the file and the optimiser URL directly. The w and q values are taken from the defaults in the Image docs: deviceSizes includes 640 and qualities defaults to 75.
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" http://localhost:3000/logo.png
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" "http://localhost:3000/_next/image?url=%2Flogo.png&w=640&q=75"Both lines should print 200 image/png. Under basePath, prefix both paths and the url value. This is the basePath: '/docs' run from the reproduction. The first two requests hit the file directly; the last two are the optimiser URLs rendered for src="/logo.png" and for src="/docs/logo.png":
GET /logo.png
404 text/html; charset=utf-8 10908
GET /docs/logo.png
200 image/png 136
GET /docs/_next/image?url=%2Flogo.png&w=128&q=75
400 43
The requested resource isn't a valid image.
GET /docs/_next/image?url=%2Fdocs%2Flogo.png&w=128&q=75
200 image/png 120In the browser, the Network tab shows the same thing: filter by Img and check that the /_next/image request returns 200, not 400.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
When the path is right and the public image still fails#
A few cases fail even after the public prefix is gone. Messages below are copied from the next dev log of the same Next.js 16.3.8 reproduction, unless the text says otherwise.
No leading slash on the src#
src="logo.png" is not a relative path for next/image. Under next dev the page returned a 500, with this error:
⨯ Error: Failed to parse src "logo.png" on `next/image`, if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)The message comes from image-loader.ts, inside a block that is skipped in production builds. Use /logo.png.
A string src with no width or height#
<Image src="/logo.png" alt="logo" /> also returned a 500:
⨯ Error: Image with src "/logo.png" is missing required "width" property.Add width and height (step 3), use fill inside a positioned (position: relative), sized parent, or import the file (step 4). Sizing and layout shift once the image loads are a separate topic: fixing next/image layout shift covers it.
A root static/ folder that still answers#
Next.js 16.3.8 still serves a root static/ folder under /static/. A file at static/old.png returned 200 at /static/old.png and 404 at /old.png in dev, and 200 at /static/old.png under next start. The dev server printed no warning. next start printed this at startup, from the branch of the router source that only runs outside development:
⚠ The static directory has been deprecated in favor of the public directory. https://nextjs.org/docs/messages/static-dir-deprecatedMove the folder into public/static/ (step 5) rather than relying on it.
A proxy or middleware in front of public/#
If public paths return a redirect or a login page instead of the image, look at the proxy or middleware before the path. The proxy.js matcher post explains that without a matcher, proxy.js runs on every request, including _next/static and public assets.
Images written to public/ after deployment#
User avatars or CMS uploads written to public/ at runtime do not fit the model above: a next start server only serves the files that were in public/ when it started. Store them in object storage instead. For Supabase, the Storage uploads guide covers buckets, RLS policies and signed URLs. Then pass the full URL to next/image. If the storage host is missing from images.remotePatterns, next dev throws an error saying the hostname "is not configured under images in your next.config.js". That text comes from the development-only block of image-loader.ts, so a production server never prints it: there, the Image docs say an unmatched protocol, hostname, port or path on /_next/image responds with 400 Bad Request. The hostname-not-configured fix walks through the exact match on protocol, hostname, port and path.
A public/ file with the same path as a page#
The Pages Router docs warn that a file in public/ must not share its path with a page in pages/. The conflicting public file page explains that only one resource can live at that URL, and the fix is to rename one of the two.
The accepted answer on the question still holds: images live in public/ and are referenced from /, and the dev server picks up new files without a build. Most of the other failures above come from a src that does not match how Next.js maps that folder: a /public prefix, a missing leading slash or a missing basePath. A file written to public/ after next start started is a different case: the src was right, but the server had not re-read the folder since it started, and a restart without a rebuild made the file appear. Check the URL with curl before changing any code. A 200 on /logo.png next to a 400 on /_next/image points at the src, not at the file.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix next/image Hostname Not Configured in Next.js
next/image strictly allow-lists remote hosts. The fix is images.remotePatterns in next.config — matched exactly on protocol, hostname, port, and pathname. Get one wrong (http vs https, a subdomain, a missing port) and it still blocks.
How to set the next/image component to 100% height
If your <Image / component ignores height: 100% and appears too small or misaligned, the issue is almost always that the parent container lacks an explicit
How to implement LCP end to end
Learn how to instrument Largest Contentful Paint (LCP) in a Next.js project, send the metric to your analytics provider, and verify that the data is accurate.


