Skip to content

Commit 337f33b

Browse files
authored
Inherit title and description from metadata into social cards (#57857)
Social metadata will be display properly if there's `title` and `description` configured. If you have `title` and `description` configured in metadata, some people's intuition will be "yes I had them, they should be the same if I have `openGraph` or `twitter`". This PR improves the configuration that you don't have to configure them as duplicate both in top `metadata` export and `metadata.openGraph` or `metadata.twitter`. But we don't always assign them if you don't configure social cards (open graph or twitter), this way users should be able to omit social cards if possible. x-ref: [feedback from slack](https://vercel.slack.com/archives/C03KAR5DCKC/p1698548491415089?thread_ts=1698512393.500649&cid=C03KAR5DCKC)
1 parent 6b6bfcd commit 337f33b

File tree

2 files changed

+115
-1
lines changed

2 files changed

+115
-1
lines changed

packages/next/src/lib/metadata/resolve-metadata.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,92 @@ describe('accumulateMetadata', () => {
461461
},
462462
})
463463
})
464+
465+
it('should inherit metadata title description into openGraph or twitter if they are configured', async () => {
466+
const metadataItems1: MetadataItems = [
467+
[
468+
{
469+
title: 'My title',
470+
description: 'My description',
471+
openGraph: {
472+
images: 'https://test.com/og.png',
473+
},
474+
},
475+
null,
476+
],
477+
]
478+
const metadata1 = await accumulateMetadata(metadataItems1)
479+
expect(metadata1).toMatchObject({
480+
openGraph: {
481+
title: {
482+
absolute: 'My title',
483+
template: null,
484+
},
485+
description: 'My description',
486+
},
487+
twitter: {
488+
title: {
489+
absolute: 'My title',
490+
template: null,
491+
},
492+
description: 'My description',
493+
},
494+
})
495+
496+
const metadataItems2: MetadataItems = [
497+
[
498+
{
499+
title: 'My title',
500+
description: 'My description',
501+
twitter: {
502+
images: 'https://test.com/twitter.png',
503+
},
504+
},
505+
null,
506+
],
507+
]
508+
const metadata2 = await accumulateMetadata(metadataItems2)
509+
expect(metadata2).toMatchObject({
510+
openGraph: null,
511+
twitter: {
512+
title: {
513+
absolute: 'My title',
514+
template: null,
515+
},
516+
description: 'My description',
517+
},
518+
})
519+
520+
// Don't override if there's already a title in twitter
521+
const metadataItems3: MetadataItems = [
522+
[
523+
{
524+
title: 'My title',
525+
description: 'My description',
526+
twitter: {
527+
title: 'My twitter title',
528+
images: 'https://test.com/twitter.png',
529+
},
530+
},
531+
null,
532+
],
533+
]
534+
const metadata3 = await accumulateMetadata(metadataItems3)
535+
expect(metadata3).toMatchObject({
536+
openGraph: null,
537+
title: {
538+
absolute: 'My title',
539+
template: null,
540+
},
541+
twitter: {
542+
title: {
543+
absolute: 'My twitter title',
544+
template: null,
545+
},
546+
description: 'My description',
547+
},
548+
})
549+
})
464550
})
465551

466552
describe('alternate', () => {

packages/next/src/lib/metadata/resolve-metadata.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { OpenGraph } from './types/opengraph-types'
1313
import type { ComponentsType } from '../../build/webpack/loaders/next-app-loader'
1414
import type { MetadataContext } from './types/resolvers'
1515
import type { LoaderTree } from '../../server/lib/app-dir-module'
16+
import type { AbsoluteTemplateString } from './types/metadata-types'
1617
import {
1718
createDefaultMetadata,
1819
createDefaultViewport,
@@ -517,19 +518,46 @@ export async function resolveMetadataItems({
517518
return metadataItems
518519
}
519520

521+
type WithTitle = { title?: AbsoluteTemplateString | null }
522+
type WithDescription = { description?: string | null }
523+
524+
const hasTitle = (metadata: WithTitle | null) => !!metadata?.title?.absolute
525+
526+
function inheritFromMetadata(
527+
metadata: ResolvedMetadata,
528+
target: (WithTitle & WithDescription) | null
529+
) {
530+
if (target) {
531+
if (!hasTitle(target) && hasTitle(metadata)) {
532+
target.title = metadata.title
533+
}
534+
if (!target.description && metadata.description) {
535+
target.description = metadata.description
536+
}
537+
}
538+
}
539+
520540
const commonOgKeys = ['title', 'description', 'images'] as const
521541
function postProcessMetadata(
522542
metadata: ResolvedMetadata,
523543
titleTemplates: TitleTemplates
524544
): ResolvedMetadata {
525545
const { openGraph, twitter } = metadata
546+
547+
// If there's no title and description configured in openGraph or twitter,
548+
// use the title and description from metadata.
549+
inheritFromMetadata(metadata, openGraph)
550+
inheritFromMetadata(metadata, twitter)
551+
526552
if (openGraph) {
553+
// If there's openGraph information but not configured in twitter,
554+
// inherit them from openGraph metadata.
527555
let autoFillProps: Partial<{
528556
[Key in (typeof commonOgKeys)[number]]: NonNullable<
529557
ResolvedMetadata['openGraph']
530558
>[Key]
531559
}> = {}
532-
const hasTwTitle = twitter?.title.absolute
560+
const hasTwTitle = hasTitle(twitter)
533561
const hasTwDescription = twitter?.description
534562
const hasTwImages = Boolean(
535563
twitter?.hasOwnProperty('images') && twitter.images

0 commit comments

Comments
 (0)