Vite Favicon Generator
Vite is unusual in treating index.html as source code rather than as a static asset: it sits at the project root and is rewritten at build time. Everything in public/ is copied to the output root untouched.
Where the files go
- my-app/
- index.htmlentry point — not inside public/
- public/copied to the output root as-is
- favicon.icoadd
- favicon-32x32.pngadd
- apple-touch-icon.pngadd
- icon-192.pngadd
- icon-512.pngadd
- site.webmanifestadd
- src/
- main.ts
- assets/bundled, hashed names
- vite.config.ts
Two asset pipelines, one of which you want
Anything imported from src/ is part of the module graph. Vite inlines it if it is small, emits it with a content hash if it is not, and rewrites the reference for you. That is the right behaviour for images inside your UI and the wrong behaviour for a favicon, which has to answer to a fixed URL.
public/ is the escape hatch. Files there are never resolved, never hashed, and never inlined — they are copied to the root of the output directory with their names intact. Icons, the manifest, and anything else a browser or a crawler asks for by path belong there.
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>Where a file can live
| Location | What Vite does with it | Use for |
|---|---|---|
| public/ | Copied to the output root, name unchanged | Icons, manifest, robots.txt |
| src/assets/ | Bundled, hashed, possibly inlined | Images used inside components |
| index.html | Parsed as the build entry and rewritten | The link tags themselves |
Vite favicon FAQ
Why does the icon only 404 in the production build?
Almost always because the path went through public/ or through an import, both of which change between dev and build. Open the built index.html and the output directory and confirm the href and the file agree — that answers it faster than any amount of cache clearing.
Do I need to import the favicon anywhere?
No. Files in public/ are deliberately outside the module graph. If you import one, Vite will emit a second hashed copy and you will have two files where you wanted one.
How do I deploy under a sub-path?
Set base in vite.config.ts. Vite rewrites the asset URLs it finds in index.html at build time, so open the built file afterwards and confirm the icon hrefs actually carry the prefix before you ship it.