Browse Source

cache failed favicons

imwald
Silberengel 3 weeks ago
parent
commit
f9137ef702
  1. 19
      src/components/Favicon/index.tsx
  2. 20
      src/lib/favicon-fail-cache.test.ts
  3. 20
      src/lib/favicon-fail-cache.ts

19
src/components/Favicon/index.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { isFaviconLoadFailed, markFaviconLoadFailed, normalizeFaviconDomain } from '@/lib/favicon-fail-cache'
import { cn } from '@/lib/utils'
import { useState } from 'react'
@ -10,19 +11,23 @@ export function Favicon({ @@ -10,19 +11,23 @@ export function Favicon({
className?: string
fallback?: React.ReactNode
}) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
const trimmed = domain?.trim() ?? ''
if (error || !trimmed) return fallback
const host = normalizeFaviconDomain(domain)
const knownFailed = host ? isFaviconLoadFailed(host) : true
const [loading, setLoading] = useState(!knownFailed)
const [error, setError] = useState(knownFailed)
if (error || !host) return fallback
return (
<div className={cn('relative', className)}>
{loading && <div className={cn('absolute inset-0', className)}>{fallback}</div>}
<img
src={`https://${trimmed}/favicon.ico`}
alt={trimmed}
src={`https://${host}/favicon.ico`}
alt={host}
className={cn('absolute inset-0', loading && 'opacity-0', className)}
onError={() => setError(true)}
onError={() => {
markFaviconLoadFailed(host)
setError(true)
}}
onLoad={() => setLoading(false)}
/>
</div>

20
src/lib/favicon-fail-cache.test.ts

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest'
import {
isFaviconLoadFailed,
markFaviconLoadFailed,
normalizeFaviconDomain
} from '@/lib/favicon-fail-cache'
describe('favicon-fail-cache', () => {
it('normalizes domain casing and trailing dot', () => {
expect(normalizeFaviconDomain(' Example.COM. ')).toBe('example.com')
})
it('remembers failed domains for the session', () => {
const host = `fail-cache-test-${Date.now()}.example`
expect(isFaviconLoadFailed(host)).toBe(false)
markFaviconLoadFailed(host)
expect(isFaviconLoadFailed(host)).toBe(true)
expect(isFaviconLoadFailed(host.toUpperCase())).toBe(true)
})
})

20
src/lib/favicon-fail-cache.ts

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
import { LRUCache } from 'lru-cache'
/** Domains whose `https://{host}/favicon.ico` already failed — skip repeat network requests. */
const FAILED_FAVICON_DOMAINS = new LRUCache<string, true>({ max: 512 })
export function normalizeFaviconDomain(domain: string): string {
return domain.trim().toLowerCase().replace(/\.$/, '')
}
export function isFaviconLoadFailed(domain: string): boolean {
const key = normalizeFaviconDomain(domain)
if (!key) return true
return FAILED_FAVICON_DOMAINS.has(key)
}
export function markFaviconLoadFailed(domain: string): void {
const key = normalizeFaviconDomain(domain)
if (!key) return
FAILED_FAVICON_DOMAINS.set(key, true)
}
Loading…
Cancel
Save