diff --git a/CHANGELOG.md b/CHANGELOG.md index e96b62c..e8f8404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to Pulse (frontend and product) are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and Pulse uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html) with a **0.x.y** version scheme while in initial development. The leading `0` indicates that the public API and behaviour may change until we release **1.0.0**. +## [0.3.0-alpha] - 2026-02-11 + +### Changed + +- **Top Referrers favicons (PULSE-52).** The Top Referrers card now shows real site favicons (e.g. Google, ChatGPT, Instagram) when the referrer is a domain or URL. “Direct” and “Unknown” keep the globe icon; if a favicon fails to load, the previous icon is shown as fallback. + ## [0.2.0-alpha] - 2026-02-11 ### Added @@ -21,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), --- -[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.2.0-alpha...HEAD +[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.3.0-alpha...HEAD +[0.3.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.2.0-alpha...v0.3.0-alpha [0.2.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.1.0-alpha...v0.2.0-alpha [0.1.0-alpha]: https://github.com/ciphera-net/pulse/releases/tag/v0.1.0-alpha diff --git a/components/dashboard/TopReferrers.tsx b/components/dashboard/TopReferrers.tsx index 9d43b1c..4974f5d 100644 --- a/components/dashboard/TopReferrers.tsx +++ b/components/dashboard/TopReferrers.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { formatNumber } from '@/lib/utils/format' -import { getReferrerIcon } from '@/lib/utils/icons' +import { getReferrerFavicon, getReferrerIcon } from '@/lib/utils/icons' import { Modal, GlobeIcon } from '@ciphera-net/ui' import { getTopReferrers, TopReferrer } from '@/lib/api/stats' @@ -19,6 +19,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI const [isModalOpen, setIsModalOpen] = useState(false) const [fullData, setFullData] = useState([]) const [isLoadingFull, setIsLoadingFull] = useState(false) + const [faviconFailed, setFaviconFailed] = useState>(new Set()) // Filter out empty/unknown referrers const filteredReferrers = (referrers || []).filter( @@ -30,6 +31,22 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI const emptySlots = Math.max(0, LIMIT - displayedReferrers.length) const showViewAll = hasData && filteredReferrers.length > LIMIT + function renderReferrerIcon(referrer: string) { + const faviconUrl = getReferrerFavicon(referrer) + const useFavicon = faviconUrl && !faviconFailed.has(referrer) + if (useFavicon) { + return ( + setFaviconFailed((prev) => new Set(prev).add(referrer))} + /> + ) + } + return {getReferrerIcon(referrer)} + } + useEffect(() => { if (isModalOpen) { const fetchData = async () => { @@ -80,7 +97,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI {displayedReferrers.map((ref, index) => (
- {getReferrerIcon(ref.referrer)} + {renderReferrerIcon(ref.referrer)} {ref.referrer}
@@ -123,7 +140,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI (fullData.length > 0 ? fullData : filteredReferrers).map((ref, index) => (
- {getReferrerIcon(ref.referrer)} + {renderReferrerIcon(ref.referrer)} {ref.referrer}
diff --git a/lib/utils/icons.tsx b/lib/utils/icons.tsx index 078b6f2..7e93641 100644 --- a/lib/utils/icons.tsx +++ b/lib/utils/icons.tsx @@ -78,11 +78,20 @@ export function getReferrerIcon(referrerName: string) { return } -export function getReferrerFavicon(referrer: string) { - try { - const url = new URL(referrer.startsWith('http') ? referrer : `https://${referrer}`); - return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`; - } catch (e) { - return null; - } +const REFERRER_NO_FAVICON = ['direct', 'unknown', ''] + +/** + * Returns a favicon URL for the referrer's domain, or null for non-URL referrers + * (e.g. "Direct", "Unknown") so callers can show an icon fallback instead. + */ +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 + try { + const url = new URL(referrer.startsWith('http') ? referrer : `https://${referrer}`) + return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32` + } catch { + return null + } } diff --git a/package.json b/package.json index 98ab95e..97b583e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-frontend", - "version": "0.1.0", + "version": "0.3.0-alpha", "private": true, "scripts": { "dev": "next dev",