History

feat(core): add favicon URL resolution utility

Add getRegistrableDomain() for extracting root domains from hostnames (with compound suffix support: .co.uk, .com.au, etc.) and getFaviconUrlForRemoteUrl() for converting remote URLs to Google Favicons API URLs. Used by GraphQL/OpenAPI/MCP plugins for iconUrl auto-generation.

Saatvik Arya committed 7 months ago

Commit 23e13771 parent d39b74d2 files · +87 −0Browse files

Unified · Split

2 files
  1. packages/sources/core/src/favicon.ts +86 −0
  2. packages/sources/core/src/index.ts +1 −0
packages/sources/core/src/favicon.ts+86−0
@@ -0,0 +1,86 @@
1const COMMON_COMPOUND_SUFFIXES = new Set([
2 "ac.uk",
3 "co.in",
4 "co.jp",
5 "co.nz",
6 "co.uk",
7 "com.au",
8 "com.br",
9 "com.mx",
10 "net.au",
11 "org.au",
12 "org.uk",
13]);
14
15const RAW_HOSTS = new Set([
16 "cdn.jsdelivr.net",
17 "raw.github.com",
18 "raw.githubusercontent.com",
19 "unpkg.com",
20]);
21
22function trimOrNull(value: string | null | undefined): string | null {
23 if (value == null) {
24 return null;
25 }
26
27 const trimmed = value.trim();
28 return trimmed.length > 0 ? trimmed : null;
29}
30
31function isIpv4Address(value: string): boolean {
32 return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value);
33}
34
35export function getRegistrableDomain(hostname: string): string | null {
36 const normalized = hostname.trim().toLowerCase().replace(/^\.+|\.+$/g, "");
37 if (!normalized) {
38 return null;
39 }
40
41 if (normalized === "localhost" || isIpv4Address(normalized)) {
42 return normalized;
43 }
44
45 const parts = normalized.split(".").filter((part) => part.length > 0);
46 if (parts.length < 2) {
47 return null;
48 }
49
50 const suffix = parts.slice(-2).join(".");
51 if (parts.length >= 3 && COMMON_COMPOUND_SUFFIXES.has(suffix)) {
52 return parts.slice(-3).join(".");
53 }
54
55 return parts.slice(-2).join(".");
56}
57
58export function getFaviconUrlForRemoteUrl(
59 value: string | null | undefined,
60 options: {
61 allowRawHosts?: boolean;
62 } = {},
63): string | null {
64 const trimmed = trimOrNull(value);
65 if (!trimmed) {
66 return null;
67 }
68
69 try {
70 const url = new URL(trimmed);
71 if (url.protocol !== "http:" && url.protocol !== "https:") {
72 return null;
73 }
74
75 if (!options.allowRawHosts && RAW_HOSTS.has(url.hostname)) {
76 return null;
77 }
78
79 const domain = getRegistrableDomain(url.hostname);
80 return domain
81 ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=32`
82 : null;
83 } catch {
84 return null;
85 }
86}
packages/sources/core/src/index.ts+1−0
@@ -3,6 +3,7 @@ export * from "./catalog-sync-result"; · 2 unmodified lines
3export * from "./discovery";3export * from "./discovery";
4export * from "./discovery-models";4export * from "./discovery-models";
5export * from "./effect-errors";5export * from "./effect-errors";
6export * from "./favicon";
6export * from "./shared";7export * from "./shared";
7export * from "./source-models";8export * from "./source-models";
8export * from "./types";9export * from "./types";

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