Skip to content

Commit 0981cdc

Browse files
authored
fix(next): admin.meta.title object form renders [object Object] in browser tab (#16117)
# Overview Previously, setting `admin.meta.title` to a Next.js `TemplateString` object would render `[object Object]` in the browser tab: ```ts // Before - broken: browser tab shows "[object Object] - My CMS" admin: { meta: { title: { default: 'Dashboard', template: '%s | Dashboard' }, titleSuffix: '- My CMS', } } // After - works: browser tab shows "Dashboard - My CMS", collection pages show "Posts | Dashboard - My CMS" admin: { meta: { title: { default: 'Dashboard', template: '%s | Dashboard' }, titleSuffix: '- My CMS', } } ``` Fixes #16111 --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1213885527383084
1 parent 3da805f commit 0981cdc

2 files changed

Lines changed: 136 additions & 4 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { generateMetadata } from './meta.js'
4+
5+
describe('generateMetadata', () => {
6+
it('should handle a string title with titleSuffix', async () => {
7+
const result = await generateMetadata({
8+
serverURL: 'http://localhost:3000',
9+
title: 'Dashboard',
10+
titleSuffix: '- My CMS',
11+
})
12+
13+
expect(result.title).toBe('Dashboard - My CMS')
14+
})
15+
16+
it('should apply titleSuffix to default and template fields of a TemplateString title object', async () => {
17+
const result = await generateMetadata({
18+
serverURL: 'http://localhost:3000',
19+
title: { default: 'Dashboard', template: '%s | Dashboard' },
20+
titleSuffix: '- My CMS',
21+
})
22+
23+
expect(typeof result.title).toBe('object')
24+
expect((result.title as { default: string; template: string }).default).toBe(
25+
'Dashboard - My CMS',
26+
)
27+
expect((result.title as { default: string; template: string }).template).toBe(
28+
'%s | Dashboard - My CMS',
29+
)
30+
})
31+
32+
it('should use the TemplateString default for ogTitle when title is a TemplateString object', async () => {
33+
const result = await generateMetadata({
34+
serverURL: 'http://localhost:3000',
35+
title: { default: 'My CMS', template: '%s | My CMS' },
36+
titleSuffix: '- Payload',
37+
})
38+
39+
// OG title must be a plain string — extract from TemplateString.default and append titleSuffix
40+
expect(result.openGraph?.title).toBe('My CMS - Payload')
41+
})
42+
43+
it('should use the TemplateString absolute for ogTitle when title has absolute property', async () => {
44+
const result = await generateMetadata({
45+
serverURL: 'http://localhost:3000',
46+
title: { absolute: 'My CMS Absolute' },
47+
titleSuffix: '- Payload',
48+
})
49+
50+
expect(result.openGraph?.title).toBe('My CMS Absolute - Payload')
51+
})
52+
53+
it('should apply titleSuffix to the absolute field of a TemplateString title object', async () => {
54+
const result = await generateMetadata({
55+
serverURL: 'http://localhost:3000',
56+
title: { absolute: 'My CMS Absolute' },
57+
titleSuffix: '- Payload',
58+
})
59+
60+
expect(typeof result.title).toBe('object')
61+
expect((result.title as { absolute: string }).absolute).toBe('My CMS Absolute - Payload')
62+
})
63+
64+
it('should use openGraph.title string over incomingMetadata.title for ogTitle', async () => {
65+
const result = await generateMetadata({
66+
serverURL: 'http://localhost:3000',
67+
title: 'My CMS',
68+
titleSuffix: '- Payload',
69+
openGraph: { title: 'Custom OG Title' },
70+
})
71+
72+
expect(result.openGraph?.title).toBe('Custom OG Title')
73+
})
74+
75+
it('should return undefined for metaTitle when no title and no titleSuffix are set', async () => {
76+
const result = await generateMetadata({
77+
serverURL: 'http://localhost:3000',
78+
})
79+
80+
expect(result.title).toBeUndefined()
81+
})
82+
83+
it('should return just the title when no titleSuffix is set', async () => {
84+
const result = await generateMetadata({
85+
serverURL: 'http://localhost:3000',
86+
title: 'My CMS',
87+
})
88+
89+
expect(result.title).toBe('My CMS')
90+
})
91+
})

packages/next/src/utilities/meta.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,44 @@ import type { MetaConfig } from 'payload'
55
import { payloadFaviconDark, payloadFaviconLight, staticOGImage } from '@payloadcms/ui/assets'
66
import * as qs from 'qs-esm'
77

8+
const appendTitleSuffix = (
9+
title: Metadata['title'],
10+
suffix: string | undefined,
11+
): Metadata['title'] => {
12+
if (!suffix || !title) {
13+
return title ?? undefined
14+
}
15+
if (typeof title === 'string') {
16+
return `${title} ${suffix}`
17+
}
18+
19+
if ('default' in title) {
20+
return { default: `${title.default} ${suffix}`, template: `${title.template} ${suffix}` }
21+
}
22+
23+
if ('template' in title) {
24+
return {
25+
absolute: `${title.absolute} ${suffix}`,
26+
template: title.template !== null ? `${title.template} ${suffix}` : null,
27+
}
28+
}
29+
30+
return { absolute: `${title.absolute} ${suffix}` }
31+
}
32+
33+
const getTitleString = (title: Metadata['title']): string | undefined => {
34+
if (!title) {
35+
return undefined
36+
}
37+
if (typeof title === 'string') {
38+
return title
39+
}
40+
if ('absolute' in title) {
41+
return title.absolute
42+
}
43+
return title.default
44+
}
45+
846
const defaultOpenGraph: Metadata['openGraph'] = {
947
description:
1048
'Payload is a headless CMS and application framework built with TypeScript, Node.js, and React.',
@@ -43,11 +81,14 @@ export const generateMetadata = async (
4381
},
4482
] satisfies Array<Icon>)
4583

46-
const metaTitle: Metadata['title'] = [incomingMetadata.title, titleSuffix]
47-
.filter(Boolean)
48-
.join(' ')
84+
const metaTitle: Metadata['title'] = appendTitleSuffix(incomingMetadata.title, titleSuffix)
85+
86+
const titleStringForOg: string | undefined =
87+
typeof incomingMetadata.openGraph?.title === 'string'
88+
? incomingMetadata.openGraph.title
89+
: getTitleString(incomingMetadata.title)
4990

50-
const ogTitle = `${typeof incomingMetadata.openGraph?.title === 'string' ? incomingMetadata.openGraph.title : incomingMetadata.title} ${titleSuffix}`
91+
const ogTitle = [titleStringForOg, titleSuffix].filter(Boolean).join(' ')
5192

5293
const mergedOpenGraph: Metadata['openGraph'] = {
5394
...(defaultOpenGraph || {}),

0 commit comments

Comments
 (0)