import { readFile } from "fs/promises"; import path from "path"; import { routing } from "@/i18n/routing"; import { getSiteSettings, getSiteSettingsMediaBindings } from "./app-config"; import { isManagedMediaFilePath, resolveMediaUploadPath } from "./media-storage"; 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"; const TRANSPARENT_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9sot5WQAAAAASUVORK5CYII="; const ICON_CONTENT_TYPES: Record = { ".ico": "image/x-icon", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp", }; type SiteIconInput = { siteName: string; faviconVersion?: 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(); } export function buildSiteIconUrls(input: SiteIconInput): SiteIconUrls { const siteName = input.siteName.trim() || "Moh"; const version = input.faviconVersion?.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 { const [settings, bindings] = await Promise.all([getSiteSettings(), getSiteSettingsMediaBindings()]); const defaultLocaleSettings = settings.locales[routing.defaultLocale]; return buildSiteIconUrls({ siteName: defaultLocaleSettings.siteName, faviconVersion: bindings.favicon?.version, faviconUrl: bindings.favicon?.url, }); } function getResolvedPathname(iconUrl: string) { const resolvedUrl = new URL(iconUrl, "https://local.invalid"); return resolvedUrl.pathname; } async function createFileResponse(absolutePath: string) { const fileBuffer = await readFile(absolutePath); const contentType = ICON_CONTENT_TYPES[path.extname(absolutePath).toLowerCase()] ?? "application/octet-stream"; return new Response(fileBuffer, { status: 200, headers: { "Content-Type": contentType, "Cache-Control": "no-store, max-age=0", }, }); } export async function buildSiteIconResponse(iconUrl: string | null) { if (iconUrl) { const pathname = getResolvedPathname(iconUrl); if (isManagedMediaFilePath(pathname)) { return createFileResponse(resolveMediaUploadPath(pathname)); } } return new Response(Buffer.from(TRANSPARENT_PNG_BASE64, "base64"), { status: 200, headers: { "Content-Type": "image/png", "Cache-Control": "no-store, max-age=0", }, }); }