From f74788ac3ee7efec528efd8ecdf52f4a8ec2f635 Mon Sep 17 00:00:00 2001 From: Raunak Bhagat Date: Wed, 17 Jun 2026 12:55:41 -0700 Subject: [PATCH] feat(branding): derive `logoUrl` in `useSettings` hook (#12159) --- web/src/lib/settings/hooks.ts | 12 ++++++++++++ web/src/lib/settings/types.ts | 7 +++++++ web/src/providers/DynamicMetadata.tsx | 18 +++--------------- web/src/refresh-components/Logo.tsx | 25 ++++++------------------- 4 files changed, 28 insertions(+), 34 deletions(-) diff --git a/web/src/lib/settings/hooks.ts b/web/src/lib/settings/hooks.ts index 32587f0073..ae2f131075 100644 --- a/web/src/lib/settings/hooks.ts +++ b/web/src/lib/settings/hooks.ts @@ -1,6 +1,7 @@ "use client"; import useSWR from "swr"; +import { useMemo } from "react"; import useCCPairs from "@/hooks/useCCPairs"; import { errorHandlingFetcher } from "@/lib/fetcher"; import { SWR_KEYS } from "@/lib/swr-keys"; @@ -74,10 +75,21 @@ export function useSettings(): AppSettings { } ); + // Cache-buster: the logo endpoint URL never changes, so the browser serves + // a cached image even after an admin uploads a new logo. Regenerating this + // timestamp whenever the enterprise settings reference changes forces a + // re-fetch. We use referential equality on the SWR data (compare: a===b), + // so this only fires when SWR actually receives new enterprise data. + // eslint-disable-next-line react-hooks/exhaustive-deps + const logoBuster = useMemo(() => Date.now(), [enterprise]); + return { ...core, enterprise: enterprise ?? null, appName: enterprise?.application_name?.trim() || "Onyx", + logoUrl: enterprise?.use_custom_logo + ? `/api/enterprise-settings/logo?v=${logoBuster}` + : null, vectorDbEnabled: !settingsLoading && !settingsError && core.vector_db_enabled !== false, isLoading: diff --git a/web/src/lib/settings/types.ts b/web/src/lib/settings/types.ts index a151779fc4..04902154ba 100644 --- a/web/src/lib/settings/types.ts +++ b/web/src/lib/settings/types.ts @@ -176,6 +176,13 @@ export interface AppSettings extends Settings { enterprise: EnterpriseSettings | null; /** Resolved display name: enterprise.application_name || "Onyx". */ appName: string; + /** + * URL of the logo image to render, or `null` to use the default Onyx SVG. + * Includes a cache-buster that updates whenever enterprise settings are + * revalidated, forcing the browser to re-fetch after an admin uploads a + * new logo. + */ + logoUrl: string | null; /** False when DISABLE_VECTOR_DB is set server-side. */ vectorDbEnabled: boolean; isLoading: boolean; diff --git a/web/src/providers/DynamicMetadata.tsx b/web/src/providers/DynamicMetadata.tsx index 4bc4e14aae..06cd5a659c 100644 --- a/web/src/providers/DynamicMetadata.tsx +++ b/web/src/providers/DynamicMetadata.tsx @@ -1,11 +1,10 @@ "use client"; -import { useEffect, useMemo } from "react"; +import { useEffect } from "react"; import { useSettings } from "@/lib/settings/hooks"; export default function DynamicMetadata() { - const settings = useSettings(); - const { appName } = settings; + const { appName, logoUrl } = useSettings(); useEffect(() => { if (document.title !== appName) { @@ -13,16 +12,5 @@ export default function DynamicMetadata() { } }, [appName]); - // Cache-buster so the favicon re-fetches after an admin uploads a new logo. - const cacheBuster = useMemo( - () => Date.now(), - // eslint-disable-next-line react-hooks/exhaustive-deps - [settings.enterprise] - ); - - const favicon = settings.enterprise?.use_custom_logo - ? `/api/enterprise-settings/logo?v=${cacheBuster}` - : "/onyx.ico"; - - return ; + return ; } diff --git a/web/src/refresh-components/Logo.tsx b/web/src/refresh-components/Logo.tsx index 979e086a62..efeacc4a76 100644 --- a/web/src/refresh-components/Logo.tsx +++ b/web/src/refresh-components/Logo.tsx @@ -8,7 +8,6 @@ import { import { cn } from "@opal/utils"; import Text from "@/refresh-components/texts/Text"; import Truncated from "@/refresh-components/texts/Truncated"; -import { useMemo } from "react"; import { SvgOnyxLogo, SvgOnyxLogoTyped } from "@opal/logos"; export interface LogoProps { @@ -27,21 +26,9 @@ export default function Logo({ onyxBranded, }: LogoProps) { const resolvedSize = size ?? DEFAULT_LOGO_SIZE_PX; - const settings = useSettings(); - const enterpriseSettings = settings.enterprise; - const logoDisplayStyle = enterpriseSettings?.logo_display_style; - const applicationName = enterpriseSettings?.application_name; - - // Cache-buster: the logo URL never changes (/api/enterprise-settings/logo) - // so the browser serves the in-memory cached image even after an admin - // uploads a new one. Generating a fresh timestamp each time enterprise - // settings are revalidated by SWR appends a unique query param to force - // the browser to re-fetch the image. - const logoBuster = useMemo( - () => Date.now(), - // eslint-disable-next-line react-hooks/exhaustive-deps - [enterpriseSettings] - ); + const { enterprise, logoUrl } = useSettings(); + const logoDisplayStyle = enterprise?.logo_display_style; + const applicationName = enterprise?.application_name; if (onyxBranded) { return folded ? ( @@ -51,7 +38,7 @@ export default function Logo({ ); } - const logo = enterpriseSettings?.use_custom_logo ? ( + const logo = logoUrl ? (
@@ -84,7 +71,7 @@ export default function Logo({ {applicationName} )} {!NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED && - !enterpriseSettings?.hide_onyx_branding && ( + !enterprise?.hide_onyx_branding && (