diff --git a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx deleted file mode 100644 index a1a3275618..0000000000 --- a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import Head from "next/head"; -import { api } from "@/utils/api"; - -export function WhitelabelingProvider() { - const { data: config } = api.whitelabeling.getPublic.useQuery(undefined, { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - }); - - if (!config) return null; - - return ( - <> - - {config.metaTitle && {config.metaTitle}} - {config.faviconUrl && } - - - {config.customCss && ( - - - Dokploy - - {getLayout()} diff --git a/apps/dokploy/pages/_document.tsx b/apps/dokploy/pages/_document.tsx index 120bb827e1..bfe175c80f 100644 --- a/apps/dokploy/pages/_document.tsx +++ b/apps/dokploy/pages/_document.tsx @@ -1,10 +1,104 @@ -import { Head, Html, Main, NextScript } from "next/document"; +import { getPublicWhitelabelingConfig } from "@dokploy/server"; +import NextDocument, { + type DocumentContext, + type DocumentInitialProps, + Head, + Html, + Main, + NextScript, +} from "next/document"; -export default function Document() { +interface WhitelabelingDocumentProps { + metaTitle: string | null; + faviconHref: string | null; + customCss: string | null; +} + +// Cache the resolved favicon (inlined as a data URI) so we don't re-fetch the +// remote image on every server render. Keyed by the configured favicon URL. +const FAVICON_CACHE_TTL = 60 * 60 * 1000; // 1 hour + +const SETTINGS_CACHE_TTL = 60 * 1000; // 1 minute + +declare global { + var __SETTINGS_CACHE: { + data: { + metaTitle: string | null; + faviconHref: string | null; + customCss: string | null; + }; + expiresAt: number; + } | null; + var __FAVICON_CACHE: Map; +} + +const faviconCache = + globalThis.__FAVICON_CACHE || + new Map(); +globalThis.__FAVICON_CACHE = faviconCache; + +/** + * Resolve the favicon to an inline data URI so it is present in the initial + * HTML and renders without a network round-trip (no flash of the default + * favicon). Falls back to the raw URL if the image can't be fetched. + */ +async function resolveFaviconHref( + faviconUrl: string | null | undefined, +): Promise { + if (!faviconUrl) return null; + + const cached = faviconCache.get(faviconUrl); + if (cached && cached.expiresAt > Date.now()) { + return cached.href; + } + + // Default to the raw URL so the custom favicon still loads if inlining fails. + let href = faviconUrl; + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 3000); + const response = await fetch(faviconUrl, { signal: controller.signal }); + clearTimeout(timeout); + + if (response.ok) { + const buffer = Buffer.from(await response.arrayBuffer()); + // Avoid embedding very large images directly in the HTML. + if (buffer.byteLength <= 512 * 1024) { + const contentType = response.headers.get("content-type") || "image/png"; + href = `data:${contentType};base64,${buffer.toString("base64")}`; + } + } + } catch { + // Keep the raw URL fallback. + } + + faviconCache.set(faviconUrl, { + href, + expiresAt: Date.now() + FAVICON_CACHE_TTL, + }); + return href; +} + +export default function Document({ + metaTitle, + faviconHref, + customCss, +}: WhitelabelingDocumentProps) { + const title = metaTitle || "Dokploy"; return ( - + {/* Rendered on the server so the correct branding is present on first + paint (and for social scrapers), avoiding a flash of / fallback to + the default Dokploy branding. */} + {title} + + {customCss && ( +