fix(web): give the favicon a single owner in root metadata (#41433)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin 2026-08-28 08:57:04 +00:00 committed by GitHub
parent 11bb82c731
commit aa208d2dd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 50 additions and 24 deletions

View File

@ -73,6 +73,49 @@ describe('Root layout System Features bootstrap', () => {
})
})
it('points the icons at the branding favicon when one is configured', async () => {
mocks.getSystemFeatures.mockResolvedValue({
branding: {
application_title: 'Acme AI',
enabled: true,
favicon: 'https://cdn.example.com/brand.ico',
},
deployment_edition: 'CLOUD',
})
const { generateMetadata } = await import('../layout')
await expect(generateMetadata()).resolves.toMatchObject({
icons: {
icon: 'https://cdn.example.com/brand.ico',
apple: 'https://cdn.example.com/brand.ico',
},
})
})
it('falls back to the static favicon without branding', async () => {
mocks.getSystemFeatures.mockResolvedValue({
branding: { enabled: false },
deployment_edition: 'CLOUD',
})
const { generateMetadata } = await import('../layout')
await expect(generateMetadata()).resolves.toMatchObject({
icons: { icon: '/favicon.ico' },
})
})
it('falls back to the static favicon when branding is enabled without one', async () => {
mocks.getSystemFeatures.mockResolvedValue({
branding: { application_title: 'Acme AI', enabled: true, favicon: '' },
deployment_edition: 'CLOUD',
})
const { generateMetadata } = await import('../layout')
await expect(generateMetadata()).resolves.toMatchObject({
icons: { icon: '/favicon.ico' },
})
})
it('renders the client recovery path when the server prefetch fails', async () => {
mocks.getSystemFeatures.mockRejectedValue(new Error('system features unavailable'))
const { default: RootLayout, generateMetadata } = await import('../layout')

View File

@ -16,6 +16,7 @@ import {
import { getLocaleOnServer } from '@/i18n-config/server'
import { headers } from '@/next/headers'
import { getApplicationTitle } from '@/utils/document-title'
import { basePath } from '@/utils/var'
import { CloudAnalytics } from './components/base/analytics-consent/cloud-analytics'
import { PartnerStackCookieRecorder } from './components/billing/partner-stack/cookie-recorder'
import { AgentationLoader } from './components/devtools/agentation-loader'
@ -33,13 +34,18 @@ export const viewport: Viewport = {
export async function generateMetadata(): Promise<Metadata> {
const systemFeatures = await prefetchSystemFeatures()
const applicationTitle = getApplicationTitle(systemFeatures?.branding)
const branding = systemFeatures?.branding
const applicationTitle = getApplicationTitle(branding)
const brandedFavicon = branding?.enabled ? branding.favicon : undefined
return {
title: {
default: applicationTitle,
template: `%s - ${applicationTitle}`,
},
icons: brandedFavicon
? { icon: brandedFavicon, apple: brandedFavicon }
: { icon: `${basePath}/favicon.ico` },
}
}

View File

@ -1,37 +1,14 @@
'use client'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useFavicon } from 'ahooks'
import { useEffect } from 'react'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { formatDocumentTitle, getApplicationTitle } from '@/utils/document-title'
import { basePath } from '@/utils/var'
export default function useDocumentTitle(title: string | null) {
const { data } = useSuspenseQuery(systemFeaturesQueryOptions())
const branding = data.branding
const titleStr = title === null ? null : formatDocumentTitle(title, getApplicationTitle(branding))
const favicon = branding.enabled ? branding.favicon : `${basePath}/favicon.ico`
useEffect(() => {
if (titleStr !== null) document.title = titleStr
}, [titleStr])
useEffect(() => {
let apple: HTMLLinkElement | null = null
if (branding.favicon) {
document
.querySelectorAll(
"link[rel='icon'], link[rel='shortcut icon'], link[rel='apple-touch-icon'], link[rel='mask-icon']",
)
.forEach((n) => n.parentNode?.removeChild(n))
apple = document.createElement('link')
apple.rel = 'apple-touch-icon'
apple.href = branding.favicon
document.head.appendChild(apple)
}
return () => {
apple?.remove()
}
}, [branding.favicon])
useFavicon(favicon)
}

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB