From 7c5785e9e8ce8733572de5ad911d8e53a585119a Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:25:06 +0100 Subject: [PATCH] feat: enhance branding options with custom favicon and logos in configuration --- .env.example | 21 ++++++++++++-- app/api/config/route.ts | 3 ++ app/layout.tsx | 13 ++++++--- components/layout/sidebar.tsx | 15 ++++++++++ hooks/use-config.ts | 12 ++++++++ lib/__tests__/config-route.test.ts | 18 ++++++++++++ setup.sh | 44 ++++++++++++++++++++++-------- 7 files changed, 109 insertions(+), 17 deletions(-) diff --git a/.env.example b/.env.example index fea2feea..eaea9c85 100644 --- a/.env.example +++ b/.env.example @@ -89,10 +89,27 @@ JMAP_SERVER_URL=https://your-jmap-server.com # LOG_LEVEL=info # ============================================================================= -# Login Page Customization (all optional) +# Branding (all optional) # ============================================================================= -# Custom logo images for the login page (PNG, SVG, etc.) +# Custom favicon for the browser tab. +# Supported formats: SVG (recommended), PNG, ICO. +# Recommended size: 32×32px minimum, 512×512px maximum (or SVG for best scaling). +# Can be an absolute URL or a path relative to the public/ directory. +# Defaults to the Bulwark favicon if not set. +# FAVICON_URL=/branding/my-favicon.svg + +# Custom logos for the sidebar (shown in the main app after login). +# Supported formats: SVG (recommended), PNG, WebP. +# Recommended size: min 24×24px, max 128×128px +# Can be absolute URLs or paths relative to the public/ directory. +# If not set, no logo is shown in the sidebar. +# APP_LOGO_LIGHT_URL=/branding/my-logo-color.svg +# APP_LOGO_DARK_URL=/branding/my-logo-white.svg + +# Custom logo images for the login page. +# Supported formats: SVG (recommended), PNG, WebP. +# Recommended size: min 32×32px, max 512×512px # Can be absolute URLs or paths relative to the public/ directory. # Light mode logo (shown on light backgrounds), defaults to Bulwark logo. LOGIN_LOGO_LIGHT_URL=/branding/Bulwark_Logo_Color.svg diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 92cb4b1d..40f18a04 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -26,6 +26,9 @@ export async function GET() { settingsSyncEnabled: process.env.SETTINGS_SYNC_ENABLED === 'true' && !!process.env.SESSION_SECRET, stalwartFeaturesEnabled: process.env.STALWART_FEATURES !== 'false', devMode: process.env.DEV_MOCK_JMAP === 'true', + faviconUrl: process.env.FAVICON_URL || '/branding/Bulwark_Favicon.svg', + appLogoLightUrl: process.env.APP_LOGO_LIGHT_URL || '', + appLogoDarkUrl: process.env.APP_LOGO_DARK_URL || '', loginLogoLightUrl: process.env.LOGIN_LOGO_LIGHT_URL || '/branding/Bulwark_Logo_Color.svg', loginLogoDarkUrl: process.env.LOGIN_LOGO_DARK_URL || '/branding/Bulwark_Logo_White.svg', loginCompanyName: process.env.LOGIN_COMPANY_NAME || '', diff --git a/app/layout.tsx b/app/layout.tsx index b4e7facb..5ab67392 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -14,10 +14,15 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); -export const metadata: Metadata = { - title: process.env.APP_NAME || process.env.NEXT_PUBLIC_APP_NAME || "Webmail", - description: "Minimalist webmail client using JMAP protocol", -}; +export async function generateMetadata(): Promise { + const faviconUrl = process.env.FAVICON_URL; + + return { + title: process.env.APP_NAME || process.env.NEXT_PUBLIC_APP_NAME || "Webmail", + description: "Minimalist webmail client using JMAP protocol", + ...(faviconUrl ? { icons: { icon: faviconUrl } } : {}), + }; +} export default async function RootLayout({ children, diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index 73d89de4..b8c7446f 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -37,6 +37,8 @@ import { useSettingsStore, KEYWORD_PALETTE, KeywordDefinition } from "@/stores/s import { useEmailStore } from "@/stores/email-store"; import { toast } from "@/stores/toast-store"; import { debug } from "@/lib/debug"; +import { useConfig } from "@/hooks/use-config"; +import { useThemeStore } from "@/stores/theme-store"; interface SidebarProps { mailboxes: Mailbox[]; @@ -361,6 +363,8 @@ export function Sidebar({ }: SidebarProps) { const { sidebarCollapsed: isCollapsed, toggleSidebarCollapsed } = useUIStore(); const { primaryIdentity } = useAuthStore(); + const { appLogoLightUrl, appLogoDarkUrl } = useConfig(); + const resolvedTheme = useThemeStore((s) => s.resolvedTheme); const [expandedFolders, setExpandedFolders] = useState>(new Set()); const [tagsExpanded, setTagsExpanded] = useState(() => { try { @@ -460,6 +464,17 @@ export function Sidebar({ + {(() => { + const logoUrl = resolvedTheme === 'dark' ? (appLogoDarkUrl || appLogoLightUrl) : (appLogoLightUrl || appLogoDarkUrl); + return logoUrl ? ( + + ) : null; + })()} +