Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor freezing metadata while resolving and fix title merging #45965

Merged
merged 9 commits into from
Feb 17, 2023
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
adopt feedbacks, resolve title separately
huozhi committed Feb 16, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit fb9f9d4ff79519974d46cb9f7a90860cf7ff7f77
2 changes: 1 addition & 1 deletion packages/next/src/lib/metadata/generate/basic.tsx
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ export function BasicMetadata({ metadata }: { metadata: ResolvedMetadata }) {
return (
<>
<meta charSet="utf-8" />
{metadata.title !== null ? (
{metadata.title !== null && metadata.title.absolute ? (
<title>{metadata.title.absolute}</title>
) : null}
<Meta name="description" content={metadata.description} />
93 changes: 42 additions & 51 deletions packages/next/src/lib/metadata/resolve-metadata.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import type { AbsoluteTemplateString } from './types/metadata-types'
import type { MetadataImageModule } from '../../build/webpack/loaders/metadata/types'
import { createDefaultMetadata } from './default-metadata'
import { resolveOpenGraph, resolveTwitter } from './resolvers/resolve-opengraph'
import { mergeTitle } from './resolvers/resolve-title'
import { resolveTitle } from './resolvers/resolve-title'
import { resolveAsArrayOrUndefined } from './generate/utils'
import { isClientReference } from '../../build/is-client-reference'
import {
@@ -31,11 +31,10 @@ type StaticMetadata = Awaited<ReturnType<typeof resolveStaticMetadata>>
type MetadataResolver = (
_parent: ResolvingMetadata
) => Metadata | Promise<Metadata>
export type MetadataItems = {
metadataExport: Metadata | MetadataResolver | null
staticFilesMetadata: StaticMetadata
isLeafLayout: boolean
}[]
export type MetadataItems = [
Metadata | MetadataResolver | null,
StaticMetadata
][]

function mergeStaticMetadata(
metadata: ResolvedMetadata,
@@ -76,41 +75,23 @@ function mergeStaticMetadata(
function merge(
target: ResolvedMetadata,
source: Metadata | null,
staticFilesMetadata: StaticMetadata,
templateStrings: {
title: string | null
openGraph: string | null
twitter: string | null
}
staticFilesMetadata: StaticMetadata
) {
const metadataBase = source?.metadataBase || null
for (const key_ in source) {
const key = key_ as keyof Metadata

switch (key) {
case 'title': {
if (source.title) {
target.title = source.title as AbsoluteTemplateString
mergeTitle(target, templateStrings.title)
}
break
}
case 'alternates': {
target.alternates = resolveAlternates(source.alternates, metadataBase)
break
}
case 'openGraph': {
target.openGraph = resolveOpenGraph(source.openGraph, metadataBase)
if (target.openGraph) {
mergeTitle(target.openGraph, templateStrings.openGraph)
}
break
}
case 'twitter': {
target.twitter = resolveTwitter(source.twitter, metadataBase)
if (target.twitter) {
mergeTitle(target.twitter, templateStrings.twitter)
}
break
}
case 'verification':
@@ -237,14 +218,13 @@ async function resolveStaticMetadata(components: ComponentsType) {
export async function collectMetadata(
loaderTree: LoaderTree,
props: any,
array: MetadataItems,
isLeafLayout: boolean
array: MetadataItems
) {
const mod = await getLayoutOrPageModule(loaderTree)
const staticFilesMetadata = await resolveStaticMetadata(loaderTree[2])
const metadataExport = mod ? await getDefinedMetadata(mod, props) : null

array.push({ metadataExport, staticFilesMetadata, isLeafLayout })
array.push([metadataExport, staticFilesMetadata])
}

export async function accumulateMetadata(
huozhi marked this conversation as resolved.
Show resolved Hide resolved
@@ -255,29 +235,27 @@ export async function accumulateMetadata(
const resolvers: ((value: ResolvedMetadata) => void)[] = []
const generateMetadataResults: (Metadata | Promise<Metadata>)[] = []

// call each `generateMetadata function concurrently and stash their resolver
let resolvedTitle: AbsoluteTemplateString | null = null
let resolvedTwitterTitle: AbsoluteTemplateString | null = null
let resolvedOpenGraphTitle: AbsoluteTemplateString | null = null

// Loop over all metadata items again, merging synchronously any static object exports,
// awaiting any static promise exports, and resolving parent metadata and awaiting any generated metadata

let resolvingIndex = 0
for (let i = 0; i < metadataItems.length; i++) {
const { metadataExport } = metadataItems[i]
const [metadataExport, staticFilesMetadata] = metadataItems[i]
let metadata: Metadata | null = null
if (typeof metadataExport === 'function') {
// call each `generateMetadata function concurrently and stash their resolver
generateMetadataResults.push(
metadataExport(
new Promise((resolve) => {
resolvers.push(resolve)
})
)
)
huozhi marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Loop over all metadata items again, merging synchronously any static object exports,
// awaiting any static promise exports, and resolving parent metadata and awaiting any generated
// metadata
let resolvingIndex = 0
for (let i = 0; i < metadataItems.length; i++) {
const { metadataExport, staticFilesMetadata, isLeafLayout } =
metadataItems[i]
let metadata: Metadata | null = null
if (typeof metadataExport === 'function') {
const resolveParent = resolvers[resolvingIndex]
const generatedMetadata = generateMetadataResults[resolvingIndex]
resolvingIndex++
@@ -294,24 +272,37 @@ export async function accumulateMetadata(

// This resolve should unblock the generateMetadata function if it awaited the parent
// argument. If it didn't await the parent argument it might already have a value since it was
// called concurrently. Regardless we await the return value before continuing on to the next
// layer
// called concurrently. Regardless we await the return value before continuing on to the next layer
resolveParent(currentResolvedMetadata)
metadata = await generatedMetadata
metadata =
generatedMetadata instanceof Promise
? await generatedMetadata
: generatedMetadata
} else {
metadata = metadataExport
}

// If the layout is the same layer with page, skip the
if (isLeafLayout && metadata) {
delete metadata.title
if ((i === 0 || (i !== 0 && i !== metadataItems.length - 2)) && metadata) {
huozhi marked this conversation as resolved.
Show resolved Hide resolved
resolvedTitle = resolveTitle(metadata?.title, resolvedTitle?.template)
resolvedTwitterTitle = resolveTitle(
metadata?.twitter?.title,
resolvedTwitterTitle?.template
)
resolvedOpenGraphTitle = resolveTitle(
metadata?.openGraph?.title,
resolvedOpenGraphTitle?.template
)
}

merge(resolvedMetadata, metadata, staticFilesMetadata, {
title: resolvedMetadata.title?.template || null,
openGraph: resolvedMetadata.openGraph?.title?.template || null,
twitter: resolvedMetadata.twitter?.title?.template || null,
})
merge(resolvedMetadata, metadata, staticFilesMetadata)
huozhi marked this conversation as resolved.
Show resolved Hide resolved
}

resolvedMetadata.title = resolvedTitle
if (resolvedMetadata.twitter && resolvedTwitterTitle)
resolvedMetadata.twitter.title = resolvedTwitterTitle
if (resolvedMetadata.openGraph && resolvedOpenGraphTitle)
resolvedMetadata.openGraph.title = resolvedOpenGraphTitle

return resolvedMetadata
}
33 changes: 16 additions & 17 deletions packages/next/src/lib/metadata/resolvers/resolve-title.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import type { Metadata } from '../types/metadata-interface'
import type { AbsoluteTemplateString } from '../types/metadata-types'

function resolveTitleTemplate(template: string | null, title: string) {
function resolveTitleTemplate(
template: string | null | undefined,
title: string
) {
return template ? template.replace(/%s/g, title) : title
}

export function mergeTitle<T extends { title?: Metadata['title'] }>(
source: T,
stashedTemplate: string | null
) {
const { title } = source

export function resolveTitle(
title: Metadata['title'],
stashedTemplate: string | null | undefined
): AbsoluteTemplateString {
let resolved
const template =
typeof source.title !== 'string' &&
source.title &&
'template' in source.title
? source.title.template
: null
typeof title !== 'string' && title && 'template' in title
? title.template
: stashedTemplate || null

if (typeof title === 'string') {
resolved = resolveTitleTemplate(stashedTemplate, title)
@@ -30,12 +29,12 @@ export function mergeTitle<T extends { title?: Metadata['title'] }>(
}
}

const target = source
if (title && typeof title !== 'string') {
const targetTitle = title as AbsoluteTemplateString
targetTitle.template = template
targetTitle.absolute = resolved || ''
return {
template,
absolute: resolved || '',
}
} else {
target.title = { absolute: resolved || title || '', template }
return { absolute: resolved || title || '', template }
}
}
5 changes: 1 addition & 4 deletions packages/next/src/server/app-render.tsx
Original file line number Diff line number Diff line change
@@ -1138,10 +1138,7 @@ export async function renderToHTMLOrFlight(
...(isPage && searchParamsProps),
}

// If the layout is located next to the page as leaf layer
const isLeafLayout =
typeof parallelRoutes.children?.[2].page !== 'undefined'
await collectMetadata(tree, layerProps, metadataItems, isLeafLayout)
await collectMetadata(tree, layerProps, metadataItems)

for (const key in parallelRoutes) {
const childTree = parallelRoutes[key]
1 change: 1 addition & 0 deletions test/e2e/app-dir/metadata/app/title-template/layout.tsx
Original file line number Diff line number Diff line change
@@ -5,5 +5,6 @@ export default function Layout(props) {
export const metadata = {
title: {
template: '%s | Layout',
default: 'Layout title',
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return 'use layout title template'
}
5 changes: 5 additions & 0 deletions test/e2e/app-dir/metadata/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -91,6 +91,11 @@ createNextDescribe(
expect(await browser.eval(`document.title`)).toBe('Extra Page | Layout')
})

it('should use parent layout title when no title is defined in page', async () => {
const browser = await next.browser('/title-template/use-layout-title')
expect(await browser.eval(`document.title`)).toBe('Layout title')
})

it('should support stashed title in two layers of page and layout', async () => {
const browser = await next.browser('/title-template/extra/inner')
expect(await browser.eval(`document.title`)).toBe(