-
Notifications
You must be signed in to change notification settings - Fork 26.1k
/
metadata.tsx
156 lines (146 loc) · 5.25 KB
/
metadata.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import type { ParsedUrlQuery } from 'querystring'
import type { GetDynamicParamFromSegment } from '../../server/app-render/app-render'
import type { LoaderTree } from '../../server/lib/app-dir-module'
import React from 'react'
import {
AppleWebAppMeta,
FormatDetectionMeta,
ItunesMeta,
BasicMeta,
ViewportMeta,
VerificationMeta,
} from './generate/basic'
import { AlternatesMetadata } from './generate/alternate'
import {
OpenGraphMetadata,
TwitterMetadata,
AppLinksMeta,
} from './generate/opengraph'
import { IconsMetadata } from './generate/icons'
import { resolveMetadata } from './resolve-metadata'
import { MetaFilter } from './generate/meta'
import type {
ResolvedMetadata,
ResolvedViewport,
} from './types/metadata-interface'
import {
createDefaultMetadata,
createDefaultViewport,
} from './default-metadata'
import { isNotFoundError } from '../../client/components/not-found'
// Use a promise to share the status of the metadata resolving,
// returning two components `MetadataTree` and `MetadataOutlet`
// `MetadataTree` is the one that will be rendered at first in the content sequence for metadata tags.
// `MetadataOutlet` is the one that will be rendered under error boundaries for metadata resolving errors.
// In this way we can let the metadata tags always render successfully,
// and the error will be caught by the error boundary and trigger fallbacks.
export function createMetadataComponents({
tree,
pathname,
trailingSlash,
query,
getDynamicParamFromSegment,
appUsingSizeAdjustment,
errorType,
createDynamicallyTrackedSearchParams,
}: {
tree: LoaderTree
pathname: string
trailingSlash: boolean
query: ParsedUrlQuery
getDynamicParamFromSegment: GetDynamicParamFromSegment
appUsingSizeAdjustment: boolean
errorType?: 'not-found' | 'redirect'
createDynamicallyTrackedSearchParams: (
searchParams: ParsedUrlQuery
) => ParsedUrlQuery
}): [React.ComponentType, React.ComponentType] {
const metadataContext = {
// Make sure the pathname without query string
pathname: pathname.split('?')[0],
trailingSlash,
}
let resolve: (value: Error | undefined) => void | undefined
// Only use promise.resolve here to avoid unhandled rejections
const metadataErrorResolving = new Promise<Error | undefined>((res) => {
resolve = res
})
async function MetadataTree() {
const defaultMetadata = createDefaultMetadata()
const defaultViewport = createDefaultViewport()
let metadata: ResolvedMetadata | undefined = defaultMetadata
let viewport: ResolvedViewport | undefined = defaultViewport
let error: any
const errorMetadataItem: [null, null, null] = [null, null, null]
const errorConvention = errorType === 'redirect' ? undefined : errorType
const searchParams = createDynamicallyTrackedSearchParams(query)
const [resolvedError, resolvedMetadata, resolvedViewport] =
await resolveMetadata({
tree,
parentParams: {},
metadataItems: [],
errorMetadataItem,
searchParams,
getDynamicParamFromSegment,
errorConvention,
metadataContext,
})
if (!resolvedError) {
viewport = resolvedViewport
metadata = resolvedMetadata
resolve(undefined)
} else {
error = resolvedError
// If a not-found error is triggered during metadata resolution, we want to capture the metadata
// for the not-found route instead of whatever triggered the error. For all error types, we resolve an
// error, which will cause the outlet to throw it so it'll be handled by an error boundary
// (either an actual error, or an internal error that renders UI such as the NotFoundBoundary).
if (!errorType && isNotFoundError(resolvedError)) {
const [notFoundMetadataError, notFoundMetadata, notFoundViewport] =
await resolveMetadata({
tree,
parentParams: {},
metadataItems: [],
errorMetadataItem,
searchParams,
getDynamicParamFromSegment,
errorConvention: 'not-found',
metadataContext,
})
viewport = notFoundViewport
metadata = notFoundMetadata
error = notFoundMetadataError || error
}
resolve(error)
}
const elements = MetaFilter([
ViewportMeta({ viewport: viewport }),
BasicMeta({ metadata }),
AlternatesMetadata({ alternates: metadata.alternates }),
ItunesMeta({ itunes: metadata.itunes }),
FormatDetectionMeta({ formatDetection: metadata.formatDetection }),
VerificationMeta({ verification: metadata.verification }),
AppleWebAppMeta({ appleWebApp: metadata.appleWebApp }),
OpenGraphMetadata({ openGraph: metadata.openGraph }),
TwitterMetadata({ twitter: metadata.twitter }),
AppLinksMeta({ appLinks: metadata.appLinks }),
IconsMetadata({ icons: metadata.icons }),
])
if (appUsingSizeAdjustment) elements.push(<meta name="next-size-adjust" />)
return (
<>
{elements.map((el, index) => {
return React.cloneElement(el as React.ReactElement, { key: index })
})}
</>
)
}
async function MetadataOutlet() {
const error = await metadataErrorResolving
if (error) {
throw error
}
return null
}
return [MetadataTree, MetadataOutlet]
}