fix: honor custom branding favicon sources
This commit is contained in:
parent
7fddbe68bc
commit
25df2d248b
@ -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";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user