public/
Serves static files from the project root at URL paths starting with a slash.
public/avatars/me.png -> /avatars/me.png
// Default response header for anything in public/
Cache-Control: public, max-age=0Errors
A static file whose name collides with a file in the pages/ directory causes an error.
Anything inside public is served from the base URL. public/avatars/me.png is fetched as /avatars/me.png — the folder name never appears in the path, which is the single detail people get wrong when they write src="/public/logo.svg" and get a 404.
When to reach for it
- Files referenced by a stable, hand-written URL:
site.webmanifest, verification files, downloadable PDFs. - Images passed to
next/imageby string path rather than imported. - Anything a third-party service must fetch at a fixed address.
What trips people up
Next.js reads this directory when the build starts, so a file written into it by your server at runtime is not served. Assets that appear after deploy belong in object storage, not here.
The default response header is Cache-Control: public, max-age=0, because Next.js cannot know whether a file has changed — there is no content hash in the URL. Imported assets and next/image output get fingerprinted URLs and long-lived caching instead, so prefer importing when you can. For robots.txt, favicon.ico and Open Graph images, use the special metadata files inside app rather than dropping them here.
Example
import Image from 'next/image'
// Reads public/avatars/<id>.png, requested as /avatars/<id>.png
export function Avatar({ id, alt }: { id: string; alt: string }) {
return <Image src={`/avatars/${id}.png`} alt={alt} width="64" height="64" />
}
export function AvatarOfMe() {
return <Avatar id="me" alt="A portrait of me" />
}When to use it
- Serve a company logo that a CMS references by absolute URL.
- Host a domain-verification file a third party must fetch at a fixed path.
- Provide a downloadable PDF or .well-known file at a stable address.
- Reference avatar images by a path built at runtime from a user id.
Frequently asked questions
How do I reference an image in the public folder in Next.js?
Start the path from the base URL and omit the folder name: public/avatars/me.png is referenced as /avatars/me.png. Writing /public/avatars/me.png returns a 404.
Why are my public files not cached by the browser?
Next.js cannot safely cache assets in public/ because they may change, so the default header is Cache-Control: public, max-age=0. Import the asset instead to get a content-hashed URL that can be cached long term.
Can I add files to the public folder at runtime?
No. Only files present when the build runs are served from public/. Anything uploaded or generated after deploy needs object storage or a Route Handler that streams it.
More examples
Importing versus referencing by path
The imported version gets a content-hashed filename, so it can be cached indefinitely and its intrinsic dimensions are known at build time — no width and height props, and no layout shift if you get them wrong. Use public/ only when the URL itself has to be stable and predictable, because a fixed URL is exactly what prevents long-lived caching.
// A. Imported from anywhere in the project — hashed URL, cached forever
import Image from 'next/image'
import logo from '@/assets/logo.png'
export function ImportedLogo() {
// width and height are inferred from the imported file
return <Image src={logo} alt="Logo" priority />
}
// B. Referenced from public/ — literal URL, Cache-Control: public, max-age=0
export function PublicLogo() {
return <Image src="/logo.png" alt="Logo" width={200} height={40} priority />
}Paths that do and do not resolve
The second line is the most common bug and it fails silently at build time — the string is valid, so nothing complains until the browser requests it. The third is worse: it works on / and breaks one level deep, which is why it usually ships. Always start a public asset path with a single leading slash.
// Given: public/images/hero.jpg
<img src="/images/hero.jpg" /> // correct
<img src="/public/images/hero.jpg" /> // 404 — 'public' is not part of the URL
<img src="images/hero.jpg" /> // relative to the current route, breaks on /blog/post
// Building a path at runtime is fine, since it is just a URL string:
const src = `/images/${slug}.jpg`Metadata files belong in app/, not public/
Both approaches produce /robots.txt, but the app/ version is generated, can read from your data, and gets the correct Content-Type and caching headers without you configuring anything. The special metadata files also emit the matching <link> and <meta> tags into the document head, which a file dropped in public/ never does.
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/', disallow: '/admin/' },
sitemap: 'https://example.com/sitemap.xml',
}
}
// app/icon.png -> served as the favicon, with a hashed URL
// app/opengraph-image.png -> og:image tag generated automatically
Discussion