Favicon Setup for Angular — Free, Fast & Private
Newer Angular CLI versions scaffold a public/ directory at the project root and give it a single catch-all entry in the assets array. Older projects put favicon.ico directly in src/ and everything else under src/assets, with two separate entries. Both are still perfectly valid; what matters is that whatever directory you use appears in angular.json.
Where each generated file goes in Angular
The generator above emits eight files with fixed names, site.webmanifest included. Static assets in Angular live in public/ (Angular 18+) or src/assets (earlier), and the markup is written in src/index.html. This is the mapping between the two.
| Generated file | Goes to | Notes |
|---|---|---|
| favicon.ico | public/favicon.ico | Pre-18 layout: src/favicon.ico, with its own entry in the assets array. |
| favicon-16x16.png | public/favicon-16x16.png | Pre-18: src/assets/favicon-16x16.png. |
| favicon-32x32.png | public/favicon-32x32.png | Pre-18: src/assets/favicon-32x32.png. |
| favicon-48x48.png | public/favicon-48x48.png | Optional — duplicated inside the .ico. |
| apple-touch-icon.png | public/apple-touch-icon.png | iOS home screen, 180×180. |
| android-chrome-192x192.png | public/android-chrome-192x192.png | Manifest icon, needed for ng add @angular/pwa. |
| android-chrome-512x512.png | public/android-chrome-512x512.png | Manifest icon and splash screen. |
| site.webmanifest | public/site.webmanifest | Or overwrite the manifest.webmanifest that ng add @angular/pwa generated. Must be in the assets array like everything else. |
The assets array is the whole game
Angular does not have a magic static folder. The CLI builder copies exactly the paths listed under projects.<name>.architect.build.options.assets in angular.json, and anything not listed is invisible to the build no matter how sensibly it is named. This is the number one reason an Angular favicon works with ng serve on one machine and 404s in production on another: someone added the file, someone else changed the assets array, and the two never met.
The modern shape is a single glob entry that treats public/ as a copy-everything directory. The older shape names src/favicon.ico explicitly and adds src/assets as a directory with an output path. Read your angular.json before you move any files — the layout you have is the layout the build expects, and converting between them is a deliberate change, not a cleanup.
If you run a multi-project workspace, remember that each project has its own assets array. A favicon added to the library-adjacent demo app will not appear in the real one.
angular.json — both layouts
// Angular 18+ scaffold: one glob covers the whole public/ folder
"assets": [
{ "glob": "**/*", "input": "public" }
]
// Older layout: favicon.ico named explicitly, assets copied as a directory
"assets": [
"src/favicon.ico",
{ "glob": "**/*", "input": "src/assets", "output": "/assets" }
]Relative hrefs and the base href
Open a scaffolded src/index.html and you will find <base href="/"> in the head and a favicon link written as href="favicon.ico" with no leading slash. That is not sloppiness. Angular applications are routinely deployed under a path prefix, and the CLI supports that with a single build flag, --base-href, which rewrites that base element. Every relative URL in the document then resolves against it automatically, favicons included.
Write href="/favicon.ico" instead and you have opted out of that machinery: the icon will resolve to the domain root regardless of where the app actually lives, which is fine at the root and broken under a prefix. Keeping the hrefs relative is the whole reason the base element is there.
The APP_BASE_HREF injection token you may have seen is a runtime concern for the router, not a build-time one for HTML. It does not affect where the browser looks for your icon files.
src/index.html — relative paths under <base>
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Your App</title> <base href="/"> <link rel="icon" href="favicon.ico" sizes="16x16 32x32 48x48"> <link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png"> <link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png"> <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png"> <link rel="manifest" href="manifest.webmanifest"> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body><app-root></app-root></body> </html>
The build output moved
Since the application builder became the default, ng build writes to dist/<project-name>/browser/ rather than straight into dist/<project-name>/. The extra directory exists to make room for a server/ sibling when SSR is enabled. Deployment scripts written against the older layout will happily upload the parent folder, and the result is a site where index.html is one level too deep and every relative icon path resolves to nothing.
When a favicon mysteriously disappears after an Angular upgrade, check the deploy target before you touch any markup. Compare the file listing under dist against what your host is actually serving; nine times out of ten the icon is present in the build and the wrong directory got shipped.
PWA support and the manifest
Running ng add @angular/pwa wires up the service worker and drops a manifest.webmanifest into the project along with a pile of Angular-branded PNGs under an icons directory. It also adds the manifest link tag to index.html and registers the icons at eight sizes, most of which no current platform requests.
Trim it, and the generator above has already written the trimmed version. Its contents are below: paste them over the generated manifest.webmanifest, or keep the downloaded site.webmanifest under its own name and repoint the link tag. Either way, delete the Angular logo PNGs and confirm the file is covered by the assets array, because a manifest the CLI does not copy is as invisible as an icon it does not copy.
The relative src values are not an oversight — they are what makes this work under --base-href, and Angular is the framework where that matters most, since its whole icon story is built on relative hrefs resolving against the <base> element. That element governs the HTML document. It does not reach inside a JSON file, so a manifest with "/android-chrome-192x192.png" in it would pin those two icons to the domain root while every other icon on the page followed the base href. Relative paths inside the manifest are resolved against the manifest's own URL instead, so the two stay in step. Keep the manifest in the same directory as the PNGs.
The service worker adds a second cache layer in front of everything, so after a favicon change expect to need a full reload — or a service worker update cycle — before the new icon appears, even in a fresh tab.
manifest.webmanifest — the downloaded site.webmanifest, trimmed to what platforms ask for
{
"name": "Your App",
"short_name": "App",
"start_url": ".",
"display": "standalone",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}Whatever you paste, the artwork itself never leaves your machine. The set above is rendered with the Canvas API and the .ico container is assembled in JavaScript on your device, so an unreleased logo stays unreleased. If the 16px preview looks like mush, that is the honest signal to simplify the mark before it ships to a Angular tab near you.
