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 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2 2
3import { SourceFavicon } from "./source-favicon";3import { sourceFaviconUrl } from "./source-favicon";
4 4
5describe("SourceFavicon", () => {5describe("SourceFavicon", () => {
6 it("renders without requesting an external favicon service", () => {6 it("uses the source site's own favicon for public URLs", () => {
7 const element = SourceFavicon({ url: "https://internal.example.test/private", size: 20 });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 });
8 17
9 expect(element.type).not.toBe("img");18 it("does not send source URLs to a third-party favicon service", () => {
10 expect(element.props).not.toHaveProperty("src");19 expect(sourceFaviconUrl("https://internal.example.test/private", 20)).not.toContain(
11 expect(element.props).not.toHaveProperty("href");20 "google.com",
21 );
12 });22 });
13});23});
packages/react/src/components/source-favicon.tsx+55−5
@@ -1,15 +1,65 @@
1import { BoxIcon } from "lucide-react";1import { BoxIcon } from "lucide-react";
2import { useState } from "react";
3import { getDomain } from "tldts";
2 4
3// ---------------------------------------------------------------------------5// ---------------------------------------------------------------------------
4// SourceFavicon — renders a neutral local source icon.6// SourceFavicon — renders the source site's own public favicon.
5// Do not fetch third-party favicon services here; source URLs may be private.7// Do not fetch third-party favicon services here; source URLs may be private.
6// ---------------------------------------------------------------------------8// ---------------------------------------------------------------------------
7 9
8export function SourceFavicon({ size = 16 }: { url?: string; size?: number }) {10const IPV4_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/;
11
12const isIpHostname = (hostname: string): boolean =>
13 IPV4_PATTERN.test(hostname) || hostname.includes(":");
14
15export 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
40export 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
9 return (54 return (
10 <BoxIcon55 <img
11 aria-hidden56 src={src}
12 className="shrink-0 text-muted-foreground"57 alt=""
58 width={size}
59 height={size}
60 loading="lazy"
61 onError={() => setFailed(true)}
62 className="shrink-0 rounded-sm"
13 style={{ width: size, height: size }}63 style={{ width: size, height: size }}
14 />64 />
15 );65 );

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