History

Restore source favicons

Rhys Sullivan committed 6 months ago

Commit 3af24b01 parent eac6fb52 files · +71 −11Browse files

Unified · Split

2 files
  1. packages/react/src/components/source-favicon.test.tsx +16 −6
  2. packages/react/src/components/source-favicon.tsx +55 −5
packages/react/src/components/source-favicon.test.tsx+16−6
@@ -1,13 +1,23 @@
11 import { describe, expect, it } from "@effect/vitest";
22
3−import { SourceFavicon } from "./source-favicon";
3+import { sourceFaviconUrl } from "./source-favicon";
44
55 describe("SourceFavicon", () => {
6− it("renders without requesting an external favicon service", () => {
7− const element = SourceFavicon({ url: "https://internal.example.test/private", size: 20 });
6+ it("uses the source site's own favicon for public URLs", () => {
7+ expect(sourceFaviconUrl("https://api.github.com/graphql", 20)).toBe(
8+ "https://github.com/favicon.ico?sz=40",
9+ );
10+ });
11+
12+ it("does not request favicons for local URLs", () => {
13+ expect(sourceFaviconUrl("http://localhost:3000/private", 20)).toBeNull();
14+ expect(sourceFaviconUrl("http://127.0.0.1:3000/private", 20)).toBeNull();
15+ expect(sourceFaviconUrl("http://api.local/private", 20)).toBeNull();
16+ });
817
9− expect(element.type).not.toBe("img");
10− expect(element.props).not.toHaveProperty("src");
11− expect(element.props).not.toHaveProperty("href");
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",
21+ );
1222 });
1323 });
packages/react/src/components/source-favicon.tsx+55−5
@@ -1,15 +1,65 @@
11 import { BoxIcon } from "lucide-react";
2+import { useState } from "react";
3+import { getDomain } from "tldts";
24
35 // ---------------------------------------------------------------------------
4−// SourceFavicon — renders a neutral local source icon.
6+// SourceFavicon — renders the source site's own public favicon.
57 // Do not fetch third-party favicon services here; source URLs may be private.
68 // ---------------------------------------------------------------------------
79
8−export function SourceFavicon({ size = 16 }: { url?: string; size?: number }) {
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+export function sourceFaviconUrl(url: string | undefined, size: number): string | null {
16+ 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);
33+ 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();
38+}
39+
40+export function SourceFavicon({ url, size = 16 }: { url?: string; size?: number }) {
41+ const [failed, setFailed] = useState(false);
42+ const src = failed ? null : sourceFaviconUrl(url, size);
43+
44+ if (!src) {
45+ return (
46+ <BoxIcon
47+ aria-hidden
48+ className="shrink-0 text-muted-foreground"
49+ style={{ width: size, height: size }}
50+ />
51+ );
52+ }
53+
954 return (
10− <BoxIcon
11− aria-hidden
12− className="shrink-0 text-muted-foreground"
55+ <img
56+ src={src}
57+ alt=""
58+ width={size}
59+ height={size}
60+ loading="lazy"
61+ onError={() => setFailed(true)}
62+ className="shrink-0 rounded-sm"
1363 style={{ width: size, height: size }}
1464 />
1565 );

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t