How hard can it be to get a website's favicon? Just fetch /favicon.ico, right?
Wrong. I tried building a reliable favicon finder and fell down a rabbit hole of legacy standards, broken markup, and bizarre edge cases.
Here's everything I learned.
The Naive Approach
const faviconUrl = new URL('/favicon.ico', siteUrl).href;
This works for maybe 70% of sites. For the rest, you'll get a 404, a redirect to the homepage, or a 1x1 transparent pixel.
The Problem: There's No Single Standard
Over the years, multiple ways to specify favicons have emerged:
1. The Original: /favicon.ico
From the late 1990s. Browsers would automatically look for /favicon.ico at the root of every domain. No HTML tag needed.
https://example.com/favicon.ico
Problem: Many modern sites don't have a file at this path anymore.
2. HTML Link Tags (Many Variants)
<!-- Standard -->
<link rel="icon" href="/favicon.png">
<!-- With type -->
<link rel="icon" type="image/png" href="/favicon-32x32.png">
<!-- Legacy IE -->
<link rel="shortcut icon" href="/favicon.ico">
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="apple-touch-icon-precomposed" href="/apple-touch-icon-precomposed.png">
<!-- Sized variants -->
<link rel="icon" sizes="16x16" href="/favicon-16x16.png">
<link rel="icon" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" sizes="192x192" href="/android-chrome-192x192.png">
<!-- SVG favicon (modern) -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<!-- Mask icon (Safari pinned tabs) -->
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
That's at least 10 different ways to specify a favicon in HTML.
3. Web App Manifest
<link rel="manifest" href="/site.webmanifest">
The manifest file contains icons:
{
"icons": [
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
]
}
4. Microsoft browserconfig.xml
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
</tile>
</msapplication>
</browserconfig>
Yes, really.
The Algorithm That Works
After testing against hundreds of sites, here's the priority order that produces the best results:
async function findFavicon(url) {
const html = await fetchHTML(url);
const baseUrl = new URL(url).origin;
const doc = parseHTML(html);
// Priority 1: SVG favicon (best quality, scales perfectly)
const svg = doc.querySelector('link[rel="icon"][type="image/svg+xml"]');
if (svg?.getAttribute('href')) {
return resolveUrl(svg.getAttribute('href'), baseUrl);
}
// Priority 2: apple-touch-icon (usually high-res, 180x180)
const apple = doc.querySelector(
'link[rel="apple-touch-icon"], link[rel="apple-touch-icon-precomposed"]'
);
if (apple?.getAttribute('href')) {
return resolveUrl(apple.getAttribute('href'), baseUrl);
}
// Priority 3: Largest explicit icon
const icons = doc.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]');
if (icons.length > 0) {
// Sort by size (largest first)
const sorted = [...icons].sort((a, b) => {
const sizeA = parseInt(a.getAttribute('sizes')?.split('x')[0] || '0');
const sizeB = parseInt(b.getAttribute('sizes')?.split('x')[0] || '0');
return sizeB - sizeA;
});
const href = sorted[0].getAttribute('href');
if (href) return resolveUrl(href, baseUrl);
}
// Priority 4: Web app manifest
const manifest = doc.querySelector('link[rel="manifest"]');
if (manifest?.getAttribute('href')) {
try {
const manifestUrl = resolveUrl(manifest.getAttribute('href'), baseUrl);
const manifestData = await fetch(manifestUrl).then(r => r.json());
if (manifestData.icons?.length) {
const largest = manifestData.icons.sort(
(a, b) => parseInt(b.sizes?.split('x')[0] || '0') - parseInt(a.sizes?.split('x')[0] || '0')
)[0];
return resolveUrl(largest.src, baseUrl);
}
} catch {}
}
// Priority 5: Default /favicon.ico
const defaultFavicon = `${baseUrl}/favicon.ico`;
try {
const res = await fetch(defaultFavicon, { method: 'HEAD' });
if (res.ok && res.headers.get('content-type')?.includes('image')) {
return defaultFavicon;
}
} catch {}
// Priority 6: Google's favicon service (public fallback)
return `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=64`;
}
function resolveUrl(href, baseUrl) {
if (href.startsWith('//')) return 'https:' + href;
if (href.startsWith('http')) return href;
return new URL(href, baseUrl).href;
}
Edge Cases That Will Ruin Your Day
1. Data URIs as Favicons
Some sites inline their favicon as a data URI:
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔥</text></svg>">
This is valid! GitHub uses an emoji favicon via data URI. Your parser needs to handle this — it's not a URL you can fetch, but you can use it directly.
2. Relative Paths with Subdirectories
<!-- On https://example.com/blog/post -->
<link rel="icon" href="../images/favicon.png">
<!-- Resolves to: https://example.com/images/favicon.png -->
Always resolve against the page URL, not just the origin.
3. Protocol-Relative URLs
<link rel="icon" href="//cdn.example.com/favicon.png">
Still used in the wild. Prepend https: and move on.
4. Multiple Favicons for Different Purposes
A well-configured site might have ALL of these:
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<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="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
Which one do you pick? It depends on your use case:
- Displaying in a list/feed: 32x32 PNG or SVG
- High-res display: apple-touch-icon (180x180)
- Scalable: SVG
5. favicon.ico That Returns HTML
Some servers return a 200 for ANY path — including /favicon.ico — but serve the homepage HTML instead. Always check the Content-Type header:
const res = await fetch(faviconUrl, { method: 'HEAD' });
const contentType = res.headers.get('content-type') || '';
if (!contentType.startsWith('image/')) {
// This is not actually a favicon
return null;
}
6. CDN-Hosted Favicons with CORS Issues
If you want to display a favicon from another origin, you might hit CORS issues in the browser. Server-side fetching avoids this entirely.
Google's Favicon API: The Lazy (Smart) Option
Google maintains a public favicon service:
https://www.google.com/s2/favicons?domain=github.com&sz=64
This returns a 64x64 PNG favicon for any domain. It's reliable, fast, and handles all the edge cases for you. Available sizes: 16, 32, 64, 128, 256.
Trade-off: You're depending on Google, and there's a slight delay for uncached domains.
DuckDuckGo's Alternative
https://icons.duckduckgo.com/ip3/github.com.ico
Similar to Google's, but from DuckDuckGo. Also free and public.
Testing Your Implementation
Test against these sites — they cover most edge cases:
| Site | Challenge |
|---|---|
| github.com | SVG favicon via data URI |
| google.com | Multiple sizes, well-configured |
| wikipedia.org | /favicon.ico with apple-touch-icon |
| reddit.com | Multiple formats |
| A personal blog | Likely missing or minimal |
| A Japanese site | Encoding + non-standard paths |
| localhost/127.0.0.1 | Should be blocked (SSRF) |
Recap
Finding a favicon reliably requires:
- Parsing HTML for 10+ different link tag variants
- Checking the web app manifest
- Falling back to /favicon.ico
- Handling relative URLs, data URIs, and protocol-relative URLs
- Validating that the response is actually an image
- Having a fallback (Google's API)
It's a lot more than just fetching /favicon.ico.
What's the most creative favicon you've seen in the wild? Drop it in the comments.
Top comments (0)