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

Resolve Viewport separately from Metadata #77427

Merged
merged 1 commit into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 3 additions & 4 deletions packages/next/src/lib/metadata/metadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,17 +355,16 @@ async function renderViewport(
workStore: WorkStore,
errorConvention?: MetadataErrorType
) {
const notFoundResolvedViewport = await resolveViewport(
const resolvedViewport = await resolveViewport(
tree,
searchParams,
errorConvention,
getDynamicParamFromSegment,
workStore
)

const elements: Array<React.ReactNode> = createViewportElements(
notFoundResolvedViewport
)
const elements: Array<React.ReactNode> =
createViewportElements(resolvedViewport)
return (
<>
{elements.map((el, index) => {
Expand Down
5 changes: 1 addition & 4 deletions packages/next/src/lib/metadata/resolve-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ function accumulateMetadata(metadataItems: MetadataItems) {
const fullMetadataItems: FullMetadataItems = metadataItems.map((item) => [
item[0],
item[1],
null,
])
return originAccumulateMetadata(fullMetadataItems, {
pathname: '/test',
Expand All @@ -23,9 +22,7 @@ function accumulateMetadata(metadataItems: MetadataItems) {

function accumulateViewport(viewportExports: Viewport[]) {
// skip the first two arguments (metadata and static metadata)
return originAccumulateViewport(
viewportExports.map((item) => [null, null, item])
)
return originAccumulateViewport(viewportExports.map((item) => item))
}

function mapUrlsToStrings(obj: any) {
Expand Down
190 changes: 170 additions & 20 deletions packages/next/src/lib/metadata/resolve-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ type ViewportResolver = (

export type MetadataErrorType = 'not-found' | 'forbidden' | 'unauthorized'

export type MetadataItems = [
Metadata | MetadataResolver | null,
StaticMetadata,
Viewport | ViewportResolver | null,
][]
export type MetadataItems = Array<
[Metadata | MetadataResolver | null, StaticMetadata]
>

export type ViewportItems = Array<Viewport | ViewportResolver | null>

type TitleTemplates = {
title: string | null
Expand Down Expand Up @@ -457,22 +457,65 @@ async function collectMetadata({
const staticFilesMetadata = await resolveStaticMetadata(tree[2], props)
const metadataExport = mod ? getDefinedMetadata(mod, props, { route }) : null

const viewportExport = mod ? getDefinedViewport(mod, props, { route }) : null

metadataItems.push([metadataExport, staticFilesMetadata, viewportExport])
metadataItems.push([metadataExport, staticFilesMetadata])

if (hasErrorConventionComponent && errorConvention) {
const errorMod = await getComponentTypeModule(tree, errorConvention)
const errorViewportExport = errorMod
? getDefinedViewport(errorMod, props, { route })
: null
const errorMetadataExport = errorMod
? getDefinedMetadata(errorMod, props, { route })
: null

errorMetadataItem[0] = errorMetadataExport
errorMetadataItem[1] = staticFilesMetadata
errorMetadataItem[2] = errorViewportExport
}
}

// [layout.metadata, static files metadata] -> ... -> [page.metadata, static files metadata]
async function collectViewport({
tree,
viewportItems,
errorViewportItemRef,
props,
route,
errorConvention,
}: {
tree: LoaderTree
viewportItems: ViewportItems
errorViewportItemRef: ErrorViewportItemRef
props: any
route: string
errorConvention?: MetadataErrorType
}) {
let mod
let modType
const hasErrorConventionComponent = Boolean(
errorConvention && tree[2][errorConvention]
)
if (errorConvention) {
mod = await getComponentTypeModule(tree, 'layout')
modType = errorConvention
} else {
const { mod: layoutOrPageMod, modType: layoutOrPageModType } =
await getLayoutOrPageModule(tree)
mod = layoutOrPageMod
modType = layoutOrPageModType
}

if (modType) {
route += `/${modType}`
}

const viewportExport = mod ? getDefinedViewport(mod, props, { route }) : null

viewportItems.push(viewportExport)

if (hasErrorConventionComponent && errorConvention) {
const errorMod = await getComponentTypeModule(tree, errorConvention)
const errorViewportExport = errorMod
? getDefinedViewport(errorMod, props, { route })
: null

errorViewportItemRef.current = errorViewportExport
}
}

Expand All @@ -485,7 +528,7 @@ const resolveMetadataItems = cache(async function (
) {
const parentParams = {}
const metadataItems: MetadataItems = []
const errorMetadataItem: MetadataItems[number] = [null, null, null]
const errorMetadataItem: MetadataItems[number] = [null, null]
const treePrefix = undefined
return resolveMetadataItemsImpl(
metadataItems,
Expand Down Expand Up @@ -580,6 +623,113 @@ async function resolveMetadataItemsImpl(
return metadataItems
}

type ErrorViewportItemRef = { current: ViewportItems[number] }
const resolveViewportItems = cache(async function (
tree: LoaderTree,
searchParams: Promise<ParsedUrlQuery>,
errorConvention: MetadataErrorType | undefined,
getDynamicParamFromSegment: GetDynamicParamFromSegment,
workStore: WorkStore
) {
const parentParams = {}
const viewportItems: ViewportItems = []
const errorViewportItemRef: ErrorViewportItemRef = {
current: null,
}
const treePrefix = undefined
return resolveViewportItemsImpl(
viewportItems,
tree,
treePrefix,
parentParams,
searchParams,
errorConvention,
errorViewportItemRef,
getDynamicParamFromSegment,
workStore
)
})

async function resolveViewportItemsImpl(
viewportItems: ViewportItems,
tree: LoaderTree,
/** Provided tree can be nested subtree, this argument says what is the path of such subtree */
treePrefix: undefined | string[],
parentParams: Params,
searchParams: Promise<ParsedUrlQuery>,
errorConvention: MetadataErrorType | undefined,
errorViewportItemRef: ErrorViewportItemRef,
getDynamicParamFromSegment: GetDynamicParamFromSegment,
workStore: WorkStore
): Promise<ViewportItems> {
const [segment, parallelRoutes, { page }] = tree
const currentTreePrefix =
treePrefix && treePrefix.length ? [...treePrefix, segment] : [segment]
const isPage = typeof page !== 'undefined'

// Handle dynamic segment params.
const segmentParam = getDynamicParamFromSegment(segment)
/**
* Create object holding the parent params and current params
*/
let currentParams = parentParams
if (segmentParam && segmentParam.value !== null) {
currentParams = {
...parentParams,
[segmentParam.param]: segmentParam.value,
}
}

const params = createServerParamsForMetadata(currentParams, workStore)

let layerProps: LayoutProps | PageProps
if (isPage) {
layerProps = {
params,
searchParams,
}
} else {
layerProps = {
params,
}
}

await collectViewport({
tree,
viewportItems,
errorViewportItemRef,
errorConvention,
props: layerProps,
route: currentTreePrefix
// __PAGE__ shouldn't be shown in a route
.filter((s) => s !== PAGE_SEGMENT_KEY)
.join('/'),
})

for (const key in parallelRoutes) {
const childTree = parallelRoutes[key]
await resolveViewportItemsImpl(
viewportItems,
childTree,
currentTreePrefix,
currentParams,
searchParams,
errorConvention,
errorViewportItemRef,
getDynamicParamFromSegment,
workStore
)
}

if (Object.keys(parallelRoutes).length === 0 && errorConvention) {
// If there are no parallel routes, place error metadata as the last item.
// e.g. layout -> layout -> not-found
viewportItems.push(errorViewportItemRef.current)
}

return viewportItems
}

type WithTitle = { title?: AbsoluteTemplateString | null }
type WithDescription = { description?: string | null }

Expand Down Expand Up @@ -692,15 +842,15 @@ function prerenderMetadata(metadataItems: MetadataItems) {
return resolversAndResults
}

function prerenderViewport(metadataItems: MetadataItems) {
function prerenderViewport(viewportItems: ViewportItems) {
// If the index is a function then it is a resolver and the next slot
// is the corresponding result. If the index is not a function it is the result
// itself.
const resolversAndResults: Array<
((value: ResolvedViewport) => void) | Result<Viewport>
> = []
for (let i = 0; i < metadataItems.length; i++) {
const viewportExport = metadataItems[i][2]
for (let i = 0; i < viewportItems.length; i++) {
const viewportExport = viewportItems[i]
getResult(resolversAndResults, viewportExport)
}
return resolversAndResults
Expand Down Expand Up @@ -865,11 +1015,11 @@ export async function accumulateMetadata(
}

export async function accumulateViewport(
metadataItems: MetadataItems
viewportItems: ViewportItems
): Promise<ResolvedViewport> {
const resolvedViewport: ResolvedViewport = createDefaultViewport()

const resolversAndResults = prerenderViewport(metadataItems)
const resolversAndResults = prerenderViewport(viewportItems)
let i = 0

while (i < resolversAndResults.length) {
Expand Down Expand Up @@ -929,14 +1079,14 @@ export async function resolveViewport(
getDynamicParamFromSegment: GetDynamicParamFromSegment,
workStore: WorkStore
): Promise<ResolvedViewport> {
const metadataItems = await resolveMetadataItems(
const viewportItems = await resolveViewportItems(
tree,
searchParams,
errorConvention,
getDynamicParamFromSegment,
workStore
)
return accumulateViewport(metadataItems)
return accumulateViewport(viewportItems)
}

function isPromiseLike<T>(
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/app-dir/metadata/app/dynamic/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
async function format({ params, searchParams }) {
const { slug } = params
const { slug } = await params
const { q } = await searchParams
return `params - ${slug}${q ? ` query - ${q}` : ''}`
}
Expand Down
14 changes: 6 additions & 8 deletions test/e2e/app-dir/metadata/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,8 +549,8 @@ describe('app dir - metadata', () => {
it('should render icon and apple touch icon meta if their images are specified', async () => {
const $ = await next.render$('/icons/static/nested')

const $icon = $('head > link[rel="icon"][type!="image/x-icon"]')
const $appleIcon = $('head > link[rel="apple-touch-icon"]')
const $icon = $('link[rel="icon"][type!="image/x-icon"]')
const $appleIcon = $('link[rel="apple-touch-icon"]')

expect($icon.attr('href')).toMatch(/\/icons\/static\/nested\/icon1/)
expect($icon.attr('sizes')).toBe('32x32')
Expand All @@ -565,19 +565,17 @@ describe('app dir - metadata', () => {
it('should not render if image file is not specified', async () => {
const $ = await next.render$('/icons/static')

const $icon = $('head > link[rel="icon"][type!="image/x-icon"]')
const $icon = $('link[rel="icon"][type!="image/x-icon"]')

expect($icon.attr('href')).toMatch(/\/icons\/static\/icon/)
expect($icon.attr('sizes')).toBe('114x114')

// No apple icon if it's not provided
const $appleIcon = $('head > link[rel="apple-touch-icon"]')
const $appleIcon = $('link[rel="apple-touch-icon"]')
expect($appleIcon.length).toBe(0)

const $dynamic = await next.render$('/icons/static/dynamic-routes/123')
const $dynamicIcon = $dynamic(
'head > link[rel="icon"][type!="image/x-icon"]'
)
const $dynamicIcon = $dynamic('link[rel="icon"][type!="image/x-icon"]')
const dynamicIconHref = $dynamicIcon.attr('href')
expect(dynamicIconHref).toMatch(
/\/icons\/static\/dynamic-routes\/123\/icon/
Expand Down Expand Up @@ -845,7 +843,7 @@ describe('app dir - metadata', () => {

await check(async () => {
const $ = await next.render$('/icons/static')
const $icon = $('head > link[rel="icon"][type!="image/x-icon"]')
const $icon = $('link[rel="icon"][type!="image/x-icon"]')
return $icon.attr('href')
}, /\/icons\/static\/icon2/)

Expand Down
Loading