修复网站设置favicon不生效与缓存命中默认图标
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-14 21:30:06 +08:00
parent f2b12b251d
commit d05167103b
3 changed files with 97 additions and 19 deletions

View File

@ -8,6 +8,7 @@ export const dynamic = "force-dynamic";
export const revalidate = 0;
const DEFAULT_CONTENT_TYPE = "image/x-icon";
const FETCH_TIMEOUT_MS = 10_000;
function noStoreHeaders(contentType: string) {
return {
@ -27,6 +28,34 @@ function inferContentType(url: string) {
return DEFAULT_CONTENT_TYPE;
}
function resolveFaviconUrl(rawUrl: string, requestUrl: string) {
const input = rawUrl.trim();
if (!input) {
return null;
}
try {
return new URL(input).toString();
} catch {
// ignore and try resolving relative path
}
const authUrl = process.env.AUTH_URL?.trim();
if (authUrl) {
try {
return new URL(input, authUrl).toString();
} catch {
// ignore and fallback to request origin
}
}
try {
return new URL(input, requestUrl).toString();
} catch {
return null;
}
}
async function readLocalFallbackFavicon() {
const file = await readFile(join(process.cwd(), "app", "favicon.ico"));
return new NextResponse(file, {
@ -35,19 +64,46 @@ async function readLocalFallbackFavicon() {
});
}
export async function GET() {
export async function GET(request: Request) {
try {
const settings = await getSiteSettings();
const faviconUrl = String(settings?.favicon || "").trim();
const rawFaviconUrl = String(settings?.favicon || "").trim();
if (!rawFaviconUrl) {
return await readLocalFallbackFavicon();
}
const faviconUrl = resolveFaviconUrl(rawFaviconUrl, request.url);
if (!faviconUrl) {
console.error("[favicon] Invalid favicon URL:", rawFaviconUrl);
return await readLocalFallbackFavicon();
}
const targetUrl = `${faviconUrl}${faviconUrl.includes("?") ? "&" : "?"}v=${Date.now()}`;
const upstream = await fetch(targetUrl, { cache: "no-store" });
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)",
},
});
} finally {
clearTimeout(timeout);
}
if (!upstream.ok) {
console.error("[favicon] Upstream request failed:", {
targetUrl,
status: upstream.status,
statusText: upstream.statusText,
});
return await readLocalFallbackFavicon();
}
@ -59,7 +115,8 @@ export async function GET() {
status: 200,
headers: noStoreHeaders(contentType),
});
} catch (_error) {
} catch (error) {
console.error("[favicon] Failed to proxy favicon:", error);
return await readLocalFallbackFavicon();
}
}

View File

@ -11,9 +11,17 @@ import { getSiteSettings } from "@/lib/db/queries";
export async function generateMetadata(): Promise<Metadata> {
const settings = await getSiteSettings();
const faviconUrl = "/api/favicon";
const updatedAt =
"updatedAt" in settings && settings.updatedAt
? new Date(settings.updatedAt).getTime()
: Date.now();
const faviconUrl = `/api/favicon?v=${updatedAt}`;
const metadataBase = process.env.AUTH_URL
? new URL(process.env.AUTH_URL)
: undefined;
return {
metadataBase,
title: {
template: `%s | ${settings.siteName}`,
default: settings.siteName,

View File

@ -14,30 +14,43 @@ export function FaviconUpdater() {
return;
}
// 仅处理本组件创建的 link,避免删除 Next/React 管理的 head 节点
const oldLinks = document.querySelectorAll(
'link[data-managed-favicon="true"]'
const managedSelector =
'link[rel="icon"],link[rel="shortcut icon"],link[rel="apple-touch-icon"]';
const existingLinks = document.querySelectorAll<HTMLLinkElement>(
managedSelector
);
oldLinks.forEach((link) => {
if (link.parentNode) {
link.parentNode.removeChild(link);
existingLinks.forEach((link) => {
link.href = urlWithCache;
link.setAttribute("data-managed-favicon", "true");
if (link.rel === "icon") {
link.type = "image/x-icon";
link.setAttribute("sizes", "any");
}
});
const createManagedLink = (rel: string, href: string, type?: string) => {
const link = document.createElement("link");
link.rel = rel;
link.href = href;
const ensureLink = (rel: string, type?: string) => {
let link = document.querySelector(
`link[rel="${rel}"]`
) as HTMLLinkElement | null;
if (!link) {
link = document.createElement("link");
link.rel = rel;
document.head.appendChild(link);
}
link.href = urlWithCache;
link.setAttribute("data-managed-favicon", "true");
if (type) {
link.type = type;
}
document.head.appendChild(link);
if (rel === "icon") {
link.setAttribute("sizes", "any");
}
};
createManagedLink("icon", urlWithCache, "image/x-icon");
createManagedLink("apple-touch-icon", urlWithCache);
createManagedLink("shortcut icon", urlWithCache);
ensureLink("icon", "image/x-icon");
ensureLink("apple-touch-icon");
ensureLink("shortcut icon");
});
return () => {