DEV Community

eatyou eatyou
eatyou eatyou

Posted on

Building a Favicon Finder: Why It's Harder Than You Think

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

That's at least 10 different ways to specify a favicon in HTML.

3. Web App Manifest

<link rel="manifest" href="/site.webmanifest">
Enter fullscreen mode Exit fullscreen mode

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" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

4. Microsoft browserconfig.xml

<browserconfig>
  <msapplication>
    <tile>
      <square150x150logo src="/mstile-150x150.png"/>
    </tile>
  </msapplication>
</browserconfig>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>">
Enter fullscreen mode Exit fullscreen mode

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 -->
Enter fullscreen mode Exit fullscreen mode

Always resolve against the page URL, not just the origin.

3. Protocol-Relative URLs

<link rel="icon" href="//cdn.example.com/favicon.png">
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Parsing HTML for 10+ different link tag variants
  2. Checking the web app manifest
  3. Falling back to /favicon.ico
  4. Handling relative URLs, data URIs, and protocol-relative URLs
  5. Validating that the response is actually an image
  6. 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)