Use CORS proxy for intelligent favicon discovery with Google fallback

- Use Shakespeare CORS proxy to actually fetch and parse HTML
- Strategy: Scrape HTML → try .svg/.ico/.png → Google service → hide if all fail
- Tracks which fallbacks have been tried to avoid loops
- Only shows favicon if something actually loads successfully
- Google service is last resort before giving up entirely

This ensures we get pure favicons when possible, with intelligent fallback.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy
2026-02-19 02:41:05 -06:00
parent 3462771fea
commit ff1dfd8ae4
+28 -13
View File
@@ -10,12 +10,14 @@ interface DomainFaviconProps {
}
/**
* Fetches the HTML of a domain and parses for favicon link tags.
* Fetches the HTML of a domain via CORS proxy and parses for favicon link tags.
* Returns the discovered favicon URL or null if not found.
*/
async function discoverFavicon(origin: string): Promise<string | null> {
try {
const response = await fetch(origin, {
// Use CORS proxy to fetch the HTML
const proxyUrl = `https://proxy.shakespeare.diy/?url=${encodeURIComponent(origin)}`;
const response = await fetch(proxyUrl, {
method: 'GET',
headers: { 'Accept': 'text/html' }
});
@@ -53,11 +55,12 @@ async function discoverFavicon(origin: string): Promise<string | null> {
/**
* Displays a favicon for a domain or URL.
* Intelligently discovers favicons by parsing HTML link tags, then falls back to common paths.
* Strategy: Scrape HTML for favicon → try common paths → fallback to Google → hide if all fail
*/
export function DomainFavicon({ domain, size = 16, className }: DomainFaviconProps) {
const [faviconUrl, setFaviconUrl] = useState<string | null>(null);
const [fallbackIndex, setFallbackIndex] = useState(0);
const [triedGoogle, setTriedGoogle] = useState(false);
const [failed, setFailed] = useState(false);
// Get origin from domain
@@ -72,18 +75,23 @@ export function DomainFavicon({ domain, size = 16, className }: DomainFaviconPro
}
}, [domain]);
// Fallback URLs to try if discovery fails
const fallbackUrls = useMemo(() => {
// Direct favicon URLs to try (svg, ico, png)
const directUrls = useMemo(() => {
if (!origin) return [];
const domain = origin.replace(/^https?:\/\//, '');
return [
`${origin}/favicon.svg`,
`${origin}/favicon.ico`,
`${origin}/favicon.png`,
`https://www.google.com/s2/favicons?domain=${domain}&sz=32`,
];
}, [origin]);
// Google fallback URL
const googleUrl = useMemo(() => {
if (!origin) return null;
const domainOnly = origin.replace(/^https?:\/\//, '');
return `https://www.google.com/s2/favicons?domain=${domainOnly}&sz=32`;
}, [origin]);
// Discover favicon from HTML on mount
useEffect(() => {
if (!origin) {
@@ -93,13 +101,15 @@ export function DomainFavicon({ domain, size = 16, className }: DomainFaviconPro
let mounted = true;
// Try to discover favicon from HTML
discoverFavicon(origin).then((url) => {
if (mounted) {
if (url) {
// Found favicon in HTML, use it
setFaviconUrl(url);
} else {
// No favicon discovered, try fallbacks
setFaviconUrl(fallbackUrls[0] || null);
// No favicon in HTML, start with first direct URL
setFaviconUrl(directUrls[0] || null);
}
}
});
@@ -107,14 +117,19 @@ export function DomainFavicon({ domain, size = 16, className }: DomainFaviconPro
return () => {
mounted = false;
};
}, [origin, fallbackUrls]);
}, [origin, directUrls]);
const handleError = () => {
// If current URL failed, try next fallback
if (fallbackIndex < fallbackUrls.length - 1) {
// If we haven't tried all direct URLs yet
if (fallbackIndex < directUrls.length - 1) {
setFallbackIndex(fallbackIndex + 1);
setFaviconUrl(fallbackUrls[fallbackIndex + 1]);
setFaviconUrl(directUrls[fallbackIndex + 1]);
} else if (!triedGoogle && googleUrl) {
// All direct URLs failed, try Google
setTriedGoogle(true);
setFaviconUrl(googleUrl);
} else {
// Even Google failed, hide the favicon
setFailed(true);
}
};