fix: honor custom branding favicon sources

This commit is contained in:
134 2026-06-22 13:26:54 +08:00
parent 7fddbe68bc
commit 25df2d248b

View File

@ -57,6 +57,20 @@ function resolveFaviconUrl(rawUrl: string, requestUrl: string) {
}
}
function uniqueNonEmptyValues(values: string[]) {
const seen = new Set<string>();
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";