Bug Description
The favicon does not switch between light and dark variants when toggling the theme in Dokploy's UI. It only updates after a full page reload.
Root Cause
_document.tsx sets a static favicon:
<link rel="icon" href="/icon.svg" />
The SVG uses @media (prefers-color-scheme: dark) to switch between black and white fills. This reacts to the OS-level color scheme, not to Dokploy's app-level theme state.
Dokploy uses next-themes with attribute="class" (in _app.tsx), meaning theme switching is class-based on the <html> element. Since the favicon is loaded as an external resource and not part of the DOM, it cannot observe class changes - it only responds to the OS media query.
This was partially addressed in PR #1049, which added the prefers-color-scheme media query to the SVG. The PR author documented the mismatch between OS theme and Dokploy theme but did not resolve it.
Steps to Reproduce
- Set OS to light mode
- Open Dokploy, toggle theme to dark
- Observe: favicon remains black (light-mode variant)
- Reload the page
- Favicon is still wrong because OS is light, but Dokploy is dark
Proposed Fix
1. Add two static SVG assets to /public:
icon-light.svg - paths with fill="black", no style block
icon-dark.svg - paths with fill="white", no style block
These are trivial derivations of the existing icon.svg with the <style> block removed and a fixed fill attribute on each path.
2. Extend WhitelabelingProvider to handle theme-aware favicon switching declaratively via next/head:
import { useTheme } from "next-themes";
export function WhitelabelingProvider() {
const { resolvedTheme } = useTheme();
const { data: config } = api.whitelabeling.getPublic.useQuery(undefined, {
staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
});
const faviconHref = config?.faviconUrl
?? (resolvedTheme === "dark" ? "/icon-dark.svg"
: resolvedTheme === "light" ? "/icon-light.svg"
: "/icon.svg");
return (
<>
<Head>
{config?.metaTitle && <title>{config.metaTitle}</title>}
<link rel="icon" href={faviconHref} key="app-favicon" />
</Head>
{config?.customCss && (
<style
id="whitelabeling-styles"
dangerouslySetInnerHTML={{ __html: config.customCss }}
/>
)}
</>
);
}
Why extend WhitelabelingProvider instead of adding a new component:
- It already owns favicon and meta title injection via
next/head
- Single source of truth for all
<head> meta - no race conditions between competing <link rel="icon"> tags
- The
key="app-favicon" ensures Next.js deduplicates the tag properly
Why declarative next/head instead of querySelector DOM manipulation:
WhitelabelingProvider already uses <Head> for favicons - mixing in imperative DOM mutation introduces timing conflicts
querySelector('link[rel="icon"]') is fragile when multiple icon links exist
- Declarative approach lets React/Next.js manage exactly one favicon entry
3. Keep _document.tsx unchanged - the existing icon.svg with prefers-color-scheme remains as a pre-hydration fallback. Before JS loads, users at least get an OS-matching icon, which is better than a static single-color fallback.
Bug Description
The favicon does not switch between light and dark variants when toggling the theme in Dokploy's UI. It only updates after a full page reload.
Root Cause
_document.tsxsets a static favicon:The SVG uses
@media (prefers-color-scheme: dark)to switch between black and white fills. This reacts to the OS-level color scheme, not to Dokploy's app-level theme state.Dokploy uses
next-themeswithattribute="class"(in_app.tsx), meaning theme switching is class-based on the<html>element. Since the favicon is loaded as an external resource and not part of the DOM, it cannot observe class changes - it only responds to the OS media query.This was partially addressed in PR #1049, which added the
prefers-color-schememedia query to the SVG. The PR author documented the mismatch between OS theme and Dokploy theme but did not resolve it.Steps to Reproduce
Proposed Fix
1. Add two static SVG assets to
/public:icon-light.svg- paths withfill="black", no style blockicon-dark.svg- paths withfill="white", no style blockThese are trivial derivations of the existing
icon.svgwith the<style>block removed and a fixedfillattribute on each path.2. Extend
WhitelabelingProviderto handle theme-aware favicon switching declaratively vianext/head:Why extend
WhitelabelingProviderinstead of adding a new component:next/head<head>meta - no race conditions between competing<link rel="icon">tagskey="app-favicon"ensures Next.js deduplicates the tag properlyWhy declarative
next/headinstead ofquerySelectorDOM manipulation:WhitelabelingProvideralready uses<Head>for favicons - mixing in imperative DOM mutation introduces timing conflictsquerySelector('link[rel="icon"]')is fragile when multiple icon links exist3. Keep
_document.tsxunchanged - the existingicon.svgwithprefers-color-schemeremains as a pre-hydration fallback. Before JS loads, users at least get an OS-matching icon, which is better than a static single-color fallback.