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 43 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
83 changes: 68 additions & 15 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,21 +37,26 @@ 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<{
treeCode: string
treeMetadataCode: string
}> {
const segmentPath = segments.join('/')

Expand All @@ -65,12 +71,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 @@ -80,9 +100,8 @@ async function createTreeCodeFromPath({
}

const parallelSegmentPath = segmentPath + '/' + parallelSegment
const { treeCode: subtreeCode } = await createSubtreePropsFromSegmentPath(
[...segments, parallelSegment]
)
const { treeCode: subtreeCode, treeMetadataCode: subTreeMetadataCode } =
await createSubtreePropsFromSegmentPath([...segments, parallelSegment])

// `page` is not included here as it's added above.
const filePaths = await Promise.all(
Expand All @@ -101,6 +120,27 @@ async function createTreeCodeFromPath({
rootLayout = layoutPath
}

// Collect metadata for the layout
if (layoutPath) {
metadataCode += `{
type: 'layout',
layer: ${segments.length},
mod: () => import(/* webpackMode: "eager" */ ${JSON.stringify(
layoutPath
)}),
path: ${JSON.stringify(layoutPath)},
},`
}
metadataCode += await resolveFileBasedMetadataForLoader(
segments.length,
(await resolve(`${appDirPrefix}${parallelSegmentPath}/`, true))!
)
metadataCode += subTreeMetadataCode

if (!rootLayout) {
rootLayout = layoutPath
}

if (!globalError) {
globalError = await resolve(
`${appDirPrefix}${parallelSegmentPath}/${GLOBAL_ERROR_FILE_TYPE}`
Expand Down Expand Up @@ -133,13 +173,16 @@ async function createTreeCodeFromPath({
.map(([key, value]) => `${key}: ${value}`)
.join(',\n')}
}`,
treeMetadataCode: metadataCode,
}
}

const { treeCode } = await createSubtreePropsFromSegmentPath([])
const { treeCode, treeMetadataCode } =
await createSubtreePropsFromSegmentPath([])
return {
treeCode: `const tree = ${treeCode}.children;`,
pages,
treeMetadataCode: `const metadata = [${treeMetadataCode}];`,
pages: `const pages = ${JSON.stringify(pages)};`,
rootLayout,
globalError,
}
Expand Down Expand Up @@ -197,7 +240,7 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{
const rest = path.slice(pathname.length + 1).split('/')

let matchedSegment = rest[0]
// It is the actual page, mark it sepcially.
// It is the actual page, mark it specially.
if (rest.length === 1 && matchedSegment === 'page') {
matchedSegment = PAGE_SEGMENT
}
Expand All @@ -212,7 +255,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 @@ -230,12 +277,17 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{
}
}

const { treeCode, pages, rootLayout, globalError } =
await createTreeCodeFromPath({
pagePath,
resolve: resolver,
resolveParallelSegments,
})
const {
treeCode,
treeMetadataCode,
pages: pageListCode,
rootLayout,
globalError,
} = await createTreeCodeFromPath({
pagePath,
resolve: resolver,
resolveParallelSegments,
})

if (!rootLayout) {
const errorMessage = `${chalk.bold(
Expand Down Expand Up @@ -263,7 +315,8 @@ const nextAppLoader: webpack.LoaderDefinitionFunction<{

const result = `
export ${treeCode}
export const pages = ${JSON.stringify(pages)}
export ${treeMetadataCode}
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
10 changes: 0 additions & 10 deletions packages/next/src/client/components/head.tsx

This file was deleted.

41 changes: 41 additions & 0 deletions packages/next/src/lib/metadata/default-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { ResolvedMetadata } from './types/metadata-interface'

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: {},
}
}
51 changes: 51 additions & 0 deletions packages/next/src/lib/metadata/generate/alternate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { ResolvedMetadata } from '../types/metadata-interface'

import React from 'react'

export function ResolvedAlternatesMetadata({
metadata,
}: {
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}
</>
)
}
56 changes: 56 additions & 0 deletions packages/next/src/lib/metadata/generate/basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { ResolvedMetadata } from '../types/metadata-interface'

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

export function ResolvedBasicMetadata({
metadata,
}: {
metadata: ResolvedMetadata
}) {
return (
<>
<meta charSet="utf-8" />
{metadata.title !== null ? (
<title>{metadata.title.absolute}</title>
) : null}
<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}
/>
))}
</>
)
}