修复网站设置favicon不生效与缓存命中默认图标
This commit is contained in:
parent
f2b12b251d
commit
d05167103b
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user