Skip to content

Commit 397c1f1

Browse files
authored
feat(next): fully expose Next.js metadata (#11593)
Payload now fully exposes Next.js' metadata options. You can now use the `admin.meta` config to set any properties that Next.js supports and Payload will inject them into its `generateMetadata` function call. The `MetaConfig` provided by Payload now directly extends the `Metadata` type from Next.js. Although `admin.meta` has always been available, it only supported a subset of options, such as `title`, `openGraph`, etc., but was lacking properties like `robots`, etc.
1 parent c8f01e3 commit 397c1f1

41 files changed

Lines changed: 214 additions & 268 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/admin/metadata.mdx

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ desc: Customize the metadata of your pages within the Admin Panel
66
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
77
---
88

9-
Every page within the Admin Panel automatically receives dynamic, auto-generated metadata derived from live document data, the user's current locale, and more, without any additional configuration. This includes the page title, description, og:image and everything in between. Metadata is fully configurable at the root level and cascades down to individual collections, documents, and custom views, allowing for the ability to control metadata on any page with high precision.
9+
Every page within the Admin Panel automatically receives dynamic, auto-generated metadata derived from live document data, the user's current locale, and more. This includes the page title, description, og:image, etc. and requires no additional configuration.
10+
11+
Metadata is fully configurable at the root level and cascades down to individual collections, documents, and custom views. This allows for the ability to control metadata on any page with high precision, while also providing sensible defaults.
12+
13+
All metadata is injected into Next.js' [`generateMetadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) function. This used to generate the `<head>` of pages within the Admin Panel. All metadata options that are available in Next.js are exposed by Payload.
1014

1115
Within the Admin Panel, metadata can be customized at the following levels:
1216

@@ -48,13 +52,10 @@ The following options are available for Root Metadata:
4852

4953
| Key | Type | Description |
5054
| --- | --- | --- |
51-
| **`title`** | `string` | The title of the Admin Panel. |
52-
| **`description`** | `string` | The description of the Admin Panel. |
53-
| **`defaultOGImageType`** | `dynamic` (default), `static`, or `off` | The type of default OG image to use. If set to `dynamic`, Payload will use Next.js image generation to create an image with the title of the page. If set to `static`, Payload will use the `defaultOGImage` URL. If set to `off`, Payload will not generate an OG image. |
54-
| **`icons`** | `IconConfig[]` | An array of icon objects. [More details](#icons) |
55-
| **`keywords`** | `string` | A comma-separated list of keywords to include in the metadata of the Admin Panel. |
56-
| **`openGraph`** | `OpenGraphConfig` | An object containing Open Graph metadata. [More details](#open-graph) |
57-
| **`titleSuffix`** | `string` | A suffix to append to the end of the title of every page. Defaults to "- Payload". |
55+
| `defaultOGImageType` | `dynamic` (default), `static`, or `off` | The type of default OG image to use. If set to `dynamic`, Payload will use Next.js image generation to create an image with the title of the page. If set to `static`, Payload will use the `defaultOGImage` URL. If set to `off`, Payload will not generate an OG image. |
56+
| `icons` | `IconConfig[]` | An array of icon objects. [More details](#icons). |
57+
| `titleSuffix` | `string` | A suffix to append to the end of the title of every page. Defaults to "- Payload". |
58+
| `[keyof Metadata]` | `unknown` | Any other properties that Next.js supports within the `generateMetadata` function. [More details](https://nextjs.org/docs/app/api-reference/functions/generate-metadata). |
5859

5960
<Banner type="success">
6061
**Reminder:**
@@ -93,17 +94,7 @@ To customize icons, use the `icons` key within the `admin.meta` object in your P
9394
}
9495
```
9596

96-
The following options are available for Icons:
97-
98-
| Key | Type | Description |
99-
| --- | --- | --- |
100-
| **`rel`** | `string` | The HTML `rel` attribute of the icon. |
101-
| **`type`** | `string` | The MIME type of the icon. |
102-
| **`color`** | `string` | The color of the icon. |
103-
| **`fetchPriority`** | `string` | The [fetch priority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) of the icon. |
104-
| **`media`** | `string` | The [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries) of the icon. |
105-
| **`sizes`** | `string` | The [sizes](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes) of the icon. |
106-
| **`url`** | `string` | The URL pointing the resource of the icon. |
97+
For a full list of all available Icon options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#icons).
10798

10899
### Open Graph
109100

@@ -135,14 +126,7 @@ To customize Open Graph metadata, use the `openGraph` key within the `admin.meta
135126
}
136127
```
137128

138-
The following options are available for Open Graph Metadata:
139-
140-
| Key | Type | Description |
141-
| --- | --- | --- |
142-
| **`description`** | `string` | The description of the Admin Panel. |
143-
| **`images`** | `OGImageConfig` or `OGImageConfig[]` | An array of image objects. |
144-
| **`siteName`** | `string` | The name of the site. |
145-
| **`title`** | `string` | The title of the Admin Panel. |
129+
For a full list of all available Open Graph options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#opengraph).
146130

147131
## Collection Metadata
148132

packages/next/src/exports/views.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { NotFoundPage } from '../views/NotFound/index.js'
2-
export { generatePageMetadata, type GenerateViewMetadata, RootPage } from '../views/Root/index.js'
2+
export { type GenerateViewMetadata, RootPage } from '../views/Root/index.js'
3+
export { generatePageMetadata } from '../views/Root/metadata.js'

packages/next/src/utilities/meta.ts

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,53 @@
11
import type { Metadata } from 'next'
2-
import type { IconConfig, MetaConfig } from 'payload'
2+
import type { Icon } from 'next/dist/lib/metadata/types/metadata-types.js'
3+
import type { MetaConfig } from 'payload'
34

45
import { payloadFaviconDark, payloadFaviconLight, staticOGImage } from '@payloadcms/ui/assets'
56
import * as qs from 'qs-esm'
67

7-
const defaultOpenGraph = {
8+
const defaultOpenGraph: Metadata['openGraph'] = {
89
description:
910
'Payload is a headless CMS and application framework built with TypeScript, Node.js, and React.',
1011
siteName: 'Payload App',
1112
title: 'Payload App',
1213
}
1314

14-
export const meta = async (args: { serverURL: string } & MetaConfig): Promise<any> => {
15-
const {
16-
defaultOGImageType,
17-
description,
18-
icons: customIcons,
19-
keywords,
20-
openGraph: openGraphFromProps,
21-
serverURL,
22-
title,
23-
titleSuffix,
24-
} = args
15+
export const generateMetadata = async (
16+
args: { serverURL: string } & MetaConfig,
17+
): Promise<Metadata> => {
18+
const { defaultOGImageType, serverURL, titleSuffix, ...rest } = args
2519

26-
const payloadIcons: IconConfig[] = [
27-
{
28-
type: 'image/png',
29-
rel: 'icon',
30-
sizes: '32x32',
31-
url: typeof payloadFaviconDark === 'object' ? payloadFaviconDark?.src : payloadFaviconDark,
32-
},
33-
{
34-
type: 'image/png',
35-
media: '(prefers-color-scheme: dark)',
36-
rel: 'icon',
37-
sizes: '32x32',
38-
url: typeof payloadFaviconLight === 'object' ? payloadFaviconLight?.src : payloadFaviconLight,
39-
},
40-
]
20+
/**
21+
* @todo find a way to remove the type assertion here.
22+
* It is a result of needing to `DeepCopy` the `MetaConfig` type from Payload.
23+
* This is required for the `DeepRequired` from `Config` to `SanitizedConfig`.
24+
*/
25+
const incomingMetadata = rest as Metadata
4126

42-
let icons = payloadIcons
27+
const icons: Metadata['icons'] =
28+
incomingMetadata.icons ||
29+
([
30+
{
31+
type: 'image/png',
32+
rel: 'icon',
33+
sizes: '32x32',
34+
url: typeof payloadFaviconDark === 'object' ? payloadFaviconDark?.src : payloadFaviconDark,
35+
},
36+
{
37+
type: 'image/png',
38+
media: '(prefers-color-scheme: dark)',
39+
rel: 'icon',
40+
sizes: '32x32',
41+
url:
42+
typeof payloadFaviconLight === 'object' ? payloadFaviconLight?.src : payloadFaviconLight,
43+
},
44+
] satisfies Array<Icon>)
4345

44-
if (customIcons && typeof customIcons === 'object' && Array.isArray(customIcons)) {
45-
icons = customIcons
46-
}
47-
48-
const metaTitle = [title, titleSuffix].filter(Boolean).join(' ')
46+
const metaTitle: Metadata['title'] = [incomingMetadata.title, titleSuffix]
47+
.filter(Boolean)
48+
.join(' ')
4949

50-
const ogTitle = `${typeof openGraphFromProps?.title === 'string' ? openGraphFromProps.title : title} ${titleSuffix}`
50+
const ogTitle = `${typeof incomingMetadata.openGraph?.title === 'string' ? incomingMetadata.openGraph.title : incomingMetadata.title} ${titleSuffix}`
5151

5252
const mergedOpenGraph: Metadata['openGraph'] = {
5353
...(defaultOpenGraph || {}),
@@ -59,7 +59,8 @@ export const meta = async (args: { serverURL: string } & MetaConfig): Promise<an
5959
height: 630,
6060
url: `/api/og${qs.stringify(
6161
{
62-
description: openGraphFromProps?.description || defaultOpenGraph.description,
62+
description:
63+
incomingMetadata.openGraph?.description || defaultOpenGraph.description,
6364
title: ogTitle,
6465
},
6566
{
@@ -84,13 +85,12 @@ export const meta = async (args: { serverURL: string } & MetaConfig): Promise<an
8485
}
8586
: {}),
8687
title: ogTitle,
87-
...(openGraphFromProps || {}),
88+
...(incomingMetadata.openGraph || {}),
8889
}
8990

9091
return Promise.resolve({
91-
description,
92+
...incomingMetadata,
9293
icons,
93-
keywords,
9494
metadataBase: new URL(
9595
serverURL ||
9696
process.env.PAYLOAD_PUBLIC_SERVER_URL ||
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import type { MetaConfig } from 'payload'
2+
13
import { getTranslation } from '@payloadcms/translations'
24

35
import type { GenerateEditViewMetadata } from '../Document/getMetaBySegment.js'
46

5-
import { meta } from '../../utilities/meta.js'
7+
import { generateMetadata } from '../../utilities/meta.js'
68

7-
export const generateMetadata: GenerateEditViewMetadata = async ({
9+
/**
10+
* @todo Remove the `MetaConfig` type assertions. They are currently required because of how the `Metadata` type from `next` consumes the `URL` type.
11+
*/
12+
export const generateAPIViewMetadata: GenerateEditViewMetadata = async ({
813
collectionConfig,
914
config,
1015
globalConfig,
@@ -17,24 +22,24 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
1722
: ''
1823

1924
return Promise.resolve(
20-
meta({
25+
generateMetadata({
2126
...(config.admin.meta || {}),
2227
description: `API - ${entityLabel}`,
2328
keywords: 'API',
2429
serverURL: config.serverURL,
2530
title: `API - ${entityLabel}`,
26-
...(collectionConfig
31+
...((collectionConfig
2732
? {
2833
...(collectionConfig?.admin.meta || {}),
2934
...(collectionConfig?.admin?.components?.views?.edit?.api?.meta || {}),
3035
}
31-
: {}),
32-
...(globalConfig
36+
: {}) as MetaConfig),
37+
...((globalConfig
3338
? {
3439
...(globalConfig?.admin.meta || {}),
3540
...(globalConfig?.admin?.components?.views?.edit?.api?.meta || {}),
3641
}
37-
: {}),
42+
: {}) as MetaConfig),
3843
}),
3944
)
4045
}

packages/next/src/views/Account/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import { EditView } from '../Edit/index.js'
1616
import { AccountClient } from './index.client.js'
1717
import { Settings } from './Settings/index.js'
1818

19-
export { generateAccountMetadata } from './meta.js'
20-
2119
export async function Account({ initPageResult, params, searchParams }: AdminViewServerProps) {
2220
const {
2321
languageOptions,

packages/next/src/views/Account/meta.ts renamed to packages/next/src/views/Account/metadata.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { GenerateViewMetadata } from '../Root/index.js'
22

3-
import { meta } from '../../utilities/meta.js'
3+
import { generateMetadata } from '../../utilities/meta.js'
44

5-
export const generateAccountMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
6-
meta({
5+
export const generateAccountViewMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
6+
generateMetadata({
77
description: `${t('authentication:accountOfCurrentUser')}`,
88
keywords: `${t('authentication:account')}`,
99
serverURL: config.serverURL,

packages/next/src/views/CreateFirstUser/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
99
import { CreateFirstUserClient } from './index.client.js'
1010
import './index.scss'
1111

12-
export { generateCreateFirstUserMetadata } from './meta.js'
13-
1412
export async function CreateFirstUserView({ initPageResult }: AdminViewServerProps) {
1513
const {
1614
locale,

packages/next/src/views/CreateFirstUser/meta.ts renamed to packages/next/src/views/CreateFirstUser/metadata.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { GenerateViewMetadata } from '../Root/index.js'
22

3-
import { meta } from '../../utilities/meta.js'
3+
import { generateMetadata } from '../../utilities/meta.js'
44

5-
export const generateCreateFirstUserMetadata: GenerateViewMetadata = async ({
5+
export const generateCreateFirstUserViewMetadata: GenerateViewMetadata = async ({
66
config,
77
i18n: { t },
88
}) =>
9-
meta({
9+
generateMetadata({
1010
description: t('authentication:createFirstUser'),
1111
keywords: t('general:create'),
1212
serverURL: config.serverURL,

packages/next/src/views/Dashboard/index.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import type { DashboardViewClientProps, DashboardViewServerPropsOnly } from './D
1010

1111
import { DefaultDashboard } from './Default/index.js'
1212

13-
export { generateDashboardMetadata } from './meta.js'
14-
1513
export async function Dashboard({ initPageResult, params, searchParams }: AdminViewServerProps) {
1614
const {
1715
locale,

packages/next/src/views/Dashboard/meta.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)