Restore source icon favicon lookup
Unified · Split
| @@ -3,21 +3,20 @@ import { describe, expect, it } from "@effect/vitest"; · 2 unmodified lines | ||
| 3 | 3 | import { sourceFaviconUrl } from "./source-favicon"; |
| 4 | 4 | |
| 5 | 5 | describe("SourceFavicon", () => { |
| 6 | − it("uses the source site's own favicon for public URLs", () => { | |
| 6 | + it("uses the favicon service that handles provider-specific icon locations", () => { | |
| 7 | 7 | expect(sourceFaviconUrl("https://api.github.com/graphql", 20)).toBe( |
| 8 | − "https://github.com/favicon.ico?sz=40", | |
| 8 | + "https://www.google.com/s2/favicons?domain=github.com&sz=40", | |
| 9 | 9 | ); |
| 10 | 10 | }); |
| 11 | 11 | |
| 12 | 12 | it("does not request favicons for local URLs", () => { |
| 13 | 13 | expect(sourceFaviconUrl("http://localhost:3000/private", 20)).toBeNull(); |
| 14 | 14 | expect(sourceFaviconUrl("http://127.0.0.1:3000/private", 20)).toBeNull(); |
| 15 | − expect(sourceFaviconUrl("http://api.local/private", 20)).toBeNull(); | |
| 16 | 15 | }); |
| 17 | 16 | |
| 18 | − it("does not send source URLs to a third-party favicon service", () => { | |
| 19 | − expect(sourceFaviconUrl("https://internal.example.test/private", 20)).not.toContain( | |
| 20 | − "google.com", | |
| 17 | + it("sends only the registrable domain to the favicon service", () => { | |
| 18 | + expect(sourceFaviconUrl("https://api.github.com/private", 20)).toBe( | |
| 19 | + "https://www.google.com/s2/favicons?domain=github.com&sz=40", | |
| 21 | 20 | ); |
| 22 | 21 | }); |
| 23 | 22 | }); |
| @@ -3,38 +3,15 @@ import { useState } from "react"; · 2 unmodified lines | ||
| 3 | 3 | import { getDomain } from "tldts"; |
| 4 | 4 | |
| 5 | 5 | // --------------------------------------------------------------------------- |
| 6 | −// SourceFavicon — renders the source site's own public favicon. | |
| 7 | −// Do not fetch third-party favicon services here; source URLs may be private. | |
| 6 | +// SourceFavicon — renders a small favicon derived from a source URL. | |
| 7 | +// Falls back to a neutral icon if the URL is missing or the image fails to load. | |
| 8 | 8 | // --------------------------------------------------------------------------- |
| 9 | 9 | |
| 10 | −const IPV4_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/; | |
| 11 | − | |
| 12 | −const isIpHostname = (hostname: string): boolean => | |
| 13 | − IPV4_PATTERN.test(hostname) || hostname.includes(":"); | |
| 14 | − | |
| 15 | 10 | export function sourceFaviconUrl(url: string | undefined, size: number): string | null { |
| 16 | 11 | if (!url) return null; |
| 17 | − if (!URL.canParse(url)) return null; | |
| 18 | − | |
| 19 | − const parsed = new URL(url); | |
| 20 | − if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null; | |
| 21 | − | |
| 22 | − const hostname = parsed.hostname.toLowerCase(); | |
| 23 | − if ( | |
| 24 | − hostname === "localhost" || | |
| 25 | − hostname.endsWith(".localhost") || | |
| 26 | − hostname.endsWith(".local") || | |
| 27 | − isIpHostname(hostname) | |
| 28 | − ) { | |
| 29 | − return null; | |
| 30 | − } | |
| 31 | − | |
| 32 | − const domain = getDomain(hostname); | |
| 12 | + const domain = getDomain(url) ?? (URL.canParse(url) ? getDomain(new URL(url).hostname) : null); | |
| 33 | 13 | if (!domain) return null; |
| 34 | − | |
| 35 | − const favicon = new URL(`https://${domain}/favicon.ico`); | |
| 36 | − favicon.searchParams.set("sz", String(size * 2)); | |
| 37 | − return favicon.toString(); | |
| 14 | + return `https://www.google.com/s2/favicons?domain=${domain}&sz=${size * 2}`; | |
| 38 | 15 | } |
| 39 | 16 | |
| 40 | 17 | export function SourceFavicon({ url, size = 16 }: { url?: string; size?: number }) { |