Favicons are those tiny icons that appear in browser tabs, bookmarks, and link previews. When building apps that display external links, you often need to fetch favicons programmatically. Let's explore the best ways to do this.
The Favicon Challenge
Getting a website's favicon seems simple—just fetch /favicon.ico, right? Unfortunately, it's more complex:
- Not all sites use
/favicon.ico - Favicons can be in different formats (ICO, PNG, SVG)
- Modern sites use multiple sizes
- Some sites use data URIs
- Fallback chains can be complex
Common Approaches
1. Direct /favicon.ico
The simplest approach, but unreliable:
const faviconUrl = `https://example.com/favicon.ico`;
Problems:
- Many sites don't have a favicon.ico file
- You can't check if it exists without fetching it
- CORS blocks client-side requests
2. Google's Favicon Service
Google provides a favicon proxy:
const faviconUrl = `https://www.google.com/s2/favicons?domain=${domain}`;
Pros:
- Simple to use
- No CORS issues
- Caches results
Cons:
- Limited sizes (16px default)
- No guarantee of accuracy
- May return Google's default icon
- Not officially supported
3. DuckDuckGo's Service
const faviconUrl = `https://icons.duckduckgo.com/ip3/${domain}.ico`;
Similar limitations to Google's service.
4. Using a Metadata API
The most reliable approach is using an API that properly parses the HTML:
const response = await fetch(
`https://api.katsau.com/v1/favicon?url=${encodeURIComponent(url)}`,
{
headers: { 'Authorization': 'Bearer YOUR_API_KEY' }
}
);
const data = await response.json();
console.log(data.data.favicon); // Best favicon URL
console.log(data.data.favicons); // All available sizes
Why this works better:
- Parses actual HTML for link tags
- Finds all favicon formats and sizes
- Handles relative URLs
- Returns the highest quality option
- Works for any website
How Websites Declare Favicons
Modern websites use multiple link tags:
<!-- Classic favicon -->
<link rel="icon" href="/favicon.ico" />
<!-- PNG favicons in multiple sizes -->
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<!-- SVG favicon (modern browsers) -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- Web manifest with icons -->
<link rel="manifest" href="/site.webmanifest" />
A good favicon API parses all of these and returns the best options.
Building a Favicon Display Component
Here's a React component that displays a website's favicon with fallback:
function SiteFavicon({ url, size = 32 }: { url: string; size?: number }) {
const [faviconUrl, setFaviconUrl] = useState<string | null>(null);
const [error, setError] = useState(false);
useEffect(() => {
async function fetchFavicon() {
try {
const res = await fetch(
`https://api.katsau.com/v1/favicon?url=${encodeURIComponent(url)}`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
const { data } = await res.json();
setFaviconUrl(data.favicon);
} catch {
setError(true);
}
}
fetchFavicon();
}, [url]);
if (error || !faviconUrl) {
return (
<div
className="bg-gray-100 rounded flex items-center justify-center"
style={{ width: size, height: size }}
>
🌐
</div>
);
}
return (
<img
src={faviconUrl}
alt=""
width={size}
height={size}
className="rounded"
onError={() => setError(true)}
/>
);
}
Best Practices
1. Cache Aggressively
Favicons rarely change. Cache them for at least 24 hours:
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
async function getCachedFavicon(url) {
const cacheKey = `favicon:${url}`;
const cached = localStorage.getItem(cacheKey);
if (cached) {
const { favicon, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < CACHE_DURATION) {
return favicon;
}
}
const favicon = await fetchFaviconFromAPI(url);
localStorage.setItem(cacheKey, JSON.stringify({
favicon,
timestamp: Date.now()
}));
return favicon;
}
2. Always Have a Fallback
Some sites genuinely don't have favicons:
<img
src={faviconUrl}
onError={(e) => {
e.currentTarget.src = '/default-site-icon.png';
}}
/>
3. Use Appropriate Sizes
Different contexts need different sizes:
| Context | Recommended Size |
|---|---|
| Navigation links | 16px |
| Bookmarks | 32px |
| Tab/sidebar | 24px |
| Cards/previews | 48px+ |
4. Consider SVG When Available
SVG favicons are scalable and look crisp at any size:
const { data } = await response.json();
// Prefer SVG if available
const favicon = data.favicons.find(f => f.type === 'image/svg+xml')
|| data.favicon;
Performance Optimization
Proxy Through Your Backend
Instead of exposing your API key client-side, proxy requests:
// Your API route: /api/favicon
export async function GET(request) {
const url = request.nextUrl.searchParams.get('url');
const response = await fetch(
`https://api.katsau.com/v1/favicon?url=${url}`,
{
headers: { 'Authorization': `Bearer ${process.env.KATSAU_API_KEY}` }
}
);
return response;
}
Lazy Load Below the Fold
For lists with many favicons:
<img
src={faviconUrl}
loading="lazy"
decoding="async"
/>
Conclusion
While it seems simple, getting favicons reliably requires handling many edge cases. Using a proper metadata API saves you from dealing with:
- CORS restrictions
- Multiple favicon formats
- Size variations
- Fallback chains
- Relative URL resolution
Focus on building your product, not parsing HTML for icons.
Need reliable favicon retrieval? Try Katsau's Favicon API - returns favicons in all available sizes.
Try Katsau API
Extract metadata, generate link previews, and monitor URLs with our powerful API. Start free with 1,000 requests per month.