From 75bf8acd1e516b1ef8e0cfaac96988c617808fd0 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Mon, 23 Mar 2026 13:21:15 +0100 Subject: [PATCH] refactor(referrers): unify icon, display name, and favicon into single registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace three separate data structures (getReferrerIcon if-chain, REFERRER_DISPLAY_OVERRIDES, REFERRER_PREFER_ICON) with a single REFERRER_REGISTRY. All matching is now exact key/hostname lookup via resolveReferrer() — no more substring includes() that caused collisions like t.co matching reddit.com. --- lib/utils/icons.tsx | 329 ++++++++++++++++++++++---------------------- 1 file changed, 162 insertions(+), 167 deletions(-) diff --git a/lib/utils/icons.tsx b/lib/utils/icons.tsx index a4e836c..d6a3d8d 100644 --- a/lib/utils/icons.tsx +++ b/lib/utils/icons.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { type ReactNode } from 'react' import { Globe, Question, @@ -49,6 +49,8 @@ function BingIcon({ size = 16, color = '#258FFA' }: { size?: number; color?: str */ export const FAVICON_SERVICE_URL = 'https://www.google.com/s2/favicons' +// ─── Browser, OS, Device icons (unchanged) ─────────────────────────────────── + const BROWSER_ICON_MAP: Record = { 'chrome': { file: 'chrome', ext: 'svg' }, 'firefox': { file: 'firefox', ext: 'svg' }, @@ -118,49 +120,94 @@ export function getDeviceIcon(deviceName: string) { return } +// ─── Referrer Registry ─────────────────────────────────────────────────────── + const SI = { size: 16 } as const -export function getReferrerIcon(referrerName: string) { - if (!referrerName) return - const lower = referrerName.toLowerCase() - // Direct traffic - if (lower === 'direct') return - // Browsers as referrers (e.g. googlechrome.com, firefox.com) - if (lower.includes('googlechrome') || lower.includes('chrome')) return Chrome - // Social / platforms - if (lower.includes('google') && !lower.includes('gemini')) return - if (lower.includes('facebook') || lower === 'fb') return - if (lower.includes('twitter') || lower === 't.co' || lower.includes('t.co/') || lower.includes('x.com')) return - if (lower.includes('linkedin')) return - if (lower.includes('instagram') || lower === 'ig') return - if (lower.includes('github')) return - if (lower.includes('youtube')) return - if (lower.includes('reddit')) return - if (lower.includes('whatsapp')) return - if (lower.includes('telegram')) return - if (lower.includes('snapchat')) return - if (lower.includes('pinterest')) return - if (lower.includes('threads')) return - if (lower.includes('discord')) return - // Search engines - if (lower.includes('bing')) return - if (lower.includes('duckduckgo')) return - if (lower.includes('brave')) return - // AI assistants - if (lower.includes('chatgpt') || lower.includes('openai')) return - if (lower.includes('perplexity')) return - if (lower.includes('claude') || lower.includes('anthropic')) return - if (lower.includes('gemini')) return - if (lower.includes('copilot')) return - if (lower.includes('deepseek')) return - if (lower.includes('grok') || lower.includes('x.ai')) return - // Shared Link - if (lower === 'shared link') return - - return +interface ReferrerEntry { + display: string + icon: () => ReactNode + hostnames?: string[] + aliases?: string[] } -const REFERRER_NO_FAVICON = ['direct', 'shared link', 'unknown', ''] +/** + * Single source of truth for all known referrer brands. + * Key = canonical label (what getReferrerLabel extracts from hostnames). + * Adding a new brand = adding one entry here. Nothing else. + */ +const REFERRER_REGISTRY: Record = { + // ── Special ── + direct: { display: 'Direct', icon: () => }, + 'shared link': { display: 'Shared Link', icon: () => }, + + // ── Social / platforms ── + google: { display: 'Google', icon: () => }, + facebook: { display: 'Facebook', icon: () => , aliases: ['fb'] }, + x: { display: 'X', icon: () => , hostnames: ['t.co', 'x.com', 'twitter.com'] }, + linkedin: { display: 'LinkedIn', icon: () => }, + instagram: { display: 'Instagram', icon: () => , aliases: ['ig'] }, + github: { display: 'GitHub', icon: () => }, + youtube: { display: 'YouTube', icon: () => , aliases: ['yt'] }, + reddit: { display: 'Reddit', icon: () => }, + whatsapp: { display: 'WhatsApp', icon: () => }, + telegram: { display: 'Telegram', icon: () => , hostnames: ['t.me'] }, + snapchat: { display: 'Snapchat', icon: () => }, + pinterest: { display: 'Pinterest', icon: () => }, + threads: { display: 'Threads', icon: () => }, + discord: { display: 'Discord', icon: () => }, + tumblr: { display: 'Tumblr', icon: () => }, + quora: { display: 'Quora', icon: () => }, + + // ── Search engines ── + bing: { display: 'Bing', icon: () => }, + duckduckgo: { display: 'DuckDuckGo', icon: () => }, + brave: { display: 'Brave', icon: () => }, + + // ── AI assistants ── + chatgpt: { display: 'ChatGPT', icon: () => , hostnames: ['chat.openai.com', 'openai.com'] }, + perplexity: { display: 'Perplexity', icon: () => }, + claude: { display: 'Claude', icon: () => , hostnames: ['anthropic.com'] }, + gemini: { display: 'Gemini', icon: () => , hostnames: ['gemini.google.com'] }, + copilot: { display: 'Copilot', icon: () => , hostnames: ['copilot.microsoft.com'] }, + deepseek: { display: 'DeepSeek', icon: () => , hostnames: ['chat.deepseek.com'] }, + grok: { display: 'Grok', icon: () => , hostnames: ['grok.x.ai', 'x.ai'] }, + you: { display: 'You.com', icon: () => }, + phind: { display: 'Phind', icon: () => }, + + // ── Browsers as referrers ── + googlechrome: { display: 'Google Chrome', icon: () => Chrome, hostnames: ['googlechrome.github.io'] }, +} + +// ── Derived lookup maps (built once at module load) ── + +/** alias → registry key (e.g. "ig" → "instagram", "fb" → "facebook") */ +const ALIAS_TO_KEY: Record = {} + +/** exact hostname → registry key (e.g. "t.co" → "x", "t.me" → "telegram") */ +const HOSTNAME_TO_KEY: Record = {} + +/** All known hostnames — union of auto-derived (key + ".com") and explicit hostnames */ +const ALL_KNOWN_HOSTNAMES = new Set() + +for (const [key, entry] of Object.entries(REFERRER_REGISTRY)) { + if (entry.aliases) { + for (const alias of entry.aliases) { + ALIAS_TO_KEY[alias] = key + } + } + if (entry.hostnames) { + for (const hostname of entry.hostnames) { + HOSTNAME_TO_KEY[hostname] = key + ALL_KNOWN_HOSTNAMES.add(hostname) + } + } + // Auto-derive common hostnames from the key itself + ALL_KNOWN_HOSTNAMES.add(`${key}.com`) + ALL_KNOWN_HOSTNAMES.add(`www.${key}.com`) +} + +// ── Referrer resolution ── /** Common subdomains to skip when deriving the main label (e.g. l.instagram.com → instagram). */ const REFERRER_SUBDOMAIN_SKIP = new Set([ @@ -168,59 +215,7 @@ const REFERRER_SUBDOMAIN_SKIP = new Set([ 'sub', 'api', 'static', 'cdn', 'blog', 'shop', 'support', 'help', 'link', ]) -/** - * Override map for display names when the heuristic would be wrong (casing or brand alias). - * Keys: lowercase label or hostname. Values: exact display name. - */ -const REFERRER_DISPLAY_OVERRIDES: Record = { - chatgpt: 'ChatGPT', - linkedin: 'LinkedIn', - youtube: 'YouTube', - reddit: 'Reddit', - github: 'GitHub', - bing: 'Bing', - brave: 'Brave', - duckduckgo: 'DuckDuckGo', - whatsapp: 'WhatsApp', - telegram: 'Telegram', - pinterest: 'Pinterest', - snapchat: 'Snapchat', - threads: 'Threads', - tumblr: 'Tumblr', - quora: 'Quora', - ig: 'Instagram', - fb: 'Facebook', - yt: 'YouTube', - googlechrome: 'Google Chrome', - 't.co': 'X', - 'x.com': 'X', - // AI assistants and search tools - openai: 'ChatGPT', - perplexity: 'Perplexity', - claude: 'Claude', - anthropic: 'Claude', - gemini: 'Gemini', - copilot: 'Copilot', - deepseek: 'DeepSeek', - grok: 'Grok', - 'you': 'You.com', - phind: 'Phind', -} - -/** - * Returns the hostname for a referrer string (URL or plain hostname), or null if invalid. - */ -function getReferrerHostname(referrer: string): string | null { - if (!referrer || typeof referrer !== 'string') return null - const trimmed = referrer.trim() - if (REFERRER_NO_FAVICON.includes(trimmed.toLowerCase())) return null - try { - const url = new URL(trimmed.startsWith('http') ? trimmed : `https://${trimmed}`) - return url.hostname.toLowerCase() - } catch { - return null - } -} +const REFERRER_NO_FAVICON = new Set(['direct', 'shared link', 'unknown', '']) /** * Derives the main label from a hostname (e.g. "l.instagram.com" → "instagram", "google.com" → "google"). @@ -234,32 +229,91 @@ function getReferrerLabel(hostname: string): string { return parts[0] ?? withoutWww } +function getReferrerHostname(referrer: string): string | null { + if (!referrer || typeof referrer !== 'string') return null + const trimmed = referrer.trim() + if (REFERRER_NO_FAVICON.has(trimmed.toLowerCase())) return null + try { + const url = new URL(trimmed.startsWith('http') ? trimmed : `https://${trimmed}`) + return url.hostname.toLowerCase() + } catch { + return null + } +} + +/** + * Resolves a raw referrer string to a registry entry. + * Returns null if no known brand matches (unknown domain → use favicon service). + */ +function resolveReferrer(referrer: string): ReferrerEntry | null { + if (!referrer || typeof referrer !== 'string') return null + const lower = referrer.trim().toLowerCase() + + // 1. Exact registry key match (e.g. "Direct", "Reddit", "Google") + if (REFERRER_REGISTRY[lower]) return REFERRER_REGISTRY[lower] + + // 2. Alias match (e.g. "ig" → instagram, "fb" → facebook) + const aliasKey = ALIAS_TO_KEY[lower] + if (aliasKey) return REFERRER_REGISTRY[aliasKey] + + // 3. Hostname-based matching + const hostname = getReferrerHostname(referrer) + if (!hostname) return null + + // 3a. Exact hostname match (e.g. "t.co" → x, "t.me" → telegram) + const hostnameKey = HOSTNAME_TO_KEY[hostname] + if (hostnameKey) return REFERRER_REGISTRY[hostnameKey] + + // 3b. Label-based lookup (e.g. "old.reddit.com" → label "reddit" → registry hit) + const label = getReferrerLabel(hostname) + if (REFERRER_REGISTRY[label]) return REFERRER_REGISTRY[label] + + // 3c. Check alias from label (e.g. hostname "ig.something.com" → label "ig" → alias → instagram) + const labelAliasKey = ALIAS_TO_KEY[label] + if (labelAliasKey) return REFERRER_REGISTRY[labelAliasKey] + + return null +} + +// ── Public API (same signatures as before) ── + +export function getReferrerIcon(referrerName: string): ReactNode { + if (!referrerName) return + const entry = resolveReferrer(referrerName) + if (entry) return entry.icon() + return +} + function capitalizeLabel(label: string): string { if (!label) return label return label.charAt(0).toUpperCase() + label.slice(1).toLowerCase() } -/** - * Returns a friendly display name for the referrer (e.g. "Google" instead of "google.com"). - * Uses a heuristic (hostname → main label → capitalize) plus a small override map for famous brands. - */ export function getReferrerDisplayName(referrer: string): string { if (!referrer || typeof referrer !== 'string') return referrer || '' const trimmed = referrer.trim() if (trimmed === '') return '' + const entry = resolveReferrer(trimmed) + if (entry) return entry.display + // Unknown referrer — derive display name from hostname const hostname = getReferrerHostname(trimmed) - if (!hostname) { - // Plain names without a dot (e.g. "Ig", "Direct") — check override map before returning raw - const overrideByPlain = REFERRER_DISPLAY_OVERRIDES[trimmed.toLowerCase()] - if (overrideByPlain) return overrideByPlain - return trimmed + if (!hostname) return trimmed + return capitalizeLabel(getReferrerLabel(hostname)) +} + +export function getReferrerFavicon(referrer: string): string | null { + if (!referrer || typeof referrer !== 'string') return null + const normalized = referrer.trim().toLowerCase() + if (REFERRER_NO_FAVICON.has(normalized)) return null + if (!normalized.includes('.')) return null + // Known brand → skip favicon service, use registry icon + if (resolveReferrer(referrer)) return null + try { + const url = new URL(referrer.startsWith('http') ? referrer : `https://${referrer}`) + return `${FAVICON_SERVICE_URL}?domain=${url.hostname.toLowerCase()}&sz=32` + } catch { + return null } - const overrideByHostname = REFERRER_DISPLAY_OVERRIDES[hostname] - if (overrideByHostname) return overrideByHostname - const label = getReferrerLabel(hostname) - const overrideByLabel = REFERRER_DISPLAY_OVERRIDES[label] - if (overrideByLabel) return overrideByLabel - return capitalizeLabel(label) } /** @@ -287,62 +341,3 @@ export function mergeReferrersByDisplayName( .map(({ referrer, pageviews }) => ({ referrer, pageviews })) .sort((a, b) => b.pageviews - a.pageviews) } - -/** - * Domains/labels where the Phosphor icon is better than Google's favicon service. - * For these, getReferrerFavicon returns null so the caller falls back to getReferrerIcon. - */ -const REFERRER_PREFER_ICON = new Set([ - // Social / platforms - 't.co', 'x.com', 'twitter.com', 'www.twitter.com', - 'google.com', 'www.google.com', - 'facebook.com', 'www.facebook.com', 'm.facebook.com', 'l.facebook.com', - 'instagram.com', 'www.instagram.com', 'l.instagram.com', - 'linkedin.com', 'www.linkedin.com', - 'github.com', 'www.github.com', - 'youtube.com', 'www.youtube.com', 'm.youtube.com', - 'reddit.com', 'www.reddit.com', 'old.reddit.com', - 'whatsapp.com', 'www.whatsapp.com', 'web.whatsapp.com', - 'telegram.org', 'web.telegram.org', 't.me', - 'snapchat.com', 'www.snapchat.com', - 'pinterest.com', 'www.pinterest.com', - 'threads.net', 'www.threads.net', - // Search engines - 'bing.com', 'www.bing.com', - 'duckduckgo.com', 'www.duckduckgo.com', - 'search.brave.com', 'brave.com', - // AI assistants - 'chatgpt.com', 'chat.openai.com', 'openai.com', - 'perplexity.ai', 'www.perplexity.ai', - 'claude.ai', 'www.claude.ai', 'anthropic.com', - 'gemini.google.com', - 'copilot.microsoft.com', - 'deepseek.com', 'chat.deepseek.com', - 'grok.x.ai', 'x.ai', - 'phind.com', 'www.phind.com', - 'you.com', 'www.you.com', -]) - -/** - * Returns a favicon URL for the referrer's domain, or null for non-URL referrers - * (e.g. "Direct", "Unknown") or known services where the Phosphor icon is better. - */ -export function getReferrerFavicon(referrer: string): string | null { - if (!referrer || typeof referrer !== 'string') return null - const normalized = referrer.trim().toLowerCase() - if (REFERRER_NO_FAVICON.includes(normalized)) return null - // Plain names without a dot (e.g. "Instagram", "WhatsApp") are not real domains - if (!normalized.includes('.')) return null - try { - const url = new URL(referrer.startsWith('http') ? referrer : `https://${referrer}`) - const hostname = url.hostname.toLowerCase() - // Use Phosphor icon for known services — Google favicons are unreliable for these - if (REFERRER_PREFER_ICON.has(hostname)) return null - // Also check if the label matches a known referrer (catches subdomains like search.google.com) - const label = getReferrerLabel(hostname) - if (REFERRER_DISPLAY_OVERRIDES[label]) return null - return `${FAVICON_SERVICE_URL}?domain=${hostname}&sz=32` - } catch { - return null - } -}