feat!: replaces admin.meta.ogImage with admin.meta.openGraph.images (#6227)
This commit is contained in:
@@ -33,7 +33,7 @@ All options for the Admin panel are defined in your base Payload config file.
|
||||
| `bundler` | The bundler that you would like to use to bundle the admin panel. Officially supported bundlers: [Webpack](/docs/admin/webpack) and [Vite](/docs/admin/vite). |
|
||||
| `user` | The `slug` of a Collection that you want be used to log in to the Admin dashboard. [More](/docs/admin/overview#the-admin-user-collection) |
|
||||
| `buildPath` | Specify an absolute path for where to store the built Admin panel bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
|
||||
| `meta` | Base meta data to use for the Admin panel. Included properties are `titleSuffix`, `ogImage`, and `favicon`. |
|
||||
| `meta` | Base meta data to use for the Admin panel. Included properties are `titleSuffix`, `icons`, and `openGraph`. Can be overridden on a per collection or per global basis. |
|
||||
| `disable` | If set to `true`, the entire Admin panel will be disabled. |
|
||||
| `indexHTML` | Optionally replace the entirety of the `index.html` file used by the Admin panel. Reference the [base index.html file](https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/index.html) to ensure your replacement has the appropriate HTML elements. |
|
||||
| `css` | Absolute path to a stylesheet that you can use to override / customize the Admin panel styling. [More](/docs/admin/customizing-css). |
|
||||
|
||||
@@ -80,6 +80,7 @@ property on a collection's config.
|
||||
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this collection. |
|
||||
| `enableRichTextLink` | The [Rich Text](/docs/fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
| `enableRichTextRelationship` | The [Rich Text](/docs/fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
| `meta` | Metadata overrides to apply to the [Admin panel](../admin/overview). Included properties are `description` and `openGraph`. |
|
||||
| `preview` | Function to generate preview URLS within the Admin panel that can point to your app. [More](#preview). |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More](/docs/live-preview/overview). |
|
||||
| `components` | Swap in your own React components to be used within this collection. [More](/docs/admin/components#collections) |
|
||||
|
||||
@@ -74,6 +74,7 @@ You can customize the way that the Admin panel behaves on a Global-by-Global bas
|
||||
| `preview` | Function to generate a preview URL within the Admin panel for this global that can point to your app. [More](#preview). |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More](/docs/live-preview/overview). |
|
||||
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this collection. |
|
||||
| `meta` | Metadata overrides to apply to the [Admin panel](../admin/overview). Included properties are `description` and `openGraph`. |
|
||||
|
||||
### Preview
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ import { findVersions as findVersionsGlobal } from './globals/findVersions.js'
|
||||
import { preview as previewGlobal } from './globals/preview.js'
|
||||
import { restoreVersion as restoreVersionGlobal } from './globals/restoreVersion.js'
|
||||
import { update as updateGlobal } from './globals/update.js'
|
||||
import { generateOGImage } from './og/index.js'
|
||||
import { routeError } from './routeError.js'
|
||||
|
||||
const endpoints = {
|
||||
@@ -113,6 +114,7 @@ const endpoints = {
|
||||
root: {
|
||||
GET: {
|
||||
access,
|
||||
og: generateOGImage,
|
||||
},
|
||||
POST: {
|
||||
'form-state': buildFormState,
|
||||
|
||||
89
packages/next/src/routes/rest/og/image.tsx
Normal file
89
packages/next/src/routes/rest/og/image.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react'
|
||||
|
||||
export const OGImage: React.FC<{
|
||||
Icon: React.ComponentType<any>
|
||||
description?: string
|
||||
fontFamily?: string
|
||||
leader?: string
|
||||
title?: string
|
||||
}> = ({ Icon, description, fontFamily = 'Arial, sans-serif', leader, title }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#000',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
fontFamily,
|
||||
height: '100%',
|
||||
justifyContent: 'space-between',
|
||||
padding: '100px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
fontSize: 50,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
{leader && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 30,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
{leader}
|
||||
</div>
|
||||
)}
|
||||
<p
|
||||
style={{
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: 2,
|
||||
display: '-webkit-box',
|
||||
fontSize: 90,
|
||||
lineHeight: 1,
|
||||
marginBottom: 0,
|
||||
marginTop: 0,
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
{description && (
|
||||
<p
|
||||
style={{
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: 2,
|
||||
display: '-webkit-box',
|
||||
flexGrow: 1,
|
||||
fontSize: 30,
|
||||
lineHeight: 1,
|
||||
marginBottom: 0,
|
||||
marginTop: 40,
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
alignItems: 'flex-end',
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
height: '38px',
|
||||
justifyContent: 'center',
|
||||
width: '38px',
|
||||
}}
|
||||
>
|
||||
<Icon fill="white" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
81
packages/next/src/routes/rest/og/index.tsx
Normal file
81
packages/next/src/routes/rest/og/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { PayloadRequestWithData } from 'payload/types'
|
||||
|
||||
import { PayloadIcon } from '@payloadcms/ui/graphics/Icon'
|
||||
import fs from 'fs/promises'
|
||||
import { ImageResponse } from 'next/og.js'
|
||||
import { NextResponse } from 'next/server.js'
|
||||
import path from 'path'
|
||||
import React from 'react'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { OGImage } from './image.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export const contentType = 'image/png'
|
||||
|
||||
export const generateOGImage = async ({ req }: { req: PayloadRequestWithData }) => {
|
||||
const config = req.payload.config
|
||||
|
||||
if (config.admin.meta.defaultOGImageType === 'off') {
|
||||
return NextResponse.json({ error: `Open Graph images are disabled` }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
|
||||
const hasTitle = searchParams.has('title')
|
||||
const title = hasTitle ? searchParams.get('title')?.slice(0, 100) : ''
|
||||
const hasLeader = searchParams.has('leader')
|
||||
const leader = hasLeader ? searchParams.get('leader')?.slice(0, 100).replace('-', ' ') : ''
|
||||
const description = searchParams.has('description') ? searchParams.get('description') : ''
|
||||
const Icon = config.admin?.components?.graphics?.Icon || PayloadIcon
|
||||
|
||||
let fontData
|
||||
|
||||
try {
|
||||
// TODO: replace with `.woff2` file when supported
|
||||
// See https://github.com/vercel/next.js/issues/63935
|
||||
// Or better yet, use a CDN like Google Fonts if ever supported
|
||||
fontData = fs.readFile(path.join(dirname, 'roboto-regular.woff'))
|
||||
} catch (e) {
|
||||
console.error(`Error reading font file or not readable: ${e.message}`) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
const fontFamily = 'Roboto, sans-serif'
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<OGImage
|
||||
Icon={Icon}
|
||||
description={description}
|
||||
fontFamily={fontFamily}
|
||||
leader={leader}
|
||||
title={title}
|
||||
/>
|
||||
),
|
||||
{
|
||||
...(fontData
|
||||
? {
|
||||
fonts: [
|
||||
{
|
||||
name: 'Roboto',
|
||||
data: await fontData,
|
||||
style: 'normal',
|
||||
weight: 400,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
height: 630,
|
||||
width: 1200,
|
||||
},
|
||||
)
|
||||
} catch (e: any) {
|
||||
console.error(`${e.message}`) // eslint-disable-line no-console
|
||||
return NextResponse.json({ error: `Internal Server Error: ${e.message}` }, { status: 500 })
|
||||
}
|
||||
}
|
||||
BIN
packages/next/src/routes/rest/og/roboto-regular.woff
Normal file
BIN
packages/next/src/routes/rest/og/roboto-regular.woff
Normal file
Binary file not shown.
@@ -1,24 +1,29 @@
|
||||
import type { Metadata } from 'next'
|
||||
import type { Icon } from 'next/dist/lib/metadata/types/metadata-types.js'
|
||||
import type { SanitizedConfig } from 'payload/types'
|
||||
import type { MetaConfig } from 'payload/config'
|
||||
|
||||
import { payloadFaviconDark, payloadFaviconLight, payloadOgImage } from '@payloadcms/ui/assets'
|
||||
import { staticOGImage } from '@payloadcms/ui/assets'
|
||||
import { payloadFaviconDark, payloadFaviconLight } from '@payloadcms/ui/assets'
|
||||
import QueryString from 'qs'
|
||||
|
||||
export const meta = async (args: {
|
||||
config: SanitizedConfig
|
||||
description?: string
|
||||
keywords?: string
|
||||
title: string
|
||||
}): Promise<Metadata> => {
|
||||
const { config, description = '', keywords = 'CMS, Admin, Dashboard', title } = args
|
||||
const defaultOpenGraph = {
|
||||
description:
|
||||
'Payload is a headless CMS and application framework built with TypeScript, Node.js, and React.',
|
||||
siteName: 'Payload App',
|
||||
title: 'Payload App',
|
||||
}
|
||||
|
||||
const titleSuffix = config.admin.meta?.titleSuffix ?? '- Payload'
|
||||
|
||||
const ogImage = config.admin?.meta?.ogImage ?? payloadOgImage?.src
|
||||
|
||||
const customIcons = config.admin.meta.icons as Metadata['icons']
|
||||
|
||||
let icons = customIcons ?? []
|
||||
export const meta = async (args: MetaConfig & { serverURL: string }): Promise<any> => {
|
||||
const {
|
||||
defaultOGImageType,
|
||||
description,
|
||||
icons: customIcons,
|
||||
keywords,
|
||||
openGraph: openGraphFromProps,
|
||||
serverURL,
|
||||
title,
|
||||
titleSuffix,
|
||||
} = args
|
||||
|
||||
const payloadIcons: Icon[] = [
|
||||
{
|
||||
@@ -36,8 +41,52 @@ export const meta = async (args: {
|
||||
},
|
||||
]
|
||||
|
||||
let icons = customIcons ?? payloadIcons // TODO: fix this type assertion
|
||||
|
||||
if (customIcons && typeof customIcons === 'object' && Array.isArray(customIcons)) {
|
||||
icons = payloadIcons.concat(customIcons)
|
||||
icons = payloadIcons.concat(customIcons) // TODO: fix this type assertion
|
||||
}
|
||||
|
||||
const metaTitle = `${title} ${titleSuffix}`
|
||||
|
||||
const ogTitle = `${typeof openGraphFromProps?.title === 'string' ? openGraphFromProps.title : title} ${titleSuffix}`
|
||||
|
||||
const mergedOpenGraph: Metadata['openGraph'] = {
|
||||
...(defaultOpenGraph || {}),
|
||||
...(defaultOGImageType === 'dynamic'
|
||||
? {
|
||||
images: [
|
||||
{
|
||||
alt: ogTitle,
|
||||
height: 630,
|
||||
url: `/api/og${QueryString.stringify(
|
||||
{
|
||||
description: openGraphFromProps?.description || defaultOpenGraph.description,
|
||||
title: ogTitle,
|
||||
},
|
||||
{
|
||||
addQueryPrefix: true,
|
||||
},
|
||||
)}`,
|
||||
width: 1200,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
...(defaultOGImageType === 'static'
|
||||
? {
|
||||
images: [
|
||||
{
|
||||
alt: ogTitle,
|
||||
height: 480,
|
||||
url: staticOGImage.src,
|
||||
width: 640,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
title: ogTitle,
|
||||
...(openGraphFromProps || {}),
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
@@ -45,23 +94,11 @@ export const meta = async (args: {
|
||||
icons,
|
||||
keywords,
|
||||
metadataBase: new URL(
|
||||
config?.serverURL ||
|
||||
serverURL ||
|
||||
process.env.PAYLOAD_PUBLIC_SERVER_URL ||
|
||||
`http://localhost:${process.env.PORT || 3000}`,
|
||||
),
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
description,
|
||||
images: [
|
||||
{
|
||||
alt: `${title} ${titleSuffix}`,
|
||||
height: 630,
|
||||
url: ogImage,
|
||||
width: 1200,
|
||||
},
|
||||
],
|
||||
title: `${title} ${titleSuffix}`,
|
||||
},
|
||||
title: `${title} ${titleSuffix}`,
|
||||
openGraph: mergedOpenGraph,
|
||||
title: metaTitle,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,33 @@
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
import type { GenerateEditViewMetadata } from '../Document/getMetaBySegment.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async ({ config }) =>
|
||||
meta({
|
||||
export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
collectionConfig,
|
||||
config,
|
||||
description: 'API',
|
||||
globalConfig,
|
||||
i18n,
|
||||
}) => {
|
||||
const entityLabel = collectionConfig
|
||||
? getTranslation(collectionConfig.labels.singular, i18n)
|
||||
: globalConfig
|
||||
? getTranslation(globalConfig.label, i18n)
|
||||
: ''
|
||||
|
||||
const metaTitle = `API - ${entityLabel}`
|
||||
const description = `API - ${entityLabel}`
|
||||
|
||||
return Promise.resolve(
|
||||
meta({
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords: 'API',
|
||||
title: 'API',
|
||||
})
|
||||
serverURL: config.serverURL,
|
||||
title: metaTitle,
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateAccountMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
config,
|
||||
description: `${t('authentication:accountOfCurrentUser')}`,
|
||||
keywords: `${t('authentication:account')}`,
|
||||
serverURL: config.serverURL,
|
||||
title: t('authentication:account'),
|
||||
...(config.admin.meta || {}),
|
||||
})
|
||||
|
||||
@@ -7,8 +7,9 @@ export const generateCreateFirstUserMetadata: GenerateViewMetadata = async ({
|
||||
i18n: { t },
|
||||
}) =>
|
||||
meta({
|
||||
config,
|
||||
description: t('authentication:createFirstUser'),
|
||||
keywords: t('general:create'),
|
||||
serverURL: config.serverURL,
|
||||
title: t('authentication:createFirstUser'),
|
||||
...(config.admin.meta || {}),
|
||||
})
|
||||
|
||||
@@ -4,8 +4,13 @@ import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateDashboardMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
config,
|
||||
description: `${t('general:dashboard')} Payload`,
|
||||
keywords: `${t('general:dashboard')}, Payload`,
|
||||
serverURL: config.serverURL,
|
||||
title: t('general:dashboard'),
|
||||
...(config.admin.meta || {}),
|
||||
openGraph: {
|
||||
title: t('general:dashboard'),
|
||||
...(config.admin.meta?.openGraph || {}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -15,7 +15,6 @@ export type GenerateEditViewMetadata = (
|
||||
args: Parameters<GenerateViewMetadata>[0] & {
|
||||
collectionConfig?: SanitizedCollectionConfig | null
|
||||
globalConfig?: SanitizedGlobalConfig | null
|
||||
isEditing: boolean
|
||||
},
|
||||
) => Promise<Metadata>
|
||||
|
||||
@@ -33,7 +32,8 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
|
||||
const isCollection = segmentOne === 'collections'
|
||||
const isGlobal = segmentOne === 'globals'
|
||||
|
||||
const isEditing = Boolean(isCollection && segments?.length > 2 && segments[2] !== 'create')
|
||||
const isEditing =
|
||||
isGlobal || Boolean(isCollection && segments?.length > 2 && segments[2] !== 'create')
|
||||
|
||||
if (isCollection) {
|
||||
// `/:id`
|
||||
|
||||
@@ -15,34 +15,40 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
}): Promise<Metadata> => {
|
||||
const { t } = i18n
|
||||
|
||||
let description: string = ''
|
||||
let title: string = ''
|
||||
let keywords: string = ''
|
||||
const entityLabel = collectionConfig
|
||||
? getTranslation(collectionConfig.labels.singular, i18n)
|
||||
: globalConfig
|
||||
? getTranslation(globalConfig.label, i18n)
|
||||
: ''
|
||||
|
||||
if (collectionConfig) {
|
||||
description = `${isEditing ? t('general:editing') : t('general:creating')} - ${getTranslation(
|
||||
collectionConfig.labels.singular,
|
||||
i18n,
|
||||
)}`
|
||||
const metaTitle = `${isEditing ? t('general:editing') : t('general:creating')} - ${entityLabel}`
|
||||
|
||||
title = `${isEditing ? t('general:editing') : t('general:creating')} - ${getTranslation(
|
||||
collectionConfig.labels.singular,
|
||||
i18n,
|
||||
)}`
|
||||
const ogTitle = `${isEditing ? t('general:edit') : t('general:edit')} - ${entityLabel}`
|
||||
|
||||
keywords = `${getTranslation(collectionConfig.labels.singular, i18n)}, Payload, CMS`
|
||||
}
|
||||
const description = `${isEditing ? t('general:editing') : t('general:creating')} - ${entityLabel}`
|
||||
|
||||
if (globalConfig) {
|
||||
description = getTranslation(globalConfig.label, i18n)
|
||||
keywords = `${getTranslation(globalConfig.label, i18n)}, Payload, CMS`
|
||||
title = getTranslation(globalConfig.label, i18n)
|
||||
}
|
||||
const keywords = `${entityLabel}, Payload, CMS`
|
||||
|
||||
const baseOGOverrides = config.admin.meta.openGraph || {}
|
||||
|
||||
const entityOGOverrides = collectionConfig
|
||||
? collectionConfig.admin?.meta?.openGraph
|
||||
: globalConfig
|
||||
? globalConfig.admin?.meta?.openGraph
|
||||
: {}
|
||||
|
||||
return meta({
|
||||
config,
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords,
|
||||
title,
|
||||
openGraph: {
|
||||
title: ogTitle,
|
||||
...baseOGOverrides,
|
||||
...entityOGOverrides,
|
||||
},
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
serverURL: config.serverURL,
|
||||
title: metaTitle,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ export const generateForgotPasswordMetadata: GenerateViewMetadata = async ({
|
||||
i18n: { t },
|
||||
}) =>
|
||||
meta({
|
||||
config,
|
||||
description: t('authentication:forgotPassword'),
|
||||
keywords: t('authentication:forgotPassword'),
|
||||
title: t('authentication:forgotPassword'),
|
||||
...(config.admin.meta || {}),
|
||||
serverURL: config.serverURL,
|
||||
})
|
||||
|
||||
@@ -23,9 +23,11 @@ export const generateListMetadata = async (
|
||||
}
|
||||
|
||||
return meta({
|
||||
config,
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords,
|
||||
serverURL: config.serverURL,
|
||||
title,
|
||||
...(collectionConfig.admin.meta || {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
import type { GenerateEditViewMetadata } from '../Document/getMetaBySegment.js'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
import { generateMetadata as generateDocumentMetadata } from '../Edit/meta.js'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
collectionConfig,
|
||||
@@ -12,37 +10,11 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
globalConfig,
|
||||
i18n,
|
||||
isEditing,
|
||||
}): Promise<Metadata> => {
|
||||
const { t } = i18n
|
||||
|
||||
let description: string = ''
|
||||
let title: string = ''
|
||||
let keywords: string = ''
|
||||
|
||||
if (collectionConfig) {
|
||||
description = `${isEditing ? t('general:editing') : t('general:creating')} - ${getTranslation(
|
||||
collectionConfig.labels.singular,
|
||||
i18n,
|
||||
)}`
|
||||
|
||||
title = `${isEditing ? t('general:editing') : t('general:creating')} - ${getTranslation(
|
||||
collectionConfig.labels.singular,
|
||||
i18n,
|
||||
)}`
|
||||
|
||||
keywords = `${getTranslation(collectionConfig.labels.singular, i18n)}, Payload, CMS`
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
description = getTranslation(globalConfig.label, i18n)
|
||||
keywords = `${getTranslation(globalConfig.label, i18n)}, Payload, CMS`
|
||||
title = getTranslation(globalConfig.label, i18n)
|
||||
}
|
||||
|
||||
return meta({
|
||||
}): Promise<Metadata> =>
|
||||
generateDocumentMetadata({
|
||||
collectionConfig,
|
||||
config,
|
||||
description,
|
||||
keywords,
|
||||
title,
|
||||
globalConfig,
|
||||
i18n,
|
||||
isEditing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateLoginMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
config,
|
||||
description: `${t('authentication:login')}`,
|
||||
keywords: `${t('authentication:login')}`,
|
||||
serverURL: config.serverURL,
|
||||
title: t('authentication:login'),
|
||||
...(config.admin.meta || {}),
|
||||
})
|
||||
|
||||
@@ -4,8 +4,8 @@ import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateLogoutMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
config,
|
||||
description: `${t('authentication:logoutUser')}`,
|
||||
keywords: `${t('authentication:logout')}`,
|
||||
serverURL: config.serverURL,
|
||||
title: t('authentication:logout'),
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SanitizedConfig } from 'payload/types'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateNotFoundMeta = ({
|
||||
export const generateNotFoundMeta = async ({
|
||||
config,
|
||||
i18n,
|
||||
}: {
|
||||
@@ -12,8 +12,8 @@ export const generateNotFoundMeta = ({
|
||||
i18n: I18n
|
||||
}): Promise<Metadata> =>
|
||||
meta({
|
||||
config,
|
||||
description: i18n.t('general:pageNotFound'),
|
||||
keywords: `404 ${i18n.t('general:notFound')}`,
|
||||
serverURL: config.serverURL,
|
||||
title: i18n.t('general:notFound'),
|
||||
})
|
||||
|
||||
@@ -9,8 +9,9 @@ export const generateResetPasswordMetadata: GenerateViewMetadata = async ({
|
||||
i18n: { t },
|
||||
}): Promise<Metadata> =>
|
||||
meta({
|
||||
config,
|
||||
description: t('authentication:resetPassword'),
|
||||
keywords: t('authentication:resetPassword'),
|
||||
serverURL: config.serverURL,
|
||||
title: t('authentication:resetPassword'),
|
||||
...(config.admin.meta || {}),
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ export { generatePageMetadata } from './meta.js'
|
||||
export type GenerateViewMetadata = (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18n
|
||||
isEditing?: boolean
|
||||
params?: { [key: string]: string | string[] }
|
||||
}) => Promise<Metadata>
|
||||
|
||||
|
||||
@@ -36,12 +36,6 @@ type Args = {
|
||||
export const generatePageMetadata = async ({ config: configPromise, params }: Args) => {
|
||||
const config = await configPromise
|
||||
|
||||
let route = config.routes.admin
|
||||
|
||||
if (Array.isArray(params.segments)) {
|
||||
route = route + '/' + params.segments.join('/')
|
||||
}
|
||||
|
||||
const segments = Array.isArray(params.segments) ? params.segments : []
|
||||
|
||||
const [segmentOne, segmentTwo] = segments
|
||||
@@ -101,7 +95,6 @@ export const generatePageMetadata = async ({ config: configPromise, params }: Ar
|
||||
config,
|
||||
globalConfig,
|
||||
i18n,
|
||||
isEditing: false,
|
||||
params,
|
||||
})
|
||||
}
|
||||
@@ -118,16 +111,19 @@ export const generatePageMetadata = async ({ config: configPromise, params }: Ar
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:version
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
const isEditing = ['api', 'create', 'preview', 'versions'].includes(segmentTwo)
|
||||
meta = await generateDocumentMetadata({ collectionConfig, config, i18n, isEditing, params })
|
||||
meta = await generateDocumentMetadata({ collectionConfig, config, i18n, params })
|
||||
} else if (isGlobal) {
|
||||
// Custom Views
|
||||
// --> /globals/:globalSlug/versions
|
||||
// --> /globals/:globalSlug/versions/:version
|
||||
// --> /globals/:globalSlug/preview
|
||||
// --> /globals/:globalSlug/api
|
||||
const isEditing = ['api', 'create', 'preview', 'versions'].includes(segmentTwo)
|
||||
meta = await generateDocumentMetadata({ config, globalConfig, i18n, isEditing, params })
|
||||
meta = await generateDocumentMetadata({
|
||||
config,
|
||||
globalConfig,
|
||||
i18n,
|
||||
params,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateUnauthorizedMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
config,
|
||||
description: t('error:unauthorized'),
|
||||
keywords: t('error:unauthorized'),
|
||||
serverURL: config.serverURL,
|
||||
title: t('error:unauthorized'),
|
||||
...(config.admin.meta || {}),
|
||||
})
|
||||
|
||||
@@ -4,8 +4,9 @@ import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateVerifyMetadata: GenerateViewMetadata = async ({ config, i18n: { t } }) =>
|
||||
meta({
|
||||
config,
|
||||
description: t('authentication:verifyUser'),
|
||||
keywords: t('authentication:verify'),
|
||||
serverURL: config.serverURL,
|
||||
title: t('authentication:verify'),
|
||||
...(config.admin.meta || {}),
|
||||
})
|
||||
|
||||
@@ -40,9 +40,12 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
}
|
||||
|
||||
return meta({
|
||||
config,
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords,
|
||||
serverURL: config.serverURL,
|
||||
title,
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
}): Promise<Metadata> => {
|
||||
const { t } = i18n
|
||||
|
||||
const entityLabel = collectionConfig
|
||||
? getTranslation(collectionConfig.labels.singular, i18n)
|
||||
: globalConfig
|
||||
? getTranslation(globalConfig.label, i18n)
|
||||
: ''
|
||||
|
||||
let title: string = ''
|
||||
let description: string = ''
|
||||
const keywords: string = ''
|
||||
@@ -23,7 +29,7 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
if (collectionConfig) {
|
||||
const useAsTitle = collectionConfig?.admin?.useAsTitle || 'id'
|
||||
const titleFromData = data?.[useAsTitle]
|
||||
title = `${t('version:versions')}${titleFromData ? ` - ${titleFromData}` : ''} - ${getTranslation(collectionConfig.labels.plural, i18n)}`
|
||||
title = `${t('version:versions')}${titleFromData ? ` - ${titleFromData}` : ''} - ${entityLabel}`
|
||||
description = t('version:viewingVersions', {
|
||||
documentTitle: data?.[useAsTitle],
|
||||
entitySlug: collectionConfig.slug,
|
||||
@@ -31,14 +37,17 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
title = `${t('version:versions')} - ${getTranslation(globalConfig.label, i18n)}`
|
||||
title = `${t('version:versions')} - ${entityLabel}`
|
||||
description = t('version:viewingVersionsGlobal', { entitySlug: globalConfig.slug })
|
||||
}
|
||||
|
||||
return meta({
|
||||
config,
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords,
|
||||
serverURL: config.serverURL,
|
||||
title,
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -147,7 +147,6 @@
|
||||
"get-port": "5.1.1",
|
||||
"graphql-http": "^1.22.0",
|
||||
"mini-css-extract-plugin": "1.6.2",
|
||||
"next": "^14.3.0-canary.7",
|
||||
"nodemon": "3.0.3",
|
||||
"object.assign": "4.1.4",
|
||||
"object.entries": "1.1.6",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
customViewSchema,
|
||||
livePreviewSchema,
|
||||
} from '../../config/shared/componentSchema.js'
|
||||
import { openGraphSchema } from '../../config/shared/openGraphSchema.js'
|
||||
|
||||
const strategyBaseSchema = joi.object().keys({
|
||||
logout: joi.boolean(),
|
||||
@@ -70,6 +71,10 @@ const collectionSchema = joi.object().keys({
|
||||
hideAPIURL: joi.bool(),
|
||||
listSearchableFields: joi.array().items(joi.string()),
|
||||
livePreview: joi.object(livePreviewSchema),
|
||||
meta: joi.object({
|
||||
description: joi.string(),
|
||||
openGraph: openGraphSchema,
|
||||
}),
|
||||
pagination: joi.object({
|
||||
defaultLimit: joi.number(),
|
||||
limits: joi.array().items(joi.number()),
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
GeneratePreviewURL,
|
||||
LabelFunction,
|
||||
LivePreviewConfig,
|
||||
OpenGraphConfig,
|
||||
} from '../../config/types.js'
|
||||
import type { DBIdentifierName } from '../../database/types.js'
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
@@ -282,6 +283,10 @@ export type CollectionAdminOptions = {
|
||||
* Live preview options
|
||||
*/
|
||||
livePreview?: LivePreviewConfig
|
||||
meta?: {
|
||||
description?: string
|
||||
openGraph?: OpenGraphConfig
|
||||
}
|
||||
pagination?: {
|
||||
defaultLimit?: number
|
||||
limits?: number[]
|
||||
|
||||
@@ -8,6 +8,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
|
||||
dateFormat: 'MMMM do yyyy, h:mm a',
|
||||
disable: false,
|
||||
meta: {
|
||||
defaultOGImageType: 'dynamic',
|
||||
titleSuffix: '- Payload',
|
||||
},
|
||||
routes: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import joi from 'joi'
|
||||
|
||||
import { adminViewSchema } from './shared/adminViewSchema.js'
|
||||
import { componentSchema, livePreviewSchema } from './shared/componentSchema.js'
|
||||
import { openGraphSchema } from './shared/openGraphSchema.js'
|
||||
|
||||
const component = joi.alternatives().try(joi.object().unknown(), joi.func())
|
||||
|
||||
@@ -66,14 +67,10 @@ export default joi.object({
|
||||
globals: joi.array().items(joi.string()),
|
||||
}),
|
||||
meta: joi.object().keys({
|
||||
icons: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.array().items(joi.alternatives().try(joi.string(), joi.object())),
|
||||
joi.object(),
|
||||
joi.string().allow(null),
|
||||
),
|
||||
ogImage: joi.string(),
|
||||
defaultOGImageType: joi.string().valid('off', 'dynamic', 'static'),
|
||||
description: joi.string(),
|
||||
icons: joi.array().items(joi.object()),
|
||||
openGraph: openGraphSchema,
|
||||
titleSuffix: joi.string(),
|
||||
}),
|
||||
routes: joi.object({
|
||||
|
||||
16
packages/payload/src/config/shared/openGraphSchema.ts
Normal file
16
packages/payload/src/config/shared/openGraphSchema.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import joi from 'joi'
|
||||
|
||||
const ogImageObj = joi.object({
|
||||
type: joi.string(),
|
||||
alt: joi.string(),
|
||||
height: joi.alternatives().try(joi.string(), joi.number()),
|
||||
url: joi.string(),
|
||||
width: joi.alternatives().try(joi.string(), joi.number()),
|
||||
})
|
||||
|
||||
export const openGraphSchema = joi.object({
|
||||
description: joi.string(),
|
||||
images: joi.alternatives().try(joi.array().items(joi.string()), joi.array().items(ogImageObj)),
|
||||
title: joi.string(),
|
||||
url: joi.string(),
|
||||
})
|
||||
@@ -72,6 +72,77 @@ export type LivePreviewConfig = {
|
||||
| string
|
||||
}
|
||||
|
||||
export type OGImageConfig = {
|
||||
alt?: string
|
||||
height?: number | string
|
||||
type?: string
|
||||
url: string
|
||||
width?: number | string
|
||||
}
|
||||
|
||||
export type OpenGraphConfig = {
|
||||
description?: string
|
||||
images?: OGImageConfig | OGImageConfig[]
|
||||
siteName?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export type IconConfig = {
|
||||
color?: string
|
||||
/**
|
||||
* @see https://developer.mozilla.org/docs/Web/API/HTMLImageElement/fetchPriority
|
||||
*/
|
||||
fetchPriority?: 'auto' | 'high' | 'low'
|
||||
media?: string
|
||||
/** defaults to rel="icon" */
|
||||
rel?: string
|
||||
sizes?: string
|
||||
type?: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export type MetaConfig = {
|
||||
/**
|
||||
* When `static`, a pre-made image will be used for all pages.
|
||||
* When `dynamic`, a unique image will be generated for each page based on page content and given overrides.
|
||||
* When `off`, no Open Graph images will be generated and the `/api/og` endpoint will be disabled. You can still provide custom images using the `openGraph.images` property.
|
||||
* @default 'dynamic'
|
||||
*/
|
||||
defaultOGImageType?: 'dynamic' | 'off' | 'static'
|
||||
/**
|
||||
* Overrides the auto-generated <meta name="description"> of admin pages
|
||||
* @example `"This is my custom CMS built with Payload."`
|
||||
*/
|
||||
description?: string
|
||||
/**
|
||||
* Icons to be rendered by devices and browsers.
|
||||
*
|
||||
* For example browser tabs, phone home screens, and search engine results.
|
||||
*/
|
||||
icons?: IconConfig
|
||||
/**
|
||||
* Overrides the auto-generated <meta name="keywords"> of admin pages
|
||||
* @example `"CMS, Payload, Custom"`
|
||||
*/
|
||||
keywords?: string
|
||||
/**
|
||||
* Metadata to be rendered as `og` meta tags in the head of the Admin Panel.
|
||||
*
|
||||
* For example when sharing the Admin Panel on social media or through messaging services.
|
||||
*/
|
||||
openGraph?: OpenGraphConfig
|
||||
/**
|
||||
* Overrides the auto-generated <title> of admin pages
|
||||
* @example `"My Admin Panel"`
|
||||
*/
|
||||
title?: string
|
||||
/**
|
||||
* String to append to the auto-generated <title> of admin pages
|
||||
* @example `" - Custom CMS"`
|
||||
*/
|
||||
titleSuffix?: string
|
||||
}
|
||||
|
||||
export type ServerOnlyLivePreviewProperties = keyof Pick<LivePreviewConfig, 'url'>
|
||||
|
||||
type GeneratePreviewURLOptions = {
|
||||
@@ -477,26 +548,7 @@ export type Config = {
|
||||
globals?: string[]
|
||||
}
|
||||
/** Base meta data to use for the Admin Panel. Included properties are titleSuffix, ogImage, and favicon. */
|
||||
meta?: {
|
||||
/**
|
||||
* An array of Next.js metadata objects that represent icons to be used by devices and browsers.
|
||||
*
|
||||
* For example browser tabs, phone home screens, and search engine results.
|
||||
* @reference https://nextjs.org/docs/app/api-reference/functions/generate-metadata#icons
|
||||
*/
|
||||
icons?: NextMetadata['icons']
|
||||
/**
|
||||
* Public path to an image
|
||||
*
|
||||
* This image may be displayed as preview when the link is shared on social media
|
||||
*/
|
||||
ogImage?: string
|
||||
/**
|
||||
* String to append to the <title> of admin pages
|
||||
* @example `" - My Brand"`
|
||||
*/
|
||||
titleSuffix?: string
|
||||
}
|
||||
meta?: MetaConfig
|
||||
routes?: {
|
||||
/** The route for the account page. */
|
||||
account?: string
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
customViewSchema,
|
||||
livePreviewSchema,
|
||||
} from '../../config/shared/componentSchema.js'
|
||||
import { openGraphSchema } from '../../config/shared/openGraphSchema.js'
|
||||
|
||||
const globalSchema = joi
|
||||
.object()
|
||||
@@ -50,6 +51,10 @@ const globalSchema = joi
|
||||
hidden: joi.alternatives().try(joi.boolean(), joi.func()),
|
||||
hideAPIURL: joi.boolean(),
|
||||
livePreview: joi.object(livePreviewSchema),
|
||||
meta: joi.object({
|
||||
description: joi.string(),
|
||||
openGraph: openGraphSchema,
|
||||
}),
|
||||
preview: joi.func(),
|
||||
}),
|
||||
custom: joi.object().pattern(joi.string(), joi.any()),
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
EntityDescriptionComponent,
|
||||
GeneratePreviewURL,
|
||||
LivePreviewConfig,
|
||||
OpenGraphConfig,
|
||||
} from '../../config/types.js'
|
||||
import type { DBIdentifierName } from '../../database/types.js'
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
@@ -129,6 +130,10 @@ export type GlobalAdminOptions = {
|
||||
* Live preview options
|
||||
*/
|
||||
livePreview?: LivePreviewConfig
|
||||
meta?: {
|
||||
description?: string
|
||||
openGraph?: OpenGraphConfig
|
||||
}
|
||||
/**
|
||||
* Function to generate custom preview URL
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { default as payloadOgImage } from '../assets/og-image.png'
|
||||
export { default as payloadFavicon } from '../assets/payload-favicon.svg'
|
||||
export { default as payloadFaviconDark } from '../assets/payload-favicon-dark.png'
|
||||
export { default as payloadFaviconLight } from '../assets/payload-favicon-light.png'
|
||||
export { default as payloadFavicon } from './payload-favicon.svg'
|
||||
export { default as payloadFaviconDark } from './payload-favicon-dark.png'
|
||||
export { default as payloadFaviconLight } from './payload-favicon-light.png'
|
||||
export { default as staticOGImage } from './static-og-image.png'
|
||||
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -17,6 +17,8 @@
|
||||
// Use a pseudo element for the accessability so that it doesn't take up DOM space
|
||||
// Also because the parent element has `overflow: hidden` which would clip an outline
|
||||
&__home {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
|
||||
&:focus-visible {
|
||||
@@ -63,6 +65,15 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
.step-nav {
|
||||
&__home {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
gap: calc(var(--base) / 4);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import type { LabelFunction } from 'payload/config'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap'
|
||||
import React, { Fragment, createContext, useContext, useState } from 'react'
|
||||
|
||||
import { Icon } from '../../graphics/Icon/index.js'
|
||||
import { PayloadIcon } from '../../graphics/Icon/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import './index.scss'
|
||||
@@ -56,6 +57,12 @@ const StepNav: React.FC<{
|
||||
routes: { admin },
|
||||
} = config
|
||||
|
||||
const { componentMap } = useComponentMap()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const Icon = componentMap?.Icon || <PayloadIcon />
|
||||
|
||||
const LinkElement = Link || 'a'
|
||||
|
||||
return (
|
||||
@@ -63,7 +70,7 @@ const StepNav: React.FC<{
|
||||
{stepNav.length > 0 ? (
|
||||
<nav className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||
<LinkElement className={`${baseClass}__home`} href={admin} tabIndex={0}>
|
||||
<Icon />
|
||||
<span title={t('general:dashboard')}>{Icon}</span>
|
||||
</LinkElement>
|
||||
<span>/</span>
|
||||
{stepNav.map((item, i) => {
|
||||
@@ -92,7 +99,9 @@ const StepNav: React.FC<{
|
||||
</nav>
|
||||
) : (
|
||||
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||
<Icon />
|
||||
<div className={`${baseClass}__home`}>
|
||||
<span title={t('general:dashboard')}>{Icon}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
|
||||
@@ -1,31 +1,11 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import { useComponentMap } from '../../providers/ComponentMap/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
|
||||
const css = `
|
||||
.graphic-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.graphic-icon path {
|
||||
fill: var(--theme-elevation-1000);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.graphic-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const PayloadIcon: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
export const PayloadIcon: React.FC<{
|
||||
fill?: string
|
||||
}> = ({ fill: fillFromProps }) => {
|
||||
const fill = fillFromProps || 'var(--theme-elevation-1000)'
|
||||
|
||||
return (
|
||||
<span title={t('general:dashboard')}>
|
||||
<svg
|
||||
className="graphic-icon"
|
||||
height="100%"
|
||||
@@ -33,22 +13,11 @@ const PayloadIcon: React.FC = () => {
|
||||
width="100%"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<style>{css}</style>
|
||||
<path d="M11.5293 0L23 6.90096V19.9978L14.3608 25V11.9032L2.88452 5.00777L11.5293 0Z" />
|
||||
<path d="M10.6559 24.2727V14.0518L2 19.0651L10.6559 24.2727Z" />
|
||||
<path
|
||||
d="M11.5293 0L23 6.90096V19.9978L14.3608 25V11.9032L2.88452 5.00777L11.5293 0Z"
|
||||
fill={fill}
|
||||
/>
|
||||
<path d="M10.6559 24.2727V14.0518L2 19.0651L10.6559 24.2727Z" fill={fill} />
|
||||
</svg>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const Icon: React.FC = () => {
|
||||
const {
|
||||
componentMap: { Icon: CustomIcon },
|
||||
} = useComponentMap()
|
||||
|
||||
if (CustomIcon) {
|
||||
return <Fragment>{CustomIcon}</Fragment>
|
||||
}
|
||||
|
||||
return <PayloadIcon />
|
||||
}
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -912,9 +912,6 @@ importers:
|
||||
mini-css-extract-plugin:
|
||||
specifier: 1.6.2
|
||||
version: 1.6.2(webpack@5.91.0)
|
||||
next:
|
||||
specifier: ^14.3.0-canary.7
|
||||
version: 14.3.0-canary.7(@babel/core@7.24.4)(@playwright/test@1.43.0)(react-dom@18.2.0)(react@18.2.0)(sass@1.74.1)
|
||||
nodemon:
|
||||
specifier: 3.0.3
|
||||
version: 3.0.3
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import path from 'path'
|
||||
import { getFileByPath } from 'payload/uploads'
|
||||
import { fileURLToPath } from 'url'
|
||||
// import path from 'path'
|
||||
// import { getFileByPath } from 'payload/uploads'
|
||||
// import { fileURLToPath } from 'url'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { MediaCollection } from './collections/Media/index.js'
|
||||
// import { MediaCollection } from './collections/Media/index.js'
|
||||
import { PostsCollection, postsSlug } from './collections/Posts/index.js'
|
||||
import { MenuGlobal } from './globals/Menu/index.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
// const filename = fileURLToPath(import.meta.url)
|
||||
// const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
// ...extend config here
|
||||
|
||||
@@ -18,6 +18,13 @@ export const Posts: CollectionConfig = {
|
||||
listSearchableFields: ['id', 'title', 'description', 'number'],
|
||||
preview: () => 'https://payloadcms.com',
|
||||
useAsTitle: 'title',
|
||||
meta: {
|
||||
description: 'This is a custom meta description for posts',
|
||||
openGraph: {
|
||||
title: 'This is a custom OG title for posts',
|
||||
description: 'This is a custom OG description for posts',
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { CustomIdRow } from './collections/CustomIdRow.js'
|
||||
import { CustomIdTab } from './collections/CustomIdTab.js'
|
||||
import { CustomViews1 } from './collections/CustomViews1.js'
|
||||
@@ -42,6 +41,7 @@ import {
|
||||
customParamViewPath,
|
||||
customViewPath,
|
||||
} from './shared.js'
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
admin: {
|
||||
components: {
|
||||
@@ -83,6 +83,12 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
routes: customAdminRoutes,
|
||||
meta: {
|
||||
titleSuffix: '- Custom CMS',
|
||||
description: 'This is a custom meta description',
|
||||
openGraph: {
|
||||
title: 'This is a custom OG title',
|
||||
description: 'This is a custom OG description',
|
||||
},
|
||||
icons: [
|
||||
{
|
||||
type: 'image/png',
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
exactText,
|
||||
getAdminRoutes,
|
||||
initPageConsoleErrorCatch,
|
||||
login,
|
||||
openDocControls,
|
||||
openDocDrawer,
|
||||
openNav,
|
||||
@@ -117,7 +116,12 @@ describe('admin', () => {
|
||||
})
|
||||
|
||||
describe('metadata', () => {
|
||||
test('should set Payload favicons', async () => {
|
||||
test('should render custom page title suffix', async () => {
|
||||
await page.goto(`${serverURL}/admin`)
|
||||
await expect(page.title()).resolves.toMatch(/- Custom CMS$/)
|
||||
})
|
||||
|
||||
test('should render payload favicons', async () => {
|
||||
await page.goto(postsUrl.admin)
|
||||
const favicons = page.locator('link[rel="icon"]')
|
||||
await expect(favicons).toHaveCount(4)
|
||||
@@ -130,7 +134,25 @@ describe('admin', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('should inject custom favicons', async () => {
|
||||
test('should render custom meta description from root config', async () => {
|
||||
await page.goto(`${serverURL}/admin`)
|
||||
await expect(page.locator('meta[name="description"]')).toHaveAttribute(
|
||||
'content',
|
||||
/This is a custom meta description/,
|
||||
)
|
||||
})
|
||||
|
||||
test('should render custom meta description from collection config', async () => {
|
||||
await page.goto(postsUrl.collection(postsCollectionSlug))
|
||||
await page.locator('.collection-list .table a').first().click()
|
||||
|
||||
await expect(page.locator('meta[name="description"]')).toHaveAttribute(
|
||||
'content',
|
||||
/This is a custom meta description for posts/,
|
||||
)
|
||||
})
|
||||
|
||||
test('should render custom favicons', async () => {
|
||||
await page.goto(postsUrl.admin)
|
||||
const favicons = page.locator('link[rel="icon"]')
|
||||
await expect(favicons).toHaveCount(4)
|
||||
@@ -138,6 +160,65 @@ describe('admin', () => {
|
||||
await expect(favicons.nth(3)).toHaveAttribute('media', '(prefers-color-scheme: dark)')
|
||||
await expect(favicons.nth(3)).toHaveAttribute('href', /\/custom-favicon-light\.[a-z\d]+\.png/)
|
||||
})
|
||||
|
||||
test('should render custom og:title from root config', async () => {
|
||||
await page.goto(`${serverURL}/admin`)
|
||||
await expect(page.locator('meta[property="og:title"]')).toHaveAttribute(
|
||||
'content',
|
||||
/This is a custom OG title/,
|
||||
)
|
||||
})
|
||||
|
||||
test('should render custom og:description from root config', async () => {
|
||||
await page.goto(`${serverURL}/admin`)
|
||||
await expect(page.locator('meta[property="og:description"]')).toHaveAttribute(
|
||||
'content',
|
||||
/This is a custom OG description/,
|
||||
)
|
||||
})
|
||||
|
||||
test('should render custom og:title from collection config', async () => {
|
||||
await page.goto(postsUrl.collection(postsCollectionSlug))
|
||||
await page.locator('.collection-list .table a').first().click()
|
||||
|
||||
await expect(page.locator('meta[property="og:title"]')).toHaveAttribute(
|
||||
'content',
|
||||
/This is a custom OG title for posts/,
|
||||
)
|
||||
})
|
||||
|
||||
test('should render custom og:description from collection config', async () => {
|
||||
await page.goto(postsUrl.collection(postsCollectionSlug))
|
||||
await page.locator('.collection-list .table a').first().click()
|
||||
|
||||
await expect(page.locator('meta[property="og:description"]')).toHaveAttribute(
|
||||
'content',
|
||||
/This is a custom OG description for posts/,
|
||||
)
|
||||
})
|
||||
|
||||
test('should render og:image with dynamic URL', async () => {
|
||||
await page.goto(postsUrl.admin)
|
||||
const encodedOGDescription = encodeURIComponent('This is a custom OG description')
|
||||
const encodedOGTitle = encodeURIComponent('This is a custom OG title')
|
||||
|
||||
await expect(page.locator('meta[property="og:image"]')).toHaveAttribute(
|
||||
'content',
|
||||
new RegExp(`/api/og\\?description=${encodedOGDescription}&title=${encodedOGTitle}`),
|
||||
)
|
||||
})
|
||||
|
||||
test('should render twitter:image with dynamic URL', async () => {
|
||||
await page.goto(postsUrl.admin)
|
||||
|
||||
const encodedOGDescription = encodeURIComponent('This is a custom OG description')
|
||||
const encodedOGTitle = encodeURIComponent('This is a custom OG title')
|
||||
|
||||
await expect(page.locator('meta[name="twitter:image"]')).toHaveAttribute(
|
||||
'content',
|
||||
new RegExp(`/api/og\\?description=${encodedOGDescription}&title=${encodedOGTitle}`),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('routing', () => {
|
||||
|
||||
Reference in New Issue
Block a user