feat: per-domain branding overrides on /api/config, manifest, pwa-icon #332

This commit is contained in:
Linus Rath
2026-05-28 20:06:47 +02:00
parent 8ae5ecba41
commit 1da04c254b
8 changed files with 509 additions and 49 deletions
+32 -10
View File
@@ -1,5 +1,12 @@
import type { MetadataRoute } from "next";
import { headers } from "next/headers";
import { configManager } from "@/lib/admin/config-manager";
import {
matchDomainBranding,
parseDomainBranding,
pickRequestHost,
type BrandingOverrideKey,
} from "@/lib/admin/domain-branding";
export const dynamic = "force-dynamic";
@@ -25,25 +32,40 @@ const withBase = (p: string) => `${BASE_PATH}${p}`;
export default async function manifest(): Promise<ExtendedManifest> {
await configManager.ensureLoaded();
const host = pickRequestHost(await headers());
const domainOverrides = matchDomainBranding(
host,
parseDomainBranding(configManager.get<unknown>("domainBranding", [])),
);
const branded = <T,>(key: BrandingOverrideKey, fallback: T): T => {
const override = domainOverrides[key];
if (typeof override === "string" && override.length > 0) return override as T;
return configManager.get<T>(key, fallback);
};
const appName =
configManager.get<string>("appName") ||
branded<string>("appName", "") ||
process.env.NEXT_PUBLIC_APP_NAME ||
"Bulwark Webmail";
const shortName = configManager.get<string>("appShortName") || appName;
const shortName = branded<string>("appShortName", "") || appName;
const description =
configManager.get<string>("appDescription") ||
branded<string>("appDescription", "") ||
"A modern webmail client built for Stalwart Mail Server";
const themeColor = configManager.get<string>("pwaThemeColor") || "#ffffff";
const backgroundColor = configManager.get<string>("pwaBackgroundColor") || "#ffffff";
const themeColor = branded<string>("pwaThemeColor", "") || "#ffffff";
const backgroundColor = branded<string>("pwaBackgroundColor", "") || "#ffffff";
// If pwaIconUrl or faviconUrl was explicitly configured (admin override or
// env var), serve dynamically resized PNGs via /api/pwa-icon/[size].
// Otherwise fall back to the static Bulwark PNGs - sources marked "default"
// are the built-in placeholder paths and not real custom icons.
// If pwaIconUrl or faviconUrl was explicitly configured (admin override,
// env var, or per-domain override), serve dynamically resized PNGs via
// /api/pwa-icon/[size]. Otherwise fall back to the static Bulwark PNGs -
// sources marked "default" are the built-in placeholder paths and not
// real custom icons.
const sources = configManager.getAllWithSources();
const hasCustomIcon =
sources.pwaIconUrl?.source !== "default" || sources.faviconUrl?.source !== "default";
!!domainOverrides.pwaIconUrl ||
!!domainOverrides.faviconUrl ||
sources.pwaIconUrl?.source !== "default" ||
sources.faviconUrl?.source !== "default";
const icons: MetadataRoute.Manifest["icons"] = hasCustomIcon
? [