Favicon Setup for Astro — Free, Fast & Private
Astro draws a hard line between src/assets and public/. Anything imported from src/ goes through the asset pipeline and comes out optimised, hashed and renamed — exactly what you do not want for a favicon, whose filename is load-bearing. public/ is the untouched-copy folder, and it is where all seven files belong.
Where each generated file goes in Astro
The generator above emits seven files with fixed names. Static assets in Astro live in public/, and the markup is written in src/layouts/Layout.astro (or a shared BaseHead.astro). This is the mapping between the two.
| Generated file | Goes to | Notes |
|---|---|---|
| favicon.ico | public/favicon.ico | The starter ships favicon.svg — this replaces or joins it. |
| favicon-16x16.png | public/favicon-16x16.png | Linked from the layout head. |
| favicon-32x32.png | public/favicon-32x32.png | Linked from the layout head. |
| favicon-48x48.png | public/favicon-48x48.png | Optional — already 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. |
| android-chrome-512x512.png | public/android-chrome-512x512.png | Manifest icon and splash. |
One BaseHead component, not one layout
Astro is a multi-page framework. Every route produces its own HTML document, and each document gets its head from whichever layout wrapped it. A brand-new project has a single Layout.astro and it is tempting to treat that as the head, but Astro sites accumulate layouts fast — a blog layout, a docs layout, a bare landing-page layout with no chrome — and the moment a second one appears, half your pages can quietly lose their icons.
The pattern that survives contact with a growing site is a BaseHead.astro component holding the tags every page needs: charset, viewport, favicons, and the props-driven title and description. Every layout imports it. Adding a layout then costs one import line, and there is exactly one place to edit when the icon set changes.
Astro does hoist and deduplicate the assets it injects itself, but it does not go hunting through your components for head tags to relocate. If a link element is rendered inside the body, that is where it stays. Keep them in the component that is actually placed inside <head>.
src/components/BaseHead.astro
---
const { title, description } = Astro.props;
const base = import.meta.env.BASE_URL;
---
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href={base + 'favicon.ico'} sizes="16x16 32x32 48x48" />
<link rel="icon" type="image/png" sizes="32x32" href={base + 'favicon-32x32.png'} />
<link rel="icon" type="image/png" sizes="16x16" href={base + 'favicon-16x16.png'} />
<link rel="apple-touch-icon" sizes="180x180" href={base + 'apple-touch-icon.png'} />
<link rel="manifest" href={base + 'site.webmanifest'} />
<title>{title}</title>
<meta name="description" content={description} />The starter ships an SVG, and that is worth keeping
A fresh Astro project comes with public/favicon.svg and a single link tag with type="image/svg+xml". SVG favicons are supported across current desktop and mobile browsers and have two real advantages: one file at any resolution, and the ability to respond to the user colour scheme through a media query inside the SVG itself, so a dark mark can flip to a light one against a dark tab strip.
What they do not do is cover everything. Safari on iOS wants the raster apple-touch-icon for home screens, Android install prompts read the manifest PNGs, and enough older software still expects an .ico that the file remains worth shipping. The workable arrangement is to keep the SVG as the preferred icon, then declare the .ico and the PNGs behind it — browsers pick the first format they understand, so ordering the SVG first gives modern browsers the good one and everything else a working fallback.
Keeping the SVG in front of the raster fallbacks
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
base, site, and subdirectory deployments
If you set the base option in astro.config.mjs — common when publishing a project site to GitHub Pages under a repository path — everything in public/ is served under that prefix. Astro does not rewrite hardcoded absolute paths inside your own markup, so a literal href="/favicon.ico" points at the domain root and misses. Read import.meta.env.BASE_URL and prefix with it, which is exactly why the BaseHead component above does so.
The separate site option is about absolute URLs for the sitemap, canonical tags and RSS. It has nothing to do with where icons resolve, and confusing the two is a reliable way to spend an afternoon. Set both, understand that only base affects asset paths, and test a built site served from a subdirectory before you rely on it.
Static output, SSR, and content collections
In the default static output mode, dist/ contains one HTML file per route plus the copied contents of public/ at the top level, which means the deployed tree looks exactly like a handwritten static site and /favicon.ico is a real file at a real path. With an SSR adapter the split changes — the server bundle lands in one directory and the client assets in another, and it is the client directory that gets served statically. Check where your adapter puts public/ files before assuming they are at the root.
Content collections do not change any of this. A markdown post rendered through a layout inherits that layout head, favicons included. What does change things is a page that renders no layout at all, which is easy to write by accident when prototyping. Such a page has no favicon markup, and the browser falls back to requesting /favicon.ico from the root — which works, but only because the .ico is sitting there.
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 Astro tab near you.
