* tag. This tries progressively more reliable sources before giving up. */ class FaviconResolver { public function resolve(string $siteUrl): ?string { return $this->fromWellKnownPath($siteUrl) ?? $this->fromHtmlLinkTag($siteUrl) ?? $this->fromGoogleFallback($siteUrl); } private function fromWellKnownPath(string $siteUrl): ?string { $candidate = rtrim($siteUrl, '/').'/favicon.ico'; return $this->isImage($candidate) ? $candidate : null; } private function fromHtmlLinkTag(string $siteUrl): ?string { $response = $this->safeGet($siteUrl); if (! $response || ! $response->successful()) { return null; } if (! preg_match_all( '/]*rel=["\'](?:shortcut icon|icon|apple-touch-icon)["\'][^>]*>/i', $response->body(), $tags )) { return null; } foreach ($tags[0] as $tag) { if (! preg_match('/href=["\']([^"\']+)["\']/i', $tag, $hrefMatch)) { continue; } $absolute = $this->toAbsoluteUrl($hrefMatch[1], $siteUrl); if ($absolute && $this->isImage($absolute)) { return $absolute; } } return null; } /** * Google's favicon service resolves almost anything (it does its own * multi-source lookup), making it a reliable last resort rather than * leaving the tool with no logo at all. */ private function fromGoogleFallback(string $siteUrl): string { return 'https://www.google.com/s2/favicons?sz=128&domain_url='.urlencode($siteUrl); } private function toAbsoluteUrl(string $href, string $base): ?string { if (Str::startsWith($href, ['http://', 'https://'])) { return $href; } $parsed = parse_url($base); if (! $parsed || ! isset($parsed['scheme'], $parsed['host'])) { return null; } if (Str::startsWith($href, '//')) { return "{$parsed['scheme']}:{$href}"; } return "{$parsed['scheme']}://{$parsed['host']}/".ltrim($href, '/'); } private function isImage(string $url): bool { $response = $this->safeGet($url); return (bool) $response?->successful() && str_starts_with((string) $response->header('Content-Type'), 'image/'); } private function safeGet(string $url): ?Response { try { return Http::timeout(6)->get($url); } catch (\Throwable) { return null; } } }