修复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 15:33:48 +08:00
parent 6cac41f1c0
commit ec8959af19
3 changed files with 97 additions and 43 deletions

65
app/api/favicon/route.ts Normal file
View File

@ -0,0 +1,65 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { NextResponse } from "next/server";
import { getSiteSettings } from "@/lib/db/queries";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export const revalidate = 0;
const DEFAULT_CONTENT_TYPE = "image/x-icon";
function noStoreHeaders(contentType: string) {
return {
"Content-Type": contentType,
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
Pragma: "no-cache",
Expires: "0",
};
}
function inferContentType(url: string) {
const lower = url.toLowerCase();
if (lower.endsWith(".png")) return "image/png";
if (lower.endsWith(".svg")) return "image/svg+xml";
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
if (lower.endsWith(".webp")) return "image/webp";
return DEFAULT_CONTENT_TYPE;
}
async function readLocalFallbackFavicon() {
const file = await readFile(join(process.cwd(), "app", "favicon.ico"));
return new NextResponse(file, {
status: 200,
headers: noStoreHeaders(DEFAULT_CONTENT_TYPE),
});
}
export async function GET() {
try {
const settings = await getSiteSettings();
const faviconUrl = String(settings?.favicon || "").trim();
if (!faviconUrl) {
return await readLocalFallbackFavicon();
}
const targetUrl = `${faviconUrl}${faviconUrl.includes("?") ? "&" : "?"}v=${Date.now()}`;
const upstream = await fetch(targetUrl, { cache: "no-store" });
if (!upstream.ok) {
return await readLocalFallbackFavicon();
}
const buffer = await upstream.arrayBuffer();
const contentType =
upstream.headers.get("content-type") || inferContentType(faviconUrl);
return new NextResponse(buffer, {
status: 200,
headers: noStoreHeaders(contentType),
});
} catch (_error) {
return await readLocalFallbackFavicon();
}
}

View File

@ -11,11 +11,7 @@ import { getSiteSettings } from "@/lib/db/queries";
export async function generateMetadata(): Promise<Metadata> {
const settings = await getSiteSettings();
// 处理 favicon URL,添加缓存破坏参数确保更新
const faviconUrl = settings.favicon
? `${settings.favicon}${settings.favicon.includes('?') ? '&' : '?'}v=${Date.now()}`
: "/favicon.ico";
const faviconUrl = "/api/favicon";
return {
title: {

View File

@ -6,47 +6,40 @@ export function FaviconUpdater() {
useEffect(() => {
let isDisposed = false;
// 从 API 获取最新的 favicon 设置
fetch("/api/site-settings")
.then((res) => res.json())
.then((settings) => {
if (!settings.favicon || isDisposed) {
return;
const timestamp = Date.now();
const urlWithCache = `/api/favicon?v=${timestamp}`;
Promise.resolve().then(() => {
if (isDisposed) {
return;
}
// 仅处理本组件创建的 link,避免删除 Next/React 管理的 head 节点
const oldLinks = document.querySelectorAll(
'link[data-managed-favicon="true"]'
);
oldLinks.forEach((link) => {
if (link.parentNode) {
link.parentNode.removeChild(link);
}
const faviconUrl = settings.favicon;
const timestamp = Date.now();
const urlWithCache = `${faviconUrl}${faviconUrl.includes("?") ? "&" : "?"}v=${timestamp}`;
// 仅处理本组件创建的 link,避免删除 Next/React 管理的 head 节点
const oldLinks = document.querySelectorAll(
'link[data-managed-favicon="true"]'
);
oldLinks.forEach((link) => {
if (link.parentNode) {
link.parentNode.removeChild(link);
}
});
const createManagedLink = (rel: string, href: string, type?: string) => {
const link = document.createElement("link");
link.rel = rel;
link.href = href;
link.setAttribute("data-managed-favicon", "true");
if (type) {
link.type = type;
}
document.head.appendChild(link);
};
createManagedLink("icon", urlWithCache, "image/x-icon");
createManagedLink("apple-touch-icon", urlWithCache);
createManagedLink("shortcut icon", urlWithCache);
})
.catch((error) => {
console.error("[Favicon] Error updating favicon:", error);
});
const createManagedLink = (rel: string, href: string, type?: string) => {
const link = document.createElement("link");
link.rel = rel;
link.href = href;
link.setAttribute("data-managed-favicon", "true");
if (type) {
link.type = type;
}
document.head.appendChild(link);
};
createManagedLink("icon", urlWithCache, "image/x-icon");
createManagedLink("apple-touch-icon", urlWithCache);
createManagedLink("shortcut icon", urlWithCache);
});
return () => {
isDisposed = true;
};