fix favicon delivery and cache invalidation
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { buildFallbackIconResponse, getDynamicSiteIconUrls } from "@/lib/site-icons";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
export async function GET() {
|
||||
const iconUrls = await getDynamicSiteIconUrls();
|
||||
|
||||
if (iconUrls.faviconAssetUrl) {
|
||||
const response = NextResponse.redirect(iconUrls.faviconAssetUrl, 307);
|
||||
|
||||
response.headers.set("Cache-Control", "no-store, max-age=0");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
return buildFallbackIconResponse(iconUrls.siteName, 180);
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { buildFallbackIconResponse, getDynamicSiteIconUrls } from "@/lib/site-icons";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
export async function GET() {
|
||||
const iconUrls = await getDynamicSiteIconUrls();
|
||||
|
||||
if (iconUrls.faviconAssetUrl) {
|
||||
const response = NextResponse.redirect(iconUrls.faviconAssetUrl, 307);
|
||||
|
||||
response.headers.set("Cache-Control", "no-store, max-age=0");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
return buildFallbackIconResponse(iconUrls.siteName, 32);
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import type { Metadata } from "next";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { getLocale } from "next-intl/server";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { buildAppMetadata } from "@/lib/metadata";
|
||||
import { getDirection } from "@/lib/locale";
|
||||
import "./globals.css";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
noStore();
|
||||
|
||||
return buildAppMetadata();
|
||||
}
|
||||
|
||||
@@ -14,6 +20,8 @@ export default async function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
noStore();
|
||||
|
||||
const locale = await getLocale().catch(() => "de");
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
import { getDynamicSiteIconUrls } from "@/lib/site-icons";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
export default async function manifest(): Promise<MetadataRoute.Manifest> {
|
||||
const iconUrls = await getDynamicSiteIconUrls();
|
||||
|
||||
return {
|
||||
name: iconUrls.siteName,
|
||||
short_name: iconUrls.siteName,
|
||||
icons: [
|
||||
{
|
||||
src: iconUrls.appleIconHref,
|
||||
sizes: "180x180",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: iconUrls.faviconHref,
|
||||
sizes: "32x32",
|
||||
type: "image/x-icon",
|
||||
},
|
||||
],
|
||||
display: "standalone",
|
||||
background_color: "#ffffff",
|
||||
theme_color: "#111827",
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,7 @@ type MediaFileRouteProps = {
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const CONTENT_TYPES: Record<string, string> = {
|
||||
".ico": "image/x-icon",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
|
||||
+12
-7
@@ -12,6 +12,7 @@ import {
|
||||
getSiteSettingsMediaBindings,
|
||||
} from "./app-config";
|
||||
import { AppLocale, getLocalizedPath, resolveLocale } from "./locale";
|
||||
import { buildSiteIconUrls } from "./site-icons";
|
||||
|
||||
function getSiteUrl(): URL {
|
||||
return new URL(process.env.NEXT_PUBLIC_SITE_URL ?? "https://mohfarawati.de");
|
||||
@@ -69,19 +70,23 @@ export function buildAppMetadataFromConfig(
|
||||
): Metadata {
|
||||
const defaultLocaleSettings = settings.locales[routing.defaultLocale];
|
||||
const openGraphImages = buildMetadataImages(bindings.defaultOgImage?.url);
|
||||
const siteIconUrls = buildSiteIconUrls({
|
||||
siteName: defaultLocaleSettings.siteName,
|
||||
faviconAssetId: bindings.favicon?.assetId,
|
||||
faviconUrl: bindings.favicon?.url,
|
||||
});
|
||||
|
||||
return {
|
||||
metadataBase: getSiteUrl(),
|
||||
title: defaultLocaleSettings.siteName,
|
||||
description: defaultLocaleSettings.siteDescription,
|
||||
applicationName: defaultLocaleSettings.siteName,
|
||||
icons: bindings.favicon?.url
|
||||
? {
|
||||
icon: [bindings.favicon.url],
|
||||
shortcut: [bindings.favicon.url],
|
||||
apple: [bindings.favicon.url],
|
||||
}
|
||||
: undefined,
|
||||
manifest: siteIconUrls.manifestHref,
|
||||
icons: {
|
||||
icon: [{ url: siteIconUrls.faviconHref }],
|
||||
shortcut: [{ url: siteIconUrls.faviconHref }],
|
||||
apple: [{ url: siteIconUrls.appleIconHref }],
|
||||
},
|
||||
openGraph: {
|
||||
title: defaultLocaleSettings.siteName,
|
||||
description: defaultLocaleSettings.siteDescription,
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
|
||||
import { routing } from "@/i18n/routing";
|
||||
|
||||
import { getSiteSettings, getSiteSettingsMediaBindings } from "./app-config";
|
||||
|
||||
const INTERNAL_FAVICON_PATH = "/favicon.ico";
|
||||
const INTERNAL_APPLE_ICON_PATH = "/apple-icon.png";
|
||||
const INTERNAL_MANIFEST_PATH = "/manifest.webmanifest";
|
||||
const DEFAULT_ICON_VERSION = "default";
|
||||
|
||||
type SiteIconInput = {
|
||||
siteName: string;
|
||||
faviconAssetId?: string | null;
|
||||
faviconUrl?: string | null;
|
||||
};
|
||||
|
||||
type SiteIconUrls = {
|
||||
siteName: string;
|
||||
version: string;
|
||||
faviconHref: string;
|
||||
appleIconHref: string;
|
||||
manifestHref: string;
|
||||
faviconAssetUrl: string | null;
|
||||
};
|
||||
|
||||
function appendVersionToUrl(url: string, version: string): string {
|
||||
const base = url.startsWith("http://") || url.startsWith("https://") ? undefined : "https://local.invalid";
|
||||
const resolved = new URL(url, base);
|
||||
|
||||
resolved.searchParams.set("v", version);
|
||||
|
||||
if (base) {
|
||||
return `${resolved.pathname}${resolved.search}${resolved.hash}`;
|
||||
}
|
||||
|
||||
return resolved.toString();
|
||||
}
|
||||
|
||||
function getFallbackLabel(siteName: string): string {
|
||||
const firstCharacter = siteName.trim().charAt(0).toUpperCase() || "M";
|
||||
|
||||
return firstCharacter;
|
||||
}
|
||||
|
||||
export function buildSiteIconUrls(input: SiteIconInput): SiteIconUrls {
|
||||
const siteName = input.siteName.trim() || "Moh";
|
||||
const version = input.faviconAssetId?.trim() || DEFAULT_ICON_VERSION;
|
||||
|
||||
return {
|
||||
siteName,
|
||||
version,
|
||||
faviconHref: appendVersionToUrl(INTERNAL_FAVICON_PATH, version),
|
||||
appleIconHref: appendVersionToUrl(INTERNAL_APPLE_ICON_PATH, version),
|
||||
manifestHref: appendVersionToUrl(INTERNAL_MANIFEST_PATH, version),
|
||||
faviconAssetUrl: input.faviconUrl ? appendVersionToUrl(input.faviconUrl, version) : null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getDynamicSiteIconUrls(): Promise<SiteIconUrls> {
|
||||
const [settings, bindings] = await Promise.all([getSiteSettings(), getSiteSettingsMediaBindings()]);
|
||||
const defaultLocaleSettings = settings.locales[routing.defaultLocale];
|
||||
|
||||
return buildSiteIconUrls({
|
||||
siteName: defaultLocaleSettings.siteName,
|
||||
faviconAssetId: bindings.favicon?.assetId,
|
||||
faviconUrl: bindings.favicon?.url,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildFallbackIconResponse(siteName: string, size: number) {
|
||||
const label = getFallbackLabel(siteName);
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "linear-gradient(135deg, #111827 0%, #0f766e 100%)",
|
||||
color: "#f8fafc",
|
||||
fontSize: size * 0.52,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.08em",
|
||||
borderRadius: size * 0.22,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
headers: {
|
||||
"Cache-Control": "no-store, max-age=0",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user