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

Initial support for metadata #44729

Merged
merged 44 commits into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
99b772c
initial impl
shuding Jan 5, 2023
cebcb87
add test case
shuding Jan 9, 2023
b183b3b
update hreflang list
shuding Jan 9, 2023
3578e1c
add title template
shuding Jan 9, 2023
28f639a
opengraph
shuding Jan 10, 2023
4d56260
opengraph parsing
shuding Jan 10, 2023
f022bed
fix key
shuding Jan 11, 2023
4cde8f8
add test
shuding Jan 11, 2023
2643bce
basic and alternate tags
shuding Jan 11, 2023
22ae819
more types and fixes
shuding Jan 11, 2023
8462dce
fix type gen
shuding Jan 11, 2023
151bf61
support client page and layout
shuding Jan 11, 2023
053bb4f
add test
shuding Jan 11, 2023
87d8f24
fix absolute title resolution
shuding Jan 12, 2023
3a147e6
fix metadata injection place
shuding Jan 13, 2023
d61826a
fix stashed titles
shuding Jan 13, 2023
0e25465
merge canary
shuding Jan 13, 2023
68a1d4b
fix reviews
shuding Jan 13, 2023
5aa9a04
add comment
shuding Jan 13, 2023
466177e
fix
shuding Jan 13, 2023
cf27019
fix fs-extra
shuding Jan 13, 2023
648e6d5
use switch for merging metdata, update error message
huozhi Jan 16, 2023
b081281
rm <base> rendering and fix tests
huozhi Jan 16, 2023
ace234b
simplify merging props
huozhi Jan 16, 2023
2316fff
Fix title merging with nested layouts
huozhi Jan 17, 2023
94271ca
rm duplicated check
huozhi Jan 17, 2023
eb9b8e6
fix test
huozhi Jan 17, 2023
067b913
merge canary
huozhi Jan 17, 2023
d612101
update per review to simplify merge and resolving
huozhi Jan 18, 2023
aeedcdd
use jsx for unblock data fetching in app router
huozhi Jan 18, 2023
f014661
disable default head.js when metadata is present
huozhi Jan 18, 2023
e631a15
remove default head
huozhi Jan 18, 2023
98dffcd
only pass template string to merge
huozhi Jan 18, 2023
6e983d1
update viewport handling
huozhi Jan 18, 2023
7715d8b
move viewport handling to resolving stage
huozhi Jan 18, 2023
31e9396
fix lint
huozhi Jan 18, 2023
3c0c8b0
simplify resolving
huozhi Jan 18, 2023
c1785c1
simplify twitter title resolving
huozhi Jan 18, 2023
2601485
use as for casting
huozhi Jan 18, 2023
013248d
merge title
huozhi Jan 18, 2023
d070664
Add metadata to head handling
timneutkens Jan 19, 2023
6848b4e
migrate tests to standalone metadata folder
huozhi Jan 19, 2023
6587394
add navigation test
huozhi Jan 19, 2023
cb1efbb
Merge branch 'canary' into shu/7s22
kodiakhq[bot] Jan 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
86 changes: 69 additions & 17 deletions packages/next/src/build/webpack/loaders/next-app-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { sep } from 'path'
import { verifyRootLayout } from '../../../lib/verifyRootLayout'
import * as Log from '../../../build/output/log'
import { APP_DIR_ALIAS } from '../../../lib/constants'
import { resolveFileBasedMetadataForLoader } from '../../../lib/metadata/resolve-metadata'

const FILE_TYPES = {
layout: 'layout',
Expand Down Expand Up @@ -36,20 +37,24 @@ async function createTreeCodeFromPath({
resolveParallelSegments,
}: {
pagePath: string
resolve: (pathname: string) => Promise<string | undefined>
resolve: (
pathname: string,
resolveDir?: boolean
) => Promise<string | undefined>
resolveParallelSegments: (
pathname: string
) => [key: string, segment: string][]
}) {
const splittedPath = pagePath.split(/[\\/]/)
const appDirPrefix = splittedPath[0]
const pages: string[] = []

let rootLayout: string | undefined
let globalError: string | undefined

async function createSubtreePropsFromSegmentPath(
segments: string[]
): Promise<string> {
): Promise<[string, string]> {
const segmentPath = segments.join('/')

// Existing tree are the children of the current segment
Expand All @@ -63,12 +68,26 @@ async function createTreeCodeFromPath({
parallelSegments.push(...resolveParallelSegments(segmentPath))
}

let metadataCode = ''

for (const [parallelKey, parallelSegment] of parallelSegments) {
if (parallelSegment === PAGE_SEGMENT) {
const matchedPagePath = `${appDirPrefix}${segmentPath}/page`
const resolvedPagePath = await resolve(matchedPagePath)
if (resolvedPagePath) pages.push(resolvedPagePath)

metadataCode += `{
type: 'page',
layer: ${
// There's an extra virtual segment.
segments.length - 1
},
mod: () => import(/* webpackMode: "eager" */ ${JSON.stringify(
resolvedPagePath
)}),
path: ${JSON.stringify(resolvedPagePath)},
},`

// Use '' for segment as it's the page. There can't be a segment called '' so this is the safest way to add it.
props[parallelKey] = `['', {}, {
page: [() => import(/* webpackMode: "eager" */ ${JSON.stringify(
Expand All @@ -78,10 +97,9 @@ async function createTreeCodeFromPath({
}

const parallelSegmentPath = segmentPath + '/' + parallelSegment
const subtree = await createSubtreePropsFromSegmentPath([
...segments,
parallelSegment,
])

const [subtreeCode, subtreeMetadataCode] =
await createSubtreePropsFromSegmentPath([...segments, parallelSegment])

// `page` is not included here as it's added above.
const filePaths = await Promise.all(
Expand All @@ -93,10 +111,29 @@ async function createTreeCodeFromPath({
})
)

const layout = filePaths.find(
([type, path]) => type === 'layout' && !!path
)?.[1]

// Collect metadata for the layout
if (layout) {
metadataCode += `{
type: 'layout',
layer: ${segments.length},
mod: () => import(/* webpackMode: "eager" */ ${JSON.stringify(
layout
)}),
shuding marked this conversation as resolved.
Show resolved Hide resolved
path: ${JSON.stringify(layout)},
},`
}
metadataCode += await resolveFileBasedMetadataForLoader(
segments.length,
(await resolve(`${appDirPrefix}${parallelSegmentPath}/`, true))!
)
metadataCode += subtreeMetadataCode

if (!rootLayout) {
rootLayout = filePaths.find(
([type, path]) => type === 'layout' && !!path
)?.[1]
rootLayout = layout
}

if (!globalError) {
Expand All @@ -107,7 +144,7 @@ async function createTreeCodeFromPath({

props[parallelKey] = `[
'${parallelSegment}',
${subtree},
${subtreeCode},
{
${filePaths
.filter(([, filePath]) => filePath !== undefined)
Expand All @@ -124,15 +161,25 @@ async function createTreeCodeFromPath({
]`
}

return `{
return [
`{
${Object.entries(props)
.map(([key, value]) => `${key}: ${value}`)
.join(',\n')}
}`
}`,
metadataCode,
]
}

const tree = await createSubtreePropsFromSegmentPath([])
return [`const tree = ${tree}.children;`, pages, rootLayout, globalError]
const [tree, metadataCode] = await createSubtreePropsFromSegmentPath([])

return [
`const tree = ${tree}.children;`,
`const metadata = [${metadataCode}];`,
`const pages = ${JSON.stringify(pages)};`,
gnoff marked this conversation as resolved.
Show resolved Hide resolved
rootLayout,
globalError,
]
}

function createAbsolutePath(appDir: string, pathToTurnAbsolute: string) {
Expand Down Expand Up @@ -202,7 +249,11 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{
return Object.entries(matched)
}

const resolver = async (pathname: string) => {
const resolver = async (pathname: string, resolveDir?: boolean) => {
if (resolveDir) {
return createAbsolutePath(appDir, pathname)
}

try {
const resolved = await resolve(this.rootContext, pathname)
this.addDependency(resolved)
Expand All @@ -220,7 +271,7 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{
}
}

const [treeCode, pages, rootLayout, globalError] =
const [treeCode, metadataCode, pageListCode, rootLayout, globalError] =
await createTreeCodeFromPath({
pagePath,
resolve: resolver,
Expand Down Expand Up @@ -253,7 +304,8 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{

const result = `
export ${treeCode}
export const pages = ${JSON.stringify(pages)}
export ${metadataCode}
export ${pageListCode}

export { default as AppRouter } from 'next/dist/client/components/app-router'
export { default as LayoutRouter } from 'next/dist/client/components/layout-router'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ interface IEntry {
? "runtime?: 'nodejs' | 'experimental-edge' | 'edge'"
: ''
}
metadata?: any
}
// =============
Expand Down
41 changes: 41 additions & 0 deletions packages/next/src/lib/metadata/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { ResolvedMetadata } from './types/metadata-interface'
huozhi marked this conversation as resolved.
Show resolved Hide resolved

export const createDefaultMetadata = (): ResolvedMetadata => {
return {
viewport: 'width=device-width, initial-scale=1',

// Other values are all null
metadataBase: null,
title: null,
description: null,
applicationName: null,
authors: null,
generator: null,
keywords: null,
referrer: null,
themeColor: null,
colorScheme: null,
creator: null,
publisher: null,
robots: null,
alternates: {
canonical: null,
languages: {},
},
icons: null,
openGraph: null,
twitter: null,
verification: {},
appleWebApp: null,
formatDetection: null,
itunes: null,
abstract: null,
appLinks: null,
archives: null,
assets: null,
bookmarks: null,
category: null,
classification: null,
other: {},
}
}
47 changes: 47 additions & 0 deletions packages/next/src/lib/metadata/generate/alternate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { ResolvedMetadata } from '../types/metadata-interface'

import React from 'react'

export function elementsFromResolvedAlternates(metadata: ResolvedMetadata) {
return (
<>
{metadata.alternates.canonical ? (
<link rel="canonical" href={metadata.alternates.canonical.toString()} />
) : null}
{Object.entries(metadata.alternates.languages).map(([locale, url]) =>
url ? (
<link
key={locale}
rel="alternate"
hrefLang={locale}
href={url.toString()}
/>
) : null
)}
{metadata.alternates.media
? Object.entries(metadata.alternates.media).map(([media, url]) =>
url ? (
<link
key={media}
rel="alternate"
media={media}
href={url.toString()}
/>
) : null
)
: null}
{metadata.alternates.types
? Object.entries(metadata.alternates.types).map(([type, url]) =>
url ? (
<link
key={type}
rel="alternate"
type={type}
href={url.toString()}
/>
) : null
)
: null}
</>
)
}
58 changes: 58 additions & 0 deletions packages/next/src/lib/metadata/generate/basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { ResolvedMetadata } from '../types/metadata-interface'

import React from 'react'
import { Meta } from './utils'

export function elementsFromResolvedBasic(metadata: ResolvedMetadata) {
return (
<>
{metadata.title !== null ? (
<title>
{typeof metadata.title === 'string'
? metadata.title
: metadata.title.absolute}
huozhi marked this conversation as resolved.
Show resolved Hide resolved
</title>
) : null}
{metadata.metadataBase !== null ? (
<base href={metadata.metadataBase.toString()} />
) : null}
huozhi marked this conversation as resolved.
Show resolved Hide resolved
<Meta name="description" content={metadata.description} />
<Meta name="application-name" content={metadata.applicationName} />
<Meta name="author" content={metadata.authors?.join(',')} />
<Meta name="generator" content={metadata.generator} />
<Meta name="keywords" content={metadata.keywords?.join(',')} />
<Meta name="referrer" content={metadata.referrer} />
<Meta name="theme-color" content={metadata.themeColor} />
<Meta name="color-scheme" content={metadata.colorScheme} />
<Meta name="viewport" content={metadata.viewport} />
<Meta name="creator" content={metadata.creator} />
<Meta name="publisher" content={metadata.publisher} />
<Meta name="robots" content={metadata.robots} />
<Meta name="abstract" content={metadata.abstract} />
{metadata.archives
? metadata.archives.map((archive) => (
<link rel="archives" href={archive} key={archive} />
))
: null}
{metadata.assets
? metadata.assets.map((asset) => (
<link rel="assets" href={asset} key={asset} />
))
: null}
{metadata.bookmarks
? metadata.bookmarks.map((bookmark) => (
<link rel="bookmarks" href={bookmark} key={bookmark} />
))
: null}
<Meta name="category" content={metadata.category} />
<Meta name="classification" content={metadata.classification} />
{Object.entries(metadata.other).map(([name, content]) => (
<Meta
key={name}
name={name}
content={Array.isArray(content) ? content.join(',') : content}
/>
))}
</>
)
}