mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
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:
parent
11bb82c731
commit
aa208d2dd7
@ -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')
|
||||
|
||||
@ -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` },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Loading…
Reference in New Issue
Block a user