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 seven files with fixed names. 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. |
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. Point the manifest at android-chrome-192x192.png and android-chrome-512x512.png, delete the generated Angular logos, and confirm the paths still resolve after the assets array change. 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 — 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",
"purpose": "any maskable" }
]
}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.
