
Next.js public Folder
Not everything in an app benefits from Next.js's rendering, caching, and optimization pipeline — sometimes you just need a file served exactly as-is, at a predictable URL, with no processing in between. The public folder is Next.js's answer to that: a directory at your project root whose contents are served directly, mapped one-to-one onto URL paths starting from /.
It's a small convention with a genuinely short reference — but the caching behavior it implies, and the one category of file it explicitly shouldn't be used for, are both worth understanding precisely rather than assuming.
Basic Usage
Any file placed in public is reachable at the corresponding path, relative to your domain's root. public/avatars/me.png is visitable at /avatars/me.png — no route file, no page component, nothing else required to make it work.
import Image from "next/image";
export function Avatar({ id, alt }) {
return <Image src={`/avatars/${id}.png`} alt={alt} width="64" height="64" />;
}
export function AvatarOfMe() {
return <Avatar id="me" alt="A portrait of me" />;
}
Notice the src here starts with /avatars/... — a plain, absolute path from the domain root, exactly as if you'd hardcoded a URL. There's no import statement pulling the image in as a module, and no build-time processing of the file itself; Next.js just serves whatever bytes are sitting in that folder, unmodified.
This makes public the right place for things like downloadable PDFs, favicons you're managing manually outside the Metadata API, robots-adjacent files you want full manual control over, fonts you're self-hosting outside next/font, and any other asset a route needs to reference by a stable, predictable URL without going through a build pipeline.
Caching Behavior
This is the one detail in this reference with real production consequences. Because Next.js has no visibility into whether a file in public might change between deploys — there's no build-time hashing or content-addressing happening on these assets the way there is for, say, JS/CSS bundles — it cannot safely cache them aggressively. The default headers applied are:
Cache-Control: public, max-age=0
max-age=0 means browsers and CDNs are told, in effect, "don't assume this is still fresh — revalidate before reusing it." That's the safe default, but it's also a real performance cost if you're serving, say, a large hero image from public and expecting browsers to cache it aggressively across repeat visits the way a Next.js-optimized next/image output would be cached.
If you need stronger caching guarantees for a static asset, the options are: give it a version- or hash-suffixed filename yourself and update references when it changes (manual cache-busting), or serve it from a real CDN/object storage layer that gives you finer control over cache headers than the public folder's built-in defaults allow. Don't assume a file in public gets aggressively cached just because it "feels static" — the framework's actual default is conservative specifically because it can't verify that assumption on your behalf.
What NOT to Put Here: Metadata Files
The docs are explicit and specific on this point: for static metadata files — robots.txt, favicon.ico, and similar — use the dedicated metadata file conventions inside the app folder instead of dropping them into public.
This isn't a stylistic preference; the two approaches solve genuinely different problems. A robots.txt or favicon.ico placed directly in public is served as a completely static, unprocessed file — fine if it truly never needs to vary. But the metadata file conventions inside app (robots.ts, the icon conventions, sitemap.ts, and the rest) let you generate that content dynamically via code when you need to — a robots.txt that varies by environment, a sitemap built from a live list of blog posts, icons generated programmatically rather than hand-exported as static image files. If you reach for public for these files out of habit, you lose that entire dynamic-generation capability without necessarily realizing you've given it up.
The practical rule: if a file's content should ever be able to change based on code, request context, or environment, it belongs in the metadata file conventions, not in public. Reserve public for assets that are genuinely, permanently static — the same bytes, forever, regardless of anything your application logic might otherwise want to vary.
Key Takeaways
| Aspect | Detail |
|---|---|
| URL mapping | public/path/to/file.ext → /path/to/file.ext, no route file required |
| Default caching | Cache-Control: public, max-age=0 — conservative, since Next.js can't verify file freshness |
| Aggressive caching | Requires manual cache-busting (versioned filenames) or an external CDN/storage layer |
| Metadata files (robots.txt, favicons, etc.) | Use the dedicated app-directory metadata conventions instead — they support dynamic generation, public doesn't |
| Best use | Genuinely static, unprocessed assets referenced by a stable, predictable URL |
The public folder earns its simplicity by making one narrow tradeoff explicit: you get a direct, zero-processing path from file to URL, in exchange for giving up the caching aggressiveness and dynamic-generation capabilities that Next.js's more specialized conventions provide. For truly static assets, that tradeoff is exactly right. For anything metadata-shaped or anything you'd want cached hard, reach for the more specific tool instead.


