public/

Serves static files from the project root at URL paths starting with a slash.

Signature
public/avatars/me.png   ->   /avatars/me.png

// Default response header for anything in public/
Cache-Control: public, max-age=0

Errors

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/image by 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

Example · typescript
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.

Example · typescript
// 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.

Example · typescript
// 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.

Example · typescript
// 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

  • Be the first to comment on this lesson.