Skip to content

Commit 8f64c82

Browse files
ztannerijjk
authored andcommitted
fix: pages router metadata bugs with React 19 (#81733)
When we updated `experimental.strictNextHead` to be true in #65418, we did not update all spots that would default to true in the case where the value was omitted entirely. This led to the default value not being correctly applied in pages router, which resulted in duplicate metadata w/ React 19. Since we made the flag the default, we can also probably clean up this flag all together, but that can be done separately. Fixes #81655 Fixes #81689
1 parent 529f14c commit 8f64c82

File tree

5 files changed

+106
-55
lines changed

5 files changed

+106
-55
lines changed

packages/next/src/build/templates/pages.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,8 @@ export async function handler(
289289
reactLoadableManifest,
290290

291291
assetPrefix: nextConfig.assetPrefix,
292-
strictNextHead: Boolean(
293-
nextConfig.experimental.strictNextHead
294-
),
292+
strictNextHead:
293+
nextConfig.experimental.strictNextHead ?? true,
295294
previewProps: prerenderManifest.preview,
296295
images: nextConfig.images as any,
297296
nextConfigOutput: nextConfig.output,

packages/next/src/server/config-shared.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,7 @@ export const defaultConfig = {
15001500
devtoolSegmentExplorer: process.env.__NEXT_DEVTOOL_NEW_PANEL_UI === 'true',
15011501
browserDebugInfoInTerminal: false,
15021502
optimizeRouterScrolling: false,
1503+
strictNextHead: true,
15031504
},
15041505
htmlLimitedBots: undefined,
15051506
bundlePagesRouterDependencies: false,

test/development/pages-dir/client-navigation/fixture/next.config.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ module.exports = {
44
// Make sure entries are not getting disposed.
55
maxInactiveAge: 1000 * 60 * 60,
66
},
7-
experimental: {
8-
strictNextHead: process.env.TEST_STRICT_NEXT_HEAD !== 'false',
9-
},
7+
experimental:
8+
process.env.TEST_STRICT_NEXT_HEAD !== undefined
9+
? {
10+
strictNextHead: process.env.TEST_STRICT_NEXT_HEAD === 'true',
11+
}
12+
: {},
1013
// scroll position can be finicky with the
1114
// indicators showing so hide by default
1215
devIndicators: false,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Head from 'next/head'
2+
3+
export default function Page() {
4+
return (
5+
<div>
6+
<Head>
7+
<title>Title Page</title>
8+
<meta property="og:title" content="Title Content" />
9+
<meta name="description" content="Description Content" />
10+
</Head>
11+
<p>This is a page!</p>
12+
</div>
13+
)
14+
}

test/development/pages-dir/client-navigation/rendering-head.test.ts

Lines changed: 83 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ import path from 'path'
88
const isReact18 = parseInt(process.env.NEXT_TEST_REACT_VERSION) === 18
99

1010
describe('Client Navigation rendering <Head />', () => {
11-
describe.each([[false], [true]])(
11+
describe.each([[false], [true], [undefined]])(
1212
'with strictNextHead=%s',
1313
(strictNextHead) => {
1414
const { next } = nextTestSetup({
1515
files: path.join(__dirname, 'fixture'),
16-
env: {
17-
TEST_STRICT_NEXT_HEAD: String(strictNextHead),
18-
},
16+
env:
17+
strictNextHead !== undefined
18+
? {
19+
TEST_STRICT_NEXT_HEAD: String(strictNextHead),
20+
}
21+
: {},
1922
})
2023

2124
function render(
@@ -37,7 +40,7 @@ describe('Client Navigation rendering <Head />', () => {
3740
test('header renders default charset', async () => {
3841
const html = await render('/default-head')
3942
expect(html).toContain(
40-
strictNextHead
43+
strictNextHead !== false
4144
? '<meta charSet="utf-8" data-next-head=""/>'
4245
: '<meta charSet="utf-8"/>'
4346
)
@@ -47,7 +50,7 @@ describe('Client Navigation rendering <Head />', () => {
4750
test('header renders default viewport', async () => {
4851
const html = await render('/default-head')
4952
expect(html).toContain(
50-
strictNextHead
53+
strictNextHead !== false
5154
? '<meta name="viewport" content="width=device-width" data-next-head=""/>'
5255
: '<meta name="viewport" content="width=device-width"/>'
5356
)
@@ -56,17 +59,17 @@ describe('Client Navigation rendering <Head />', () => {
5659
test('header helper renders header information', async () => {
5760
const html = await render('/head')
5861
expect(html).toContain(
59-
strictNextHead
62+
strictNextHead !== false
6063
? '<meta charSet="iso-8859-5" data-next-head=""/>'
6164
: '<meta charSet="iso-8859-5"/>'
6265
)
6366
expect(html).toContain(
64-
strictNextHead
67+
strictNextHead !== false
6568
? '<meta content="my meta" data-next-head=""/>'
6669
: '<meta content="my meta"/>'
6770
)
6871
expect(html).toContain(
69-
strictNextHead
72+
strictNextHead !== false
7073
? '<meta name="viewport" content="width=device-width,initial-scale=1" data-next-head=""/>'
7174
: '<meta name="viewport" content="width=device-width,initial-scale=1"/>'
7275
)
@@ -76,17 +79,17 @@ describe('Client Navigation rendering <Head />', () => {
7679
test('header helper dedupes tags', async () => {
7780
const html = await render('/head')
7881
expect(html).toContain(
79-
strictNextHead
82+
strictNextHead !== false
8083
? '<meta charSet="iso-8859-5" data-next-head=""/>'
8184
: '<meta charSet="iso-8859-5"/>'
8285
)
8386
expect(html).not.toContain(
84-
strictNextHead
87+
strictNextHead !== false
8588
? '<meta charSet="utf-8" data-next-head=""/>'
8689
: '<meta charSet="utf-8"/>'
8790
)
8891
expect(html).toContain(
89-
strictNextHead
92+
strictNextHead !== false
9093
? '<meta name="viewport" content="width=device-width,initial-scale=1" data-next-head=""/>'
9194
: '<meta name="viewport" content="width=device-width,initial-scale=1"/>'
9295
)
@@ -96,24 +99,25 @@ describe('Client Navigation rendering <Head />', () => {
9699
'<meta name="viewport" content="width=device-width"'
97100
)
98101
expect(html).toContain(
99-
strictNextHead
102+
strictNextHead !== false
100103
? '<meta content="my meta" data-next-head=""/>'
101104
: '<meta content="my meta"/>'
102105
)
103106
expect(html).toContain(
104-
strictNextHead
107+
strictNextHead !== false
105108
? '<link rel="stylesheet" href="/dup-style.css" data-next-head=""/><link rel="stylesheet" href="/dup-style.css" data-next-head=""/>'
106109
: '<link rel="stylesheet" href="/dup-style.css"/><link rel="stylesheet" href="/dup-style.css"/>'
107110
)
108-
const dedupeLink = strictNextHead
109-
? '<link rel="stylesheet" href="dedupe-style.css" data-next-head=""/>'
110-
: '<link rel="stylesheet" href="dedupe-style.css"/>'
111+
const dedupeLink =
112+
strictNextHead !== false
113+
? '<link rel="stylesheet" href="dedupe-style.css" data-next-head=""/>'
114+
: '<link rel="stylesheet" href="dedupe-style.css"/>'
111115
expect(html).toContain(dedupeLink)
112116
expect(
113117
html.substring(html.indexOf(dedupeLink) + dedupeLink.length)
114118
).not.toContain('<link rel="stylesheet" href="dedupe-style.css"')
115119
expect(html).toContain(
116-
strictNextHead
120+
strictNextHead !== false
117121
? '<link rel="alternate" hrefLang="en" href="/last/en" data-next-head=""/>'
118122
: '<link rel="alternate" hrefLang="en" href="/last/en"/>'
119123
)
@@ -129,12 +133,12 @@ describe('Client Navigation rendering <Head />', () => {
129133
// Expect exactly one `viewport`
130134
expect((html.match(/name="viewport"/g) || []).length).toBe(1)
131135
expect(html).toContain(
132-
strictNextHead
136+
strictNextHead !== false
133137
? '<meta charSet="iso-8859-1" data-next-head=""/>'
134138
: '<meta charSet="iso-8859-1"/>'
135139
)
136140
expect(html).toContain(
137-
strictNextHead
141+
strictNextHead !== false
138142
? '<meta name="viewport" content="width=500" data-next-head=""/>'
139143
: '<meta name="viewport" content="width=500"/>'
140144
)
@@ -144,98 +148,98 @@ describe('Client Navigation rendering <Head />', () => {
144148
const html = await render('/head')
145149
console.log(html)
146150
expect(html).toContain(
147-
strictNextHead
151+
strictNextHead !== false
148152
? '<meta property="article:tag" content="tag1" data-next-head=""/>'
149153
: '<meta property="article:tag" content="tag1"/>'
150154
)
151155
expect(html).toContain(
152-
strictNextHead
156+
strictNextHead !== false
153157
? '<meta property="article:tag" content="tag2" data-next-head=""/>'
154158
: '<meta property="article:tag" content="tag2"/>'
155159
)
156160
expect(html).not.toContain('<meta property="dedupe:tag" content="tag3"')
157161
expect(html).toContain(
158-
strictNextHead
162+
strictNextHead !== false
159163
? '<meta property="dedupe:tag" content="tag4" data-next-head=""/>'
160164
: '<meta property="dedupe:tag" content="tag4"/>'
161165
)
162166
expect(html).toContain(
163-
strictNextHead
167+
strictNextHead !== false
164168
? '<meta property="og:image" content="ogImageTag1" data-next-head=""/>'
165169
: '<meta property="og:image" content="ogImageTag1"/>'
166170
)
167171
expect(html).toContain(
168-
strictNextHead
172+
strictNextHead !== false
169173
? '<meta property="og:image" content="ogImageTag2" data-next-head=""/>'
170174
: '<meta property="og:image" content="ogImageTag2"/>'
171175
)
172176
expect(html).toContain(
173-
strictNextHead
177+
strictNextHead !== false
174178
? '<meta property="og:image:alt" content="ogImageAltTag1" data-next-head=""/>'
175179
: '<meta property="og:image:alt" content="ogImageAltTag1"/>'
176180
)
177181
expect(html).toContain(
178-
strictNextHead
182+
strictNextHead !== false
179183
? '<meta property="og:image:alt" content="ogImageAltTag2" data-next-head=""/>'
180184
: '<meta property="og:image:alt" content="ogImageAltTag2"/>'
181185
)
182186
expect(html).toContain(
183-
strictNextHead
187+
strictNextHead !== false
184188
? '<meta property="og:image:width" content="ogImageWidthTag1" data-next-head=""/>'
185189
: '<meta property="og:image:width" content="ogImageWidthTag1"/>'
186190
)
187191
expect(html).toContain(
188-
strictNextHead
192+
strictNextHead !== false
189193
? '<meta property="og:image:width" content="ogImageWidthTag2" data-next-head=""/>'
190194
: '<meta property="og:image:width" content="ogImageWidthTag2"/>'
191195
)
192196
expect(html).toContain(
193-
strictNextHead
197+
strictNextHead !== false
194198
? '<meta property="og:image:height" content="ogImageHeightTag1" data-next-head=""/>'
195199
: '<meta property="og:image:height" content="ogImageHeightTag1"/>'
196200
)
197201
expect(html).toContain(
198-
strictNextHead
202+
strictNextHead !== false
199203
? '<meta property="og:image:height" content="ogImageHeightTag2" data-next-head=""/>'
200204
: '<meta property="og:image:height" content="ogImageHeightTag2"/>'
201205
)
202206
expect(html).toContain(
203-
strictNextHead
207+
strictNextHead !== false
204208
? '<meta property="og:image:type" content="ogImageTypeTag1" data-next-head=""/>'
205209
: '<meta property="og:image:type" content="ogImageTypeTag1"/>'
206210
)
207211
expect(html).toContain(
208-
strictNextHead
212+
strictNextHead !== false
209213
? '<meta property="og:image:type" content="ogImageTypeTag2" data-next-head=""/>'
210214
: '<meta property="og:image:type" content="ogImageTypeTag2"/>'
211215
)
212216
expect(html).toContain(
213-
strictNextHead
217+
strictNextHead !== false
214218
? '<meta property="og:image:secure_url" content="ogImageSecureUrlTag1" data-next-head=""/>'
215219
: '<meta property="og:image:secure_url" content="ogImageSecureUrlTag1"/>'
216220
)
217221
expect(html).toContain(
218-
strictNextHead
222+
strictNextHead !== false
219223
? '<meta property="og:image:secure_url" content="ogImageSecureUrlTag2" data-next-head=""/>'
220224
: '<meta property="og:image:secure_url" content="ogImageSecureUrlTag2"/>'
221225
)
222226
expect(html).toContain(
223-
strictNextHead
227+
strictNextHead !== false
224228
? '<meta property="og:image:url" content="ogImageUrlTag1" data-next-head=""/>'
225229
: '<meta property="og:image:url" content="ogImageUrlTag1"/>'
226230
)
227231
expect(html).toContain(
228-
strictNextHead
232+
strictNextHead !== false
229233
? '<meta property="og:image:url" content="ogImageUrlTag2" data-next-head=""/>'
230234
: '<meta property="og:image:url" content="ogImageUrlTag2"/>'
231235
)
232236
expect(html).toContain(
233-
strictNextHead
237+
strictNextHead !== false
234238
? '<meta property="fb:pages" content="fbpages1" data-next-head=""/>'
235239
: '<meta property="fb:pages" content="fbpages1"/>'
236240
)
237241
expect(html).toContain(
238-
strictNextHead
242+
strictNextHead !== false
239243
? '<meta property="fb:pages" content="fbpages2" data-next-head=""/>'
240244
: '<meta property="fb:pages" content="fbpages2"/>'
241245
)
@@ -244,12 +248,12 @@ describe('Client Navigation rendering <Head />', () => {
244248
test('header helper avoids dedupe of meta tags with the same name if they use unique keys', async () => {
245249
const html = await render('/head')
246250
expect(html).toContain(
247-
strictNextHead
251+
strictNextHead !== false
248252
? '<meta name="citation_author" content="authorName1" data-next-head=""/>'
249253
: '<meta name="citation_author" content="authorName1"/>'
250254
)
251255
expect(html).toContain(
252-
strictNextHead
256+
strictNextHead !== false
253257
? '<meta name="citation_author" content="authorName2" data-next-head=""/>'
254258
: '<meta name="citation_author" content="authorName2"/>'
255259
)
@@ -258,12 +262,12 @@ describe('Client Navigation rendering <Head />', () => {
258262
test('header helper renders Fragment children', async () => {
259263
const html = await render('/head')
260264
expect(html).toContain(
261-
strictNextHead
265+
strictNextHead !== false
262266
? '<title data-next-head="">Fragment title</title>'
263267
: '<title>Fragment title</title>'
264268
)
265269
expect(html).toContain(
266-
strictNextHead
270+
strictNextHead !== false
267271
? '<meta content="meta fragment" data-next-head=""/>'
268272
: '<meta content="meta fragment"/>'
269273
)
@@ -272,27 +276,28 @@ describe('Client Navigation rendering <Head />', () => {
272276
test('header helper renders boolean attributes correctly children', async () => {
273277
const html = await render('/head')
274278
expect(html).toContain(
275-
strictNextHead
279+
strictNextHead !== false
276280
? '<script src="/test-async-false.js" data-next-head="">'
277281
: '<script src="/test-async-false.js">'
278282
)
279283
expect(html).toContain(
280-
strictNextHead
284+
strictNextHead !== false
281285
? '<script src="/test-async-true.js" async="" data-next-head="">'
282286
: ''
283287
)
284288
expect(html).toContain(
285-
strictNextHead
289+
strictNextHead !== false
286290
? '<script src="/test-defer.js" defer="" data-next-head="">'
287291
: ''
288292
)
289293
})
290294

291295
it('should place charset element at the top of <head>', async () => {
292296
const html = await render('/head-priority')
293-
const nextHeadElement = strictNextHead
294-
? '<meta charSet="iso-8859-5" data-next-head=""/><meta name="viewport" content="width=device-width,initial-scale=1" data-next-head=""/><meta name="title" content="head title" data-next-head=""/>'
295-
: '<meta charSet="iso-8859-5"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="title" content="head title"/><meta name="next-head-count" content="3"/>'
297+
const nextHeadElement =
298+
strictNextHead !== false
299+
? '<meta charSet="iso-8859-5" data-next-head=""/><meta name="viewport" content="width=device-width,initial-scale=1" data-next-head=""/><meta name="title" content="head title" data-next-head=""/>'
300+
: '<meta charSet="iso-8859-5"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="title" content="head title"/><meta name="next-head-count" content="3"/>'
296301
const documentHeadElement =
297302
'<meta name="keywords" content="document head test"/>'
298303

@@ -304,6 +309,35 @@ describe('Client Navigation rendering <Head />', () => {
304309
: `<head>${nextHeadElement}${documentHeadElement}`
305310
)
306311
})
312+
313+
test('custom meta properties are rendered only once', async () => {
314+
const browser = await next.browser('/head-with-custom-metadata')
315+
316+
// Check that title appears only once
317+
const titleElements = await browser.elementsByCss('title')
318+
expect(titleElements).toHaveLength(1)
319+
const titleText = await browser.elementByCss('title').text()
320+
expect(titleText).toBe('Title Page')
321+
322+
// Check that each meta property appears only once
323+
const ogTitleElements = await browser.elementsByCss(
324+
'meta[property="og:title"]'
325+
)
326+
expect(ogTitleElements).toHaveLength(1)
327+
const ogTitleContent = await browser
328+
.elementByCss('meta[property="og:title"]')
329+
.getAttribute('content')
330+
expect(ogTitleContent).toBe('Title Content')
331+
332+
const descriptionElements = await browser.elementsByCss(
333+
'meta[name="description"]'
334+
)
335+
expect(descriptionElements).toHaveLength(1)
336+
const descriptionContent = await browser
337+
.elementByCss('meta[name="description"]')
338+
.getAttribute('content')
339+
expect(descriptionContent).toBe('Description Content')
340+
})
307341
}
308342
)
309343
})

0 commit comments

Comments
 (0)