修复网站设置favicon不生效与缓存命中默认图标
This commit is contained in:
parent
f2b12b251d
commit
d05167103b
@ -8,6 +8,7 @@ export const dynamic = "force-dynamic";
|
|||||||
export const revalidate = 0;
|
export const revalidate = 0;
|
||||||
|
|
||||||
const DEFAULT_CONTENT_TYPE = "image/x-icon";
|
const DEFAULT_CONTENT_TYPE = "image/x-icon";
|
||||||
|
const FETCH_TIMEOUT_MS = 10_000;
|
||||||
|
|
||||||
function noStoreHeaders(contentType: string) {
|
function noStoreHeaders(contentType: string) {
|
||||||
return {
|
return {
|
||||||
@ -27,6 +28,34 @@ function inferContentType(url: string) {
|
|||||||
return DEFAULT_CONTENT_TYPE;
|
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() {
|
async function readLocalFallbackFavicon() {
|
||||||
const file = await readFile(join(process.cwd(), "app", "favicon.ico"));
|
const file = await readFile(join(process.cwd(), "app", "favicon.ico"));
|
||||||
return new NextResponse(file, {
|
return new NextResponse(file, {
|
||||||
@ -35,19 +64,46 @@ async function readLocalFallbackFavicon() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
const settings = await getSiteSettings();
|
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) {
|
if (!faviconUrl) {
|
||||||
|
console.error("[favicon] Invalid favicon URL:", rawFaviconUrl);
|
||||||
return await readLocalFallbackFavicon();
|
return await readLocalFallbackFavicon();
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetUrl = `${faviconUrl}${faviconUrl.includes("?") ? "&" : "?"}v=${Date.now()}`;
|
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) {
|
if (!upstream.ok) {
|
||||||
|
console.error("[favicon] Upstream request failed:", {
|
||||||
|
targetUrl,
|
||||||
|
status: upstream.status,
|
||||||
|
statusText: upstream.statusText,
|
||||||
|
});
|
||||||
return await readLocalFallbackFavicon();
|
return await readLocalFallbackFavicon();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +115,8 @@ export async function GET() {
|
|||||||
status: 200,
|
status: 200,
|
||||||
headers: noStoreHeaders(contentType),
|
headers: noStoreHeaders(contentType),
|
||||||
});
|
});
|
||||||
} catch (_error) {
|
} catch (error) {
|
||||||
|
console.error("[favicon] Failed to proxy favicon:", error);
|
||||||
return await readLocalFallbackFavicon();
|
return await readLocalFallbackFavicon();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,17 @@ import { getSiteSettings } from "@/lib/db/queries";
|
|||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const settings = await getSiteSettings();
|
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 {
|
return {
|
||||||
|
metadataBase,
|
||||||
title: {
|
title: {
|
||||||
template: `%s | ${settings.siteName}`,
|
template: `%s | ${settings.siteName}`,
|
||||||
default: settings.siteName,
|
default: settings.siteName,
|
||||||
|
|||||||
@ -14,30 +14,43 @@ export function FaviconUpdater() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅处理本组件创建的 link,避免删除 Next/React 管理的 head 节点
|
const managedSelector =
|
||||||
const oldLinks = document.querySelectorAll(
|
'link[rel="icon"],link[rel="shortcut icon"],link[rel="apple-touch-icon"]';
|
||||||
'link[data-managed-favicon="true"]'
|
const existingLinks = document.querySelectorAll<HTMLLinkElement>(
|
||||||
|
managedSelector
|
||||||
);
|
);
|
||||||
oldLinks.forEach((link) => {
|
|
||||||
if (link.parentNode) {
|
existingLinks.forEach((link) => {
|
||||||
link.parentNode.removeChild(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 ensureLink = (rel: string, type?: string) => {
|
||||||
const link = document.createElement("link");
|
let link = document.querySelector(
|
||||||
link.rel = rel;
|
`link[rel="${rel}"]`
|
||||||
link.href = href;
|
) 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");
|
link.setAttribute("data-managed-favicon", "true");
|
||||||
if (type) {
|
if (type) {
|
||||||
link.type = type;
|
link.type = type;
|
||||||
}
|
}
|
||||||
document.head.appendChild(link);
|
if (rel === "icon") {
|
||||||
|
link.setAttribute("sizes", "any");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
createManagedLink("icon", urlWithCache, "image/x-icon");
|
ensureLink("icon", "image/x-icon");
|
||||||
createManagedLink("apple-touch-icon", urlWithCache);
|
ensureLink("apple-touch-icon");
|
||||||
createManagedLink("shortcut icon", urlWithCache);
|
ensureLink("shortcut icon");
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user