- {config.logoUrl ? (
+ {effectiveLogoUrl ? (

diff --git a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx
index 3ba9e80bd..ee0e54e78 100644
--- a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx
+++ b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-settings.tsx
@@ -26,6 +26,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
+import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { WhitelabelingPreview } from "./whitelabeling-preview";
@@ -36,20 +37,42 @@ const safeUrlField = z
message: "Only http:// and https:// URLs are allowed",
});
-const formSchema = z.object({
- appName: z.string(),
- appDescription: z.string(),
- logoUrl: safeUrlField,
- faviconUrl: safeUrlField,
- customCss: z.string(),
- loginLogoUrl: safeUrlField,
- supportUrl: safeUrlField,
- docsUrl: safeUrlField,
- errorPageTitle: z.string(),
- errorPageDescription: z.string(),
- footerText: z.string(),
- ogImageUrl: safeUrlField,
-});
+const formSchema = z
+ .object({
+ appName: z.string(),
+ appDescription: z.string(),
+ logoUrl: safeUrlField,
+ faviconUrl: safeUrlField,
+ customCss: z.string(),
+ loginLogoUrl: safeUrlField,
+ supportUrl: safeUrlField,
+ docsUrl: safeUrlField,
+ errorPageTitle: z.string(),
+ errorPageDescription: z.string(),
+ footerText: z.string(),
+ ogImageUrl: safeUrlField,
+ logoLightUrl: z.string(),
+ loginLogoLightUrl: z.string(),
+ faviconLightUrl: z.string(),
+ dualModeBranding: z.boolean(),
+ })
+ .superRefine((data, ctx) => {
+ if (!data.dualModeBranding) return;
+ for (const field of [
+ "logoLightUrl",
+ "loginLogoLightUrl",
+ "faviconLightUrl",
+ ] as const) {
+ const val = data[field];
+ if (val !== "" && !/^https?:\/\//i.test(val)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: "Only http:// and https:// URLs are allowed",
+ path: [field],
+ });
+ }
+ }
+ });
type FormSchema = z.infer
;
@@ -195,6 +218,10 @@ export function WhitelabelingSettings() {
errorPageDescription: "",
footerText: "",
ogImageUrl: "",
+ logoLightUrl: "",
+ loginLogoLightUrl: "",
+ faviconLightUrl: "",
+ dualModeBranding: false,
},
resolver: zodResolver(formSchema),
});
@@ -214,6 +241,10 @@ export function WhitelabelingSettings() {
errorPageDescription: data.errorPageDescription ?? "",
footerText: data.footerText ?? "",
ogImageUrl: data.ogImageUrl ?? "",
+ logoLightUrl: data.logoLightUrl ?? "",
+ loginLogoLightUrl: data.loginLogoLightUrl ?? "",
+ faviconLightUrl: data.faviconLightUrl ?? "",
+ dualModeBranding: data.dualModeBranding ?? false,
});
}
}, [data, form]);
@@ -244,6 +275,10 @@ export function WhitelabelingSettings() {
errorPageDescription: values.errorPageDescription || null,
footerText: values.footerText || null,
ogImageUrl: values.ogImageUrl || null,
+ logoLightUrl: values.logoLightUrl || null,
+ loginLogoLightUrl: values.loginLogoLightUrl || null,
+ faviconLightUrl: values.faviconLightUrl || null,
+ dualModeBranding: values.dualModeBranding,
},
})
.then(async () => {
@@ -412,6 +447,111 @@ export function WhitelabelingSettings() {
+ {/* Light Mode Logos Section */}
+
+
+
+
+ Light Mode Logos
+
+ Use separate logos for light and dark themes. When enabled,
+ the logos in Branding are used for dark mode.
+
+
+
(
+
+
+ {
+ field.onChange(checked);
+ if (!checked) {
+ form.clearErrors([
+ "logoLightUrl",
+ "loginLogoLightUrl",
+ "faviconLightUrl",
+ ]);
+ }
+ }}
+ />
+
+
+ )}
+ />
+
+
+
+
+
{/* Appearance Section */}
diff --git a/apps/dokploy/components/shared/logo.tsx b/apps/dokploy/components/shared/logo.tsx
index 2c316e22f..500925df3 100644
--- a/apps/dokploy/components/shared/logo.tsx
+++ b/apps/dokploy/components/shared/logo.tsx
@@ -1,16 +1,25 @@
+import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
interface Props {
className?: string;
logoUrl?: string;
+ logoLightUrl?: string;
}
-export const Logo = ({ className = "size-14", logoUrl }: Props) => {
- if (logoUrl) {
+export const Logo = ({
+ className = "size-14",
+ logoUrl,
+ logoLightUrl,
+}: Props) => {
+ const { resolvedTheme } = useTheme();
+ const effectiveUrl =
+ resolvedTheme === "light" && logoLightUrl ? logoLightUrl : logoUrl;
+
+ if (effectiveUrl) {
return (
- // biome-ignore lint/performance/noImgElement: this is for dynamic logo loading
diff --git a/apps/dokploy/pages/_document.tsx b/apps/dokploy/pages/_document.tsx
index a8f3b5931..5e6b6948b 100644
--- a/apps/dokploy/pages/_document.tsx
+++ b/apps/dokploy/pages/_document.tsx
@@ -13,6 +13,7 @@ interface WhitelabelingDocumentProps {
appDescription: string | null;
ogImageUrl: string | null;
faviconHref: string | null;
+ faviconLightHref: string | null;
customCss: string | null;
baseUrl: string;
}
@@ -22,6 +23,7 @@ export default function Document({
appDescription,
ogImageUrl,
faviconHref,
+ faviconLightHref,
customCss,
baseUrl,
}: WhitelabelingDocumentProps) {
@@ -44,7 +46,22 @@ export default function Document({
-
+ {faviconLightHref ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
{customCss && (