feat: enhance branding options with custom favicon and logos in configuration
This commit is contained in:
+19
-2
@@ -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
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
+9
-4
@@ -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<Metadata> {
|
||||
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,
|
||||
|
||||
@@ -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<Set<string>>(new Set());
|
||||
const [tagsExpanded, setTagsExpanded] = useState(() => {
|
||||
try {
|
||||
@@ -460,6 +464,17 @@ export function Sidebar({
|
||||
<X className="w-5 h-5" />
|
||||
</Button>
|
||||
|
||||
{(() => {
|
||||
const logoUrl = resolvedTheme === 'dark' ? (appLogoDarkUrl || appLogoLightUrl) : (appLogoLightUrl || appLogoDarkUrl);
|
||||
return logoUrl ? (
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt=""
|
||||
className={cn("object-contain flex-shrink-0", isCollapsed ? "w-6 h-6" : "w-6 h-6")}
|
||||
/>
|
||||
) : null;
|
||||
})()}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
||||
@@ -13,6 +13,9 @@ interface ConfigData {
|
||||
settingsSyncEnabled: boolean;
|
||||
stalwartFeaturesEnabled: boolean;
|
||||
devMode: boolean;
|
||||
faviconUrl: string;
|
||||
appLogoLightUrl: string;
|
||||
appLogoDarkUrl: string;
|
||||
loginLogoLightUrl: string;
|
||||
loginLogoDarkUrl: string;
|
||||
loginCompanyName: string;
|
||||
@@ -79,6 +82,9 @@ export function useConfig(): AppConfig {
|
||||
settingsSyncEnabled: configCache?.settingsSyncEnabled || false,
|
||||
stalwartFeaturesEnabled: configCache?.stalwartFeaturesEnabled ?? true,
|
||||
devMode: configCache?.devMode || false,
|
||||
faviconUrl: configCache?.faviconUrl || '/branding/Bulwark_Favicon.svg',
|
||||
appLogoLightUrl: configCache?.appLogoLightUrl || '',
|
||||
appLogoDarkUrl: configCache?.appLogoDarkUrl || '',
|
||||
loginLogoLightUrl: configCache?.loginLogoLightUrl || '/branding/Bulwark_Logo_Color.svg',
|
||||
loginLogoDarkUrl: configCache?.loginLogoDarkUrl || '/branding/Bulwark_Logo_White.svg',
|
||||
loginCompanyName: configCache?.loginCompanyName || '',
|
||||
@@ -103,6 +109,9 @@ export function useConfig(): AppConfig {
|
||||
settingsSyncEnabled: configCache.settingsSyncEnabled,
|
||||
stalwartFeaturesEnabled: configCache.stalwartFeaturesEnabled,
|
||||
devMode: configCache.devMode,
|
||||
faviconUrl: configCache.faviconUrl,
|
||||
appLogoLightUrl: configCache.appLogoLightUrl,
|
||||
appLogoDarkUrl: configCache.appLogoDarkUrl,
|
||||
loginLogoLightUrl: configCache.loginLogoLightUrl,
|
||||
loginLogoDarkUrl: configCache.loginLogoDarkUrl,
|
||||
loginCompanyName: configCache.loginCompanyName,
|
||||
@@ -128,6 +137,9 @@ export function useConfig(): AppConfig {
|
||||
settingsSyncEnabled: data.settingsSyncEnabled,
|
||||
stalwartFeaturesEnabled: data.stalwartFeaturesEnabled,
|
||||
devMode: data.devMode,
|
||||
faviconUrl: data.faviconUrl,
|
||||
appLogoLightUrl: data.appLogoLightUrl,
|
||||
appLogoDarkUrl: data.appLogoDarkUrl,
|
||||
loginLogoLightUrl: data.loginLogoLightUrl,
|
||||
loginLogoDarkUrl: data.loginLogoDarkUrl,
|
||||
loginCompanyName: data.loginCompanyName,
|
||||
|
||||
@@ -27,6 +27,9 @@ describe('config API route', () => {
|
||||
delete process.env.SETTINGS_SYNC_ENABLED;
|
||||
delete process.env.STALWART_FEATURES;
|
||||
delete process.env.DEV_MOCK_JMAP;
|
||||
delete process.env.FAVICON_URL;
|
||||
delete process.env.APP_LOGO_LIGHT_URL;
|
||||
delete process.env.APP_LOGO_DARK_URL;
|
||||
delete process.env.LOGIN_COMPANY_NAME;
|
||||
delete process.env.LOGIN_IMPRINT_URL;
|
||||
delete process.env.LOGIN_PRIVACY_POLICY_URL;
|
||||
@@ -60,6 +63,9 @@ describe('config API route', () => {
|
||||
expect(config.loginImprintUrl).toBe('');
|
||||
expect(config.loginPrivacyPolicyUrl).toBe('');
|
||||
expect(config.loginWebsiteUrl).toBe('');
|
||||
expect(config.faviconUrl).toBe('/branding/Bulwark_Favicon.svg');
|
||||
expect(config.appLogoLightUrl).toBe('');
|
||||
expect(config.appLogoDarkUrl).toBe('');
|
||||
});
|
||||
|
||||
it('should use runtime env vars over defaults', async () => {
|
||||
@@ -142,4 +148,16 @@ describe('config API route', () => {
|
||||
|
||||
expect(config.stalwartFeaturesEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should return custom favicon and app logo URLs', async () => {
|
||||
process.env.FAVICON_URL = '/branding/custom-favicon.svg';
|
||||
process.env.APP_LOGO_LIGHT_URL = '/branding/my-logo.svg';
|
||||
process.env.APP_LOGO_DARK_URL = '/branding/my-logo-white.svg';
|
||||
|
||||
const config = await getConfig();
|
||||
|
||||
expect(config.faviconUrl).toBe('/branding/custom-favicon.svg');
|
||||
expect(config.appLogoLightUrl).toBe('/branding/my-logo.svg');
|
||||
expect(config.appLogoDarkUrl).toBe('/branding/my-logo-white.svg');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,6 +67,9 @@ CFG_LOG_LEVEL="info"
|
||||
CFG_LOGIN_COMPANY_NAME=""
|
||||
CFG_LOGIN_LOGO_LIGHT_URL=""
|
||||
CFG_LOGIN_LOGO_DARK_URL=""
|
||||
CFG_FAVICON_URL=""
|
||||
CFG_APP_LOGO_LIGHT_URL=""
|
||||
CFG_APP_LOGO_DARK_URL=""
|
||||
CFG_LOGIN_IMPRINT_URL=""
|
||||
CFG_LOGIN_PRIVACY_POLICY_URL=""
|
||||
CFG_LOGIN_WEBSITE_URL=""
|
||||
@@ -761,20 +764,30 @@ screen_login_customization() {
|
||||
CURRENT_STEP=5
|
||||
draw_header
|
||||
|
||||
section_header 5 "Login Page Branding" \
|
||||
"Add your company name and legal links to the login page."
|
||||
section_header 5 "Branding" \
|
||||
"Customize logos, favicon, and login page for your domain identity."
|
||||
|
||||
echo -e " All fields are ${BOLD}optional${RESET}. Press Enter to skip any field."
|
||||
echo ""
|
||||
|
||||
echo -e " ${DIM}Custom favicon for the browser tab (SVG recommended, 32×32 to 512×512px)${RESET}"
|
||||
echo -e " ${DIM}Leave blank to use the default Bulwark favicon.${RESET}"
|
||||
prompt_value "Favicon URL" "$CFG_FAVICON_URL" "CFG_FAVICON_URL"
|
||||
echo ""
|
||||
echo -e " ${DIM}App logo shown in the sidebar (SVG recommended, 24×24 to 128×128px)${RESET}"
|
||||
echo -e " ${DIM}Leave blank for no sidebar logo.${RESET}"
|
||||
prompt_value "App logo URL (light mode)" "$CFG_APP_LOGO_LIGHT_URL" "CFG_APP_LOGO_LIGHT_URL"
|
||||
echo ""
|
||||
prompt_value "App logo URL (dark mode)" "$CFG_APP_LOGO_DARK_URL" "CFG_APP_LOGO_DARK_URL"
|
||||
echo ""
|
||||
echo -e " ${DIM}Shown on the login page footer. Example: Acme Corp${RESET}"
|
||||
prompt_value "Company / organization name" "$CFG_LOGIN_COMPANY_NAME" "CFG_LOGIN_COMPANY_NAME"
|
||||
echo ""
|
||||
echo -e " ${DIM}Custom logo URLs for the login page (PNG, SVG, etc.)${RESET}"
|
||||
echo -e " ${DIM}Custom logo URLs for the login page (SVG recommended, 32×32 to 512×512px)${RESET}"
|
||||
echo -e " ${DIM}Leave blank to use the default Bulwark logo.${RESET}"
|
||||
prompt_value "Logo URL (light mode)" "$CFG_LOGIN_LOGO_LIGHT_URL" "CFG_LOGIN_LOGO_LIGHT_URL"
|
||||
prompt_value "Login logo URL (light mode)" "$CFG_LOGIN_LOGO_LIGHT_URL" "CFG_LOGIN_LOGO_LIGHT_URL"
|
||||
echo ""
|
||||
prompt_value "Logo URL (dark mode)" "$CFG_LOGIN_LOGO_DARK_URL" "CFG_LOGIN_LOGO_DARK_URL"
|
||||
prompt_value "Login logo URL (dark mode)" "$CFG_LOGIN_LOGO_DARK_URL" "CFG_LOGIN_LOGO_DARK_URL"
|
||||
echo ""
|
||||
prompt_value "Website URL" "$CFG_LOGIN_WEBSITE_URL" "CFG_LOGIN_WEBSITE_URL"
|
||||
echo ""
|
||||
@@ -782,9 +795,9 @@ screen_login_customization() {
|
||||
echo ""
|
||||
prompt_value "Privacy policy URL" "$CFG_LOGIN_PRIVACY_POLICY_URL" "CFG_LOGIN_PRIVACY_POLICY_URL"
|
||||
|
||||
if [[ -z "$CFG_LOGIN_COMPANY_NAME" && -z "$CFG_LOGIN_LOGO_LIGHT_URL" && -z "$CFG_LOGIN_LOGO_DARK_URL" && -z "$CFG_LOGIN_WEBSITE_URL" && -z "$CFG_LOGIN_IMPRINT_URL" && -z "$CFG_LOGIN_PRIVACY_POLICY_URL" ]]; then
|
||||
if [[ -z "$CFG_LOGIN_COMPANY_NAME" && -z "$CFG_LOGIN_LOGO_LIGHT_URL" && -z "$CFG_LOGIN_LOGO_DARK_URL" && -z "$CFG_FAVICON_URL" && -z "$CFG_APP_LOGO_LIGHT_URL" && -z "$CFG_APP_LOGO_DARK_URL" && -z "$CFG_LOGIN_WEBSITE_URL" && -z "$CFG_LOGIN_IMPRINT_URL" && -z "$CFG_LOGIN_PRIVACY_POLICY_URL" ]]; then
|
||||
echo ""
|
||||
note "No branding configured. The login page will show defaults."
|
||||
note "No branding configured. The app will use defaults."
|
||||
fi
|
||||
|
||||
draw_footer
|
||||
@@ -892,13 +905,19 @@ screen_summary() {
|
||||
|
||||
# Login page
|
||||
echo -e " ${CYAN}${BOLD}BRANDING${RESET}"
|
||||
if [[ -n "$CFG_LOGIN_COMPANY_NAME" || -n "$CFG_LOGIN_LOGO_LIGHT_URL" || -n "$CFG_LOGIN_LOGO_DARK_URL" || -n "$CFG_LOGIN_WEBSITE_URL" || -n "$CFG_LOGIN_IMPRINT_URL" || -n "$CFG_LOGIN_PRIVACY_POLICY_URL" ]]; then
|
||||
if [[ -n "$CFG_LOGIN_COMPANY_NAME" || -n "$CFG_LOGIN_LOGO_LIGHT_URL" || -n "$CFG_LOGIN_LOGO_DARK_URL" || -n "$CFG_FAVICON_URL" || -n "$CFG_APP_LOGO_LIGHT_URL" || -n "$CFG_APP_LOGO_DARK_URL" || -n "$CFG_LOGIN_WEBSITE_URL" || -n "$CFG_LOGIN_IMPRINT_URL" || -n "$CFG_LOGIN_PRIVACY_POLICY_URL" ]]; then
|
||||
[[ -n "$CFG_FAVICON_URL" ]] && \
|
||||
echo -e " Favicon URL ........... ${BOLD}${CFG_FAVICON_URL}${RESET}"
|
||||
[[ -n "$CFG_APP_LOGO_LIGHT_URL" ]] && \
|
||||
echo -e " App Logo (light) ...... ${BOLD}${CFG_APP_LOGO_LIGHT_URL}${RESET}"
|
||||
[[ -n "$CFG_APP_LOGO_DARK_URL" ]] && \
|
||||
echo -e " App Logo (dark) ....... ${BOLD}${CFG_APP_LOGO_DARK_URL}${RESET}"
|
||||
[[ -n "$CFG_LOGIN_COMPANY_NAME" ]] && \
|
||||
echo -e " Company Name .......... ${BOLD}${CFG_LOGIN_COMPANY_NAME}${RESET}"
|
||||
[[ -n "$CFG_LOGIN_LOGO_LIGHT_URL" ]] && \
|
||||
echo -e " Logo (light mode) ..... ${BOLD}${CFG_LOGIN_LOGO_LIGHT_URL}${RESET}"
|
||||
echo -e " Login Logo (light) .... ${BOLD}${CFG_LOGIN_LOGO_LIGHT_URL}${RESET}"
|
||||
[[ -n "$CFG_LOGIN_LOGO_DARK_URL" ]] && \
|
||||
echo -e " Logo (dark mode) ...... ${BOLD}${CFG_LOGIN_LOGO_DARK_URL}${RESET}"
|
||||
echo -e " Login Logo (dark) ..... ${BOLD}${CFG_LOGIN_LOGO_DARK_URL}${RESET}"
|
||||
[[ -n "$CFG_LOGIN_WEBSITE_URL" ]] && \
|
||||
echo -e " Website URL ........... ${BOLD}${CFG_LOGIN_WEBSITE_URL}${RESET}"
|
||||
[[ -n "$CFG_LOGIN_IMPRINT_URL" ]] && \
|
||||
@@ -1003,9 +1022,12 @@ ENVEOF
|
||||
LOG_FORMAT=${CFG_LOG_FORMAT}
|
||||
LOG_LEVEL=${CFG_LOG_LEVEL}
|
||||
|
||||
# -- Login Page Customization --------------------------------------------------
|
||||
# -- Branding ------------------------------------------------------------------
|
||||
ENVEOF
|
||||
|
||||
[[ -n "$CFG_FAVICON_URL" ]] && echo "FAVICON_URL=${CFG_FAVICON_URL}" >> "$ENV_FILE"
|
||||
[[ -n "$CFG_APP_LOGO_LIGHT_URL" ]] && echo "APP_LOGO_LIGHT_URL=${CFG_APP_LOGO_LIGHT_URL}" >> "$ENV_FILE"
|
||||
[[ -n "$CFG_APP_LOGO_DARK_URL" ]] && echo "APP_LOGO_DARK_URL=${CFG_APP_LOGO_DARK_URL}" >> "$ENV_FILE"
|
||||
[[ -n "$CFG_LOGIN_COMPANY_NAME" ]] && echo "LOGIN_COMPANY_NAME=${CFG_LOGIN_COMPANY_NAME}" >> "$ENV_FILE"
|
||||
[[ -n "$CFG_LOGIN_LOGO_LIGHT_URL" ]] && echo "LOGIN_LOGO_LIGHT_URL=${CFG_LOGIN_LOGO_LIGHT_URL}" >> "$ENV_FILE"
|
||||
[[ -n "$CFG_LOGIN_LOGO_DARK_URL" ]] && echo "LOGIN_LOGO_DARK_URL=${CFG_LOGIN_LOGO_DARK_URL}" >> "$ENV_FILE"
|
||||
|
||||
Reference in New Issue
Block a user