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 type { Metadata } from "next";
|
||||||
|
import { unstable_noStore as noStore } from "next/cache";
|
||||||
import { getLocale } from "next-intl/server";
|
import { getLocale } from "next-intl/server";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { buildAppMetadata } from "@/lib/metadata";
|
import { buildAppMetadata } from "@/lib/metadata";
|
||||||
import { getDirection } from "@/lib/locale";
|
import { getDirection } from "@/lib/locale";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
export const revalidate = 0;
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
|
noStore();
|
||||||
|
|
||||||
return buildAppMetadata();
|
return buildAppMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,6 +20,8 @@ export default async function RootLayout({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
noStore();
|
||||||
|
|
||||||
const locale = await getLocale().catch(() => "de");
|
const locale = await getLocale().catch(() => "de");
|
||||||
|
|
||||||
return (
|
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";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
const CONTENT_TYPES: Record<string, string> = {
|
const CONTENT_TYPES: Record<string, string> = {
|
||||||
|
".ico": "image/x-icon",
|
||||||
".jpg": "image/jpeg",
|
".jpg": "image/jpeg",
|
||||||
".jpeg": "image/jpeg",
|
".jpeg": "image/jpeg",
|
||||||
".png": "image/png",
|
".png": "image/png",
|
||||||
|
|||||||
+12
-7
@@ -12,6 +12,7 @@ import {
|
|||||||
getSiteSettingsMediaBindings,
|
getSiteSettingsMediaBindings,
|
||||||
} from "./app-config";
|
} from "./app-config";
|
||||||
import { AppLocale, getLocalizedPath, resolveLocale } from "./locale";
|
import { AppLocale, getLocalizedPath, resolveLocale } from "./locale";
|
||||||
|
import { buildSiteIconUrls } from "./site-icons";
|
||||||
|
|
||||||
function getSiteUrl(): URL {
|
function getSiteUrl(): URL {
|
||||||
return new URL(process.env.NEXT_PUBLIC_SITE_URL ?? "https://mohfarawati.de");
|
return new URL(process.env.NEXT_PUBLIC_SITE_URL ?? "https://mohfarawati.de");
|
||||||
@@ -69,19 +70,23 @@ export function buildAppMetadataFromConfig(
|
|||||||
): Metadata {
|
): Metadata {
|
||||||
const defaultLocaleSettings = settings.locales[routing.defaultLocale];
|
const defaultLocaleSettings = settings.locales[routing.defaultLocale];
|
||||||
const openGraphImages = buildMetadataImages(bindings.defaultOgImage?.url);
|
const openGraphImages = buildMetadataImages(bindings.defaultOgImage?.url);
|
||||||
|
const siteIconUrls = buildSiteIconUrls({
|
||||||
|
siteName: defaultLocaleSettings.siteName,
|
||||||
|
faviconAssetId: bindings.favicon?.assetId,
|
||||||
|
faviconUrl: bindings.favicon?.url,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
metadataBase: getSiteUrl(),
|
metadataBase: getSiteUrl(),
|
||||||
title: defaultLocaleSettings.siteName,
|
title: defaultLocaleSettings.siteName,
|
||||||
description: defaultLocaleSettings.siteDescription,
|
description: defaultLocaleSettings.siteDescription,
|
||||||
applicationName: defaultLocaleSettings.siteName,
|
applicationName: defaultLocaleSettings.siteName,
|
||||||
icons: bindings.favicon?.url
|
manifest: siteIconUrls.manifestHref,
|
||||||
? {
|
icons: {
|
||||||
icon: [bindings.favicon.url],
|
icon: [{ url: siteIconUrls.faviconHref }],
|
||||||
shortcut: [bindings.favicon.url],
|
shortcut: [{ url: siteIconUrls.faviconHref }],
|
||||||
apple: [bindings.favicon.url],
|
apple: [{ url: siteIconUrls.appleIconHref }],
|
||||||
}
|
},
|
||||||
: undefined,
|
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: defaultLocaleSettings.siteName,
|
title: defaultLocaleSettings.siteName,
|
||||||
description: defaultLocaleSettings.siteDescription,
|
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