import { useEffect, useState } from 'react'; import { Globe } from 'lucide-react'; interface FaviconImageProps { domain: string; /** WordPress Site Icon URL, when the admin has set one. Preferred source. */ siteIcon?: string; className?: string; fallbackClassName?: string; } function directFaviconUrl(domain: string) { return `https://${domain}/favicon.ico`; } function preload(url: string, timeoutMs = 4000): Promise<{ ok: boolean; width: number; height: number }> { return new Promise((resolve) => { const img = new Image(); const timer = setTimeout(() => resolve({ ok: false, width: 0, height: 0 }), timeoutMs); img.onload = () => { clearTimeout(timer); resolve({ ok: img.naturalWidth > 1 && img.naturalHeight > 1, width: img.naturalWidth, height: img.naturalHeight }); }; img.onerror = () => { clearTimeout(timer); resolve({ ok: false, width: 0, height: 0 }); }; img.src = url; }); } export function FaviconImage({ domain, siteIcon, className = 'size-8 rounded-xl', fallbackClassName = 'size-8 text-white/60' }: FaviconImageProps) { const [resolved, setResolved] = useState(undefined); useEffect(() => { // Preferred source: WordPress's own Site Icon, handed to us by PHP via // get_site_icon_url(). It is served from this site's media library, is // already the right size, and needs no network guessing. if (siteIcon) { setResolved(siteIcon); return; } if (!domain) { setResolved(null); return; } let cancelled = false; (async () => { // No Site Icon configured — try the site's own /favicon.ico. We only // ever ask the site itself: a third-party favicon service (we used // Google's) would leak which domains an admin is viewing, and the // plugin documents making no such request. Failure falls through to // the bundled icon below. const direct = directFaviconUrl(domain); const d = await preload(direct); if (cancelled) return; setResolved(d.ok ? direct : null); })(); return () => { cancelled = true; }; }, [domain, siteIcon]); if (resolved === undefined) return
; if (resolved === null) return ; return ; }