diff --git a/app/api/favicon/route.ts b/app/api/favicon/route.ts index a3bad1ca2..0382b454f 100644 --- a/app/api/favicon/route.ts +++ b/app/api/favicon/route.ts @@ -57,6 +57,20 @@ function resolveFaviconUrl(rawUrl: string, requestUrl: string) { } } +function uniqueNonEmptyValues(values: string[]) { + const seen = new Set(); + const result: string[] = []; + + for (const value of values) { + const trimmed = value.trim(); + if (!trimmed || seen.has(trimmed)) continue; + seen.add(trimmed); + result.push(trimmed); + } + + return result; +} + async function readLocalFallbackFavicon(preferDark = false) { const file = await readFile( join(process.cwd(), "public", "default-favicon.ico") @@ -142,71 +156,103 @@ export async function GET(request: Request) { const requestUrl = new URL(request.url); const preferDark = requestUrl.searchParams.get("theme") === "dark"; const settings = await getSiteSettings(); - const rawFaviconUrl = String( - (preferDark && settings?.siteLogoDark) || settings?.favicon || "" - ).trim(); + const configuredFavicon = String(settings?.favicon || "").trim(); + const configuredLightLogo = String(settings?.siteLogo || "").trim(); + const configuredDarkLogo = String(settings?.siteLogoDark || "").trim(); + const rawFaviconCandidates = preferDark + ? uniqueNonEmptyValues([ + configuredFavicon, + configuredDarkLogo, + configuredLightLogo, + ]) + : uniqueNonEmptyValues([ + configuredFavicon, + configuredLightLogo, + configuredDarkLogo, + ]); - if (!rawFaviconUrl) { + if (rawFaviconCandidates.length === 0) { return await readLocalFallbackFavicon(preferDark); } - const faviconUrl = resolveFaviconUrl(rawFaviconUrl, request.url); - if (!faviconUrl) { - console.error("[favicon] Invalid favicon URL:", rawFaviconUrl); - return await readLocalFallbackFavicon(preferDark); - } + for (const rawFaviconUrl of rawFaviconCandidates) { + const faviconUrl = resolveFaviconUrl(rawFaviconUrl, request.url); + if (!faviconUrl) { + console.error("[favicon] Invalid favicon URL:", rawFaviconUrl); + continue; + } - const targetUrl = `${faviconUrl}${faviconUrl.includes("?") ? "&" : "?"}v=${Date.now()}`; - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + const targetUrl = `${faviconUrl}${faviconUrl.includes("?") ? "&" : "?"}v=${Date.now()}`; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); - let upstream: Response; - try { - upstream = await fetch(targetUrl, { - cache: "no-store", - signal: controller.signal, - redirect: "follow", - headers: { - Accept: "image/*,*/*;q=0.8", - "User-Agent": "Mozilla/5.0 (compatible; BuyiAIFavicon/1.0)", - }, + let upstream: Response; + try { + upstream = await fetch(targetUrl, { + cache: "no-store", + signal: controller.signal, + redirect: "follow", + headers: { + Accept: "image/*,*/*;q=0.8", + "User-Agent": "Mozilla/5.0 (compatible; BuyiAIFavicon/1.0)", + }, + }); + } catch (error) { + console.error("[favicon] Candidate request failed:", { + targetUrl, + error, + }); + continue; + } finally { + clearTimeout(timeout); + } + + if (!upstream.ok) { + console.error("[favicon] Upstream request failed:", { + targetUrl, + status: upstream.status, + statusText: upstream.statusText, + }); + continue; + } + + const buffer = await upstream.arrayBuffer(); + const contentType = + upstream.headers.get("content-type") || inferContentType(faviconUrl); + + if (preferDark) { + const transformed = await transformFaviconForDark(buffer).catch((error) => { + console.error("[favicon] Failed to transform dark favicon candidate:", { + targetUrl, + error, + }); + return null; + }); + if (!transformed) continue; + + return new NextResponse(transformed, { + status: 200, + headers: noStoreHeaders(DARK_FAVICON_CONTENT_TYPE), + }); + } + + return new NextResponse(buffer, { + status: 200, + headers: noStoreHeaders(contentType), }); - } finally { - clearTimeout(timeout); } - if (!upstream.ok) { - console.error("[favicon] Upstream request failed:", { - targetUrl, - status: upstream.status, - statusText: upstream.statusText, - }); - return await readLocalFallbackFavicon(preferDark); - } - - const buffer = await upstream.arrayBuffer(); - const contentType = - upstream.headers.get("content-type") || inferContentType(faviconUrl); - if (preferDark) { - const transformed = await transformFaviconForDark(buffer).catch((error) => { - console.error("[favicon] Failed to transform dark favicon:", error); - return null; - }); - const darkFavicon = - transformed || (await renderDarkFallbackFavicon().catch(() => null)); - if (darkFavicon) { - return new NextResponse(darkFavicon, { + const fallback = await renderDarkFallbackFavicon().catch(() => null); + if (fallback) { + return new NextResponse(fallback, { status: 200, headers: noStoreHeaders(DARK_FAVICON_CONTENT_TYPE), }); } } - return new NextResponse(buffer, { - status: 200, - headers: noStoreHeaders(contentType), - }); + return await readLocalFallbackFavicon(preferDark); } catch (error) { console.error("[favicon] Failed to proxy favicon:", error); const preferDark = new URL(request.url).searchParams.get("theme") === "dark";