-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Description
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 withfill="black", no style blockicon-dark.svg- paths withfill="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:
WhitelabelingProvideralready uses<Head>for favicons - mixing in imperative DOM mutation introduces timing conflictsquerySelector('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.