feat: custom view and document-level metadata (#7716)
This commit is contained in:
@@ -45,7 +45,7 @@ export const meta = async (args: { serverURL: string } & MetaConfig): Promise<an
|
||||
icons = customIcons
|
||||
}
|
||||
|
||||
const metaTitle = `${title} ${titleSuffix}`
|
||||
const metaTitle = [title, titleSuffix].filter(Boolean).join(' ')
|
||||
|
||||
const ogTitle = `${typeof openGraphFromProps?.title === 'string' ? openGraphFromProps.title : title} ${titleSuffix}`
|
||||
|
||||
|
||||
@@ -16,18 +16,25 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
? getTranslation(globalConfig.label, i18n)
|
||||
: ''
|
||||
|
||||
const metaTitle = `API - ${entityLabel}`
|
||||
const description = `API - ${entityLabel}`
|
||||
|
||||
return Promise.resolve(
|
||||
meta({
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
description: `API - ${entityLabel}`,
|
||||
keywords: 'API',
|
||||
serverURL: config.serverURL,
|
||||
title: metaTitle,
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
title: `API - ${entityLabel}`,
|
||||
...(collectionConfig
|
||||
? {
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(collectionConfig?.admin?.components?.views?.edit?.api?.meta || {}),
|
||||
}
|
||||
: {}),
|
||||
...(globalConfig
|
||||
? {
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin?.components?.views?.edit?.api?.meta || {}),
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,25 +12,42 @@ export const getCustomViewByRoute = ({
|
||||
views:
|
||||
| SanitizedCollectionConfig['admin']['components']['views']
|
||||
| SanitizedGlobalConfig['admin']['components']['views']
|
||||
}): EditViewComponent => {
|
||||
if (typeof views?.edit === 'object' && typeof views?.edit !== 'function') {
|
||||
const foundViewConfig = Object.entries(views.edit).find(([, view]) => {
|
||||
if (typeof view === 'object' && typeof view !== 'function' && 'path' in view) {
|
||||
}): {
|
||||
Component: EditViewComponent
|
||||
viewKey?: string
|
||||
} => {
|
||||
if (typeof views?.edit === 'object') {
|
||||
let viewKey: string
|
||||
|
||||
const foundViewConfig = Object.entries(views.edit).find(([key, view]) => {
|
||||
if (typeof view === 'object' && 'path' in view) {
|
||||
const viewPath = `${baseRoute}${view.path}`
|
||||
|
||||
return isPathMatchingRoute({
|
||||
const isMatching = isPathMatchingRoute({
|
||||
currentRoute,
|
||||
exact: true,
|
||||
path: viewPath,
|
||||
})
|
||||
|
||||
if (isMatching) {
|
||||
viewKey = key
|
||||
}
|
||||
|
||||
return isMatching
|
||||
}
|
||||
|
||||
return false
|
||||
})?.[1]
|
||||
|
||||
if (foundViewConfig && 'Component' in foundViewConfig) {
|
||||
return foundViewConfig.Component
|
||||
return {
|
||||
Component: foundViewConfig.Component,
|
||||
viewKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
return {
|
||||
Component: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload'
|
||||
import type { EditConfig, SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload'
|
||||
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
@@ -10,11 +10,13 @@ import { generateMetadata as livePreviewMeta } from '../LivePreview/meta.js'
|
||||
import { generateNotFoundMeta } from '../NotFound/meta.js'
|
||||
import { generateMetadata as versionMeta } from '../Version/meta.js'
|
||||
import { generateMetadata as versionsMeta } from '../Versions/meta.js'
|
||||
import { getViewsFromConfig } from './getViewsFromConfig.js'
|
||||
|
||||
export type GenerateEditViewMetadata = (
|
||||
args: {
|
||||
collectionConfig?: SanitizedCollectionConfig | null
|
||||
globalConfig?: SanitizedGlobalConfig | null
|
||||
view?: keyof EditConfig
|
||||
} & Parameters<GenerateViewMetadata>[0],
|
||||
) => Promise<Metadata>
|
||||
|
||||
@@ -36,54 +38,71 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
|
||||
isGlobal || Boolean(isCollection && segments?.length > 2 && segments[2] !== 'create')
|
||||
|
||||
if (isCollection) {
|
||||
// `/:id`
|
||||
// `/:collection/:id`
|
||||
if (params.segments.length === 3) {
|
||||
fn = editMeta
|
||||
}
|
||||
|
||||
// `/:id/api`
|
||||
if (params.segments.length === 4 && params.segments[3] === 'api') {
|
||||
fn = apiMeta
|
||||
// `/:collection/:id/:view`
|
||||
if (params.segments.length === 4) {
|
||||
switch (params.segments[3]) {
|
||||
case 'api':
|
||||
// `/:collection/:id/api`
|
||||
fn = apiMeta
|
||||
break
|
||||
case 'preview':
|
||||
// `/:collection/:id/preview`
|
||||
fn = livePreviewMeta
|
||||
break
|
||||
case 'versions':
|
||||
// `/:collection/:id/versions`
|
||||
fn = versionsMeta
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// `/:id/preview`
|
||||
if (params.segments.length === 4 && params.segments[3] === 'preview') {
|
||||
fn = livePreviewMeta
|
||||
}
|
||||
|
||||
// `/:id/versions`
|
||||
if (params.segments.length === 4 && params.segments[3] === 'versions') {
|
||||
fn = versionsMeta
|
||||
}
|
||||
|
||||
// `/:id/versions/:version`
|
||||
if (params.segments.length === 5 && params.segments[3] === 'versions') {
|
||||
fn = versionMeta
|
||||
// `/:collection/:id/:slug-1/:slug-2`
|
||||
if (params.segments.length === 5) {
|
||||
switch (params.segments[3]) {
|
||||
case 'versions':
|
||||
// `/:collection/:id/versions/:version`
|
||||
fn = versionMeta
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isGlobal) {
|
||||
// `/:slug`
|
||||
// `/:global`
|
||||
if (params.segments?.length === 2) {
|
||||
fn = editMeta
|
||||
}
|
||||
|
||||
// `/:slug/api`
|
||||
if (params.segments?.length === 3 && params.segments[2] === 'api') {
|
||||
fn = apiMeta
|
||||
// `/:global/:view`
|
||||
if (params.segments?.length === 3) {
|
||||
switch (params.segments[2]) {
|
||||
case 'api':
|
||||
// `/:global/api`
|
||||
fn = apiMeta
|
||||
break
|
||||
case 'preview':
|
||||
// `/:global/preview`
|
||||
fn = livePreviewMeta
|
||||
break
|
||||
case 'versions':
|
||||
// `/:global/versions`
|
||||
fn = versionsMeta
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// `/:slug/preview`
|
||||
if (params.segments?.length === 3 && params.segments[2] === 'preview') {
|
||||
fn = livePreviewMeta
|
||||
}
|
||||
|
||||
// `/:slug/versions`
|
||||
if (params.segments?.length === 3 && params.segments[2] === 'versions') {
|
||||
fn = versionsMeta
|
||||
}
|
||||
|
||||
// `/:slug/versions/:version`
|
||||
// `/:global/versions/:version`
|
||||
if (params.segments?.length === 4 && params.segments[2] === 'versions') {
|
||||
fn = versionMeta
|
||||
}
|
||||
@@ -101,6 +120,31 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
|
||||
i18n,
|
||||
isEditing,
|
||||
})
|
||||
} else {
|
||||
const { viewKey } = getViewsFromConfig({
|
||||
collectionConfig,
|
||||
config,
|
||||
globalConfig,
|
||||
overrideDocPermissions: true,
|
||||
routeSegments: typeof segments === 'string' ? [segments] : segments,
|
||||
})
|
||||
|
||||
if (viewKey) {
|
||||
const customViewConfig =
|
||||
collectionConfig?.admin?.components?.views?.edit?.[viewKey] ||
|
||||
globalConfig?.admin?.components?.views?.edit?.[viewKey]
|
||||
|
||||
if (customViewConfig) {
|
||||
return editMeta({
|
||||
collectionConfig,
|
||||
config,
|
||||
globalConfig,
|
||||
i18n,
|
||||
isEditing,
|
||||
view: viewKey as keyof EditConfig,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return generateNotFoundMeta({ config, i18n })
|
||||
|
||||
@@ -31,25 +31,36 @@ export const getViewsFromConfig = ({
|
||||
config,
|
||||
docPermissions,
|
||||
globalConfig,
|
||||
overrideDocPermissions,
|
||||
routeSegments,
|
||||
}: {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
config: SanitizedConfig
|
||||
docPermissions: CollectionPermission | GlobalPermission
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
routeSegments: string[]
|
||||
}): {
|
||||
} & (
|
||||
| {
|
||||
docPermissions: CollectionPermission | GlobalPermission
|
||||
overrideDocPermissions?: false | undefined
|
||||
}
|
||||
| {
|
||||
docPermissions?: never
|
||||
overrideDocPermissions: true
|
||||
}
|
||||
)): {
|
||||
CustomView: ViewFromConfig<ServerSideEditViewProps>
|
||||
DefaultView: ViewFromConfig<ServerSideEditViewProps>
|
||||
/**
|
||||
* The error view to display if CustomView or DefaultView do not exist (could be either due to not found, or unauthorized). Can be null
|
||||
*/
|
||||
ErrorView: ViewFromConfig<AdminViewProps>
|
||||
viewKey: string
|
||||
} | null => {
|
||||
// Conditionally import and lazy load the default view
|
||||
let DefaultView: ViewFromConfig<ServerSideEditViewProps> = null
|
||||
let CustomView: ViewFromConfig<ServerSideEditViewProps> = null
|
||||
let ErrorView: ViewFromConfig<AdminViewProps> = null
|
||||
let viewKey: string
|
||||
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
@@ -69,7 +80,7 @@ export const getViewsFromConfig = ({
|
||||
const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] =
|
||||
routeSegments
|
||||
|
||||
if (!docPermissions?.read?.permission) {
|
||||
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
|
||||
notFound()
|
||||
} else {
|
||||
// `../:id`, or `../create`
|
||||
@@ -77,7 +88,11 @@ export const getViewsFromConfig = ({
|
||||
case 3: {
|
||||
switch (segment3) {
|
||||
case 'create': {
|
||||
if ('create' in docPermissions && docPermissions?.create?.permission) {
|
||||
if (
|
||||
!overrideDocPermissions &&
|
||||
'create' in docPermissions &&
|
||||
docPermissions?.create?.permission
|
||||
) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
||||
}
|
||||
@@ -93,12 +108,44 @@ export const getViewsFromConfig = ({
|
||||
}
|
||||
|
||||
default: {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultEditView,
|
||||
const baseRoute = [
|
||||
adminRoute !== '/' && adminRoute,
|
||||
'collections',
|
||||
collectionSlug,
|
||||
segment3,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
const currentRoute = [baseRoute, segment4, segment5, ...remainingSegments]
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
const { Component: CustomViewComponent, viewKey: customViewKey } =
|
||||
getCustomViewByRoute({
|
||||
baseRoute,
|
||||
currentRoute,
|
||||
views,
|
||||
})
|
||||
|
||||
console.log('CustomViewComponent', customViewKey)
|
||||
|
||||
if (customViewKey) {
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
}
|
||||
} else {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
||||
}
|
||||
|
||||
DefaultView = {
|
||||
Component: DefaultEditView,
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -130,7 +177,7 @@ export const getViewsFromConfig = ({
|
||||
}
|
||||
|
||||
case 'versions': {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'versions'),
|
||||
}
|
||||
@@ -159,13 +206,21 @@ export const getViewsFromConfig = ({
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByRoute({
|
||||
const { Component: CustomViewComponent, viewKey: customViewKey } =
|
||||
getCustomViewByRoute({
|
||||
baseRoute,
|
||||
currentRoute,
|
||||
views,
|
||||
}),
|
||||
})
|
||||
|
||||
if (customViewKey) {
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -175,7 +230,7 @@ export const getViewsFromConfig = ({
|
||||
// `../:id/versions/:version`, etc
|
||||
default: {
|
||||
if (segment4 === 'versions') {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'version'),
|
||||
}
|
||||
@@ -201,14 +256,23 @@ export const getViewsFromConfig = ({
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByRoute({
|
||||
const { Component: CustomViewComponent, viewKey: customViewKey } = getCustomViewByRoute(
|
||||
{
|
||||
baseRoute,
|
||||
currentRoute,
|
||||
views,
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
if (customViewKey) {
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -218,7 +282,7 @@ export const getViewsFromConfig = ({
|
||||
if (globalConfig) {
|
||||
const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments
|
||||
|
||||
if (!docPermissions?.read?.permission) {
|
||||
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
|
||||
notFound()
|
||||
} else {
|
||||
switch (routeSegments.length) {
|
||||
@@ -257,10 +321,11 @@ export const getViewsFromConfig = ({
|
||||
}
|
||||
|
||||
case 'versions': {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'versions'),
|
||||
}
|
||||
|
||||
DefaultView = {
|
||||
Component: DefaultVersionsView,
|
||||
}
|
||||
@@ -273,7 +338,7 @@ export const getViewsFromConfig = ({
|
||||
}
|
||||
|
||||
default: {
|
||||
if (docPermissions?.read?.permission) {
|
||||
if (!overrideDocPermissions && docPermissions?.read?.permission) {
|
||||
const baseRoute = [adminRoute, globalEntity, globalSlug, segment3]
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
@@ -282,15 +347,23 @@ export const getViewsFromConfig = ({
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByRoute({
|
||||
const { Component: CustomViewComponent, viewKey: customViewKey } =
|
||||
getCustomViewByRoute({
|
||||
baseRoute,
|
||||
currentRoute,
|
||||
views,
|
||||
}),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultEditView,
|
||||
})
|
||||
|
||||
if (customViewKey) {
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
}
|
||||
} else {
|
||||
DefaultView = {
|
||||
Component: DefaultEditView,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ErrorView = {
|
||||
@@ -306,7 +379,7 @@ export const getViewsFromConfig = ({
|
||||
default: {
|
||||
// `../:slug/versions/:version`, etc
|
||||
if (segment3 === 'versions') {
|
||||
if (docPermissions?.readVersions?.permission) {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'version'),
|
||||
}
|
||||
@@ -327,14 +400,23 @@ export const getViewsFromConfig = ({
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByRoute({
|
||||
const { Component: CustomViewComponent, viewKey: customViewKey } = getCustomViewByRoute(
|
||||
{
|
||||
baseRoute,
|
||||
currentRoute,
|
||||
views,
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
if (customViewKey) {
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -345,5 +427,6 @@ export const getViewsFromConfig = ({
|
||||
CustomView,
|
||||
DefaultView,
|
||||
ErrorView,
|
||||
viewKey,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import type { MetaConfig } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
@@ -12,6 +13,7 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
globalConfig,
|
||||
i18n,
|
||||
isEditing,
|
||||
view = 'default',
|
||||
}): Promise<Metadata> => {
|
||||
const { t } = i18n
|
||||
|
||||
@@ -21,34 +23,45 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
? getTranslation(globalConfig.label, i18n)
|
||||
: ''
|
||||
|
||||
const metaTitle = `${isEditing ? t('general:editing') : t('general:creating')} - ${entityLabel}`
|
||||
const metaToUse: MetaConfig = {
|
||||
...(config.admin.meta || {}),
|
||||
description: `${isEditing ? t('general:editing') : t('general:creating')} - ${entityLabel}`,
|
||||
keywords: `${entityLabel}, Payload, CMS`,
|
||||
title: `${isEditing ? t('general:editing') : t('general:creating')} - ${entityLabel}`,
|
||||
}
|
||||
|
||||
const ogTitle = `${isEditing ? t('general:edit') : t('general:edit')} - ${entityLabel}`
|
||||
|
||||
const description = `${isEditing ? t('general:editing') : t('general:creating')} - ${entityLabel}`
|
||||
|
||||
const keywords = `${entityLabel}, Payload, CMS`
|
||||
|
||||
const baseOGOverrides = config.admin.meta.openGraph || {}
|
||||
|
||||
const entityOGOverrides = collectionConfig
|
||||
? collectionConfig.admin?.meta?.openGraph
|
||||
: globalConfig
|
||||
? globalConfig.admin?.meta?.openGraph
|
||||
: {}
|
||||
const ogToUse: MetaConfig['openGraph'] = {
|
||||
title: `${isEditing ? t('general:edit') : t('general:edit')} - ${entityLabel}`,
|
||||
...(config.admin.meta.openGraph || {}),
|
||||
...(collectionConfig
|
||||
? {
|
||||
...(collectionConfig?.admin.meta?.openGraph || {}),
|
||||
...(collectionConfig?.admin?.components?.views?.edit?.[view]?.meta?.openGraph || {}),
|
||||
}
|
||||
: {}),
|
||||
...(globalConfig
|
||||
? {
|
||||
...(globalConfig?.admin.meta?.openGraph || {}),
|
||||
...(globalConfig?.admin?.components?.views?.edit?.[view]?.meta?.openGraph || {}),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
|
||||
return meta({
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords,
|
||||
openGraph: {
|
||||
title: ogTitle,
|
||||
...baseOGOverrides,
|
||||
...entityOGOverrides,
|
||||
},
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
...metaToUse,
|
||||
openGraph: ogToUse,
|
||||
...(collectionConfig
|
||||
? {
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(collectionConfig?.admin?.components?.views?.edit?.[view]?.meta || {}),
|
||||
}
|
||||
: {}),
|
||||
...(globalConfig
|
||||
? {
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin?.components?.views?.edit?.[view]?.meta || {}),
|
||||
}
|
||||
: {}),
|
||||
serverURL: config.serverURL,
|
||||
title: metaTitle,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,4 +17,5 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
globalConfig,
|
||||
i18n,
|
||||
isEditing,
|
||||
view: 'livePreview',
|
||||
})
|
||||
|
||||
42
packages/next/src/views/Root/generateCustomViewMetadata.ts
Normal file
42
packages/next/src/views/Root/generateCustomViewMetadata.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { Metadata } from 'next'
|
||||
import type {
|
||||
AdminViewConfig,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedConfig,
|
||||
SanitizedGlobalConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { meta } from '../../utilities/meta.js'
|
||||
|
||||
export const generateCustomViewMetadata = async (args: {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
config: SanitizedConfig
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
i18n: I18nClient
|
||||
viewConfig: AdminViewConfig
|
||||
}): Promise<Metadata> => {
|
||||
const {
|
||||
config,
|
||||
// i18n: { t },
|
||||
viewConfig,
|
||||
} = args
|
||||
|
||||
if (!viewConfig) {
|
||||
return null
|
||||
}
|
||||
|
||||
return meta({
|
||||
description: `Payload`,
|
||||
keywords: `Payload`,
|
||||
serverURL: config.serverURL,
|
||||
title: 'Payload',
|
||||
...(config.admin.meta || {}),
|
||||
...(viewConfig.meta || {}),
|
||||
openGraph: {
|
||||
title: 'Payload',
|
||||
...(config.admin.meta?.openGraph || {}),
|
||||
...(viewConfig.meta?.openGraph || {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
65
packages/next/src/views/Root/getCustomViewByRoute.ts
Normal file
65
packages/next/src/views/Root/getCustomViewByRoute.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { AdminViewConfig, SanitizedConfig } from 'payload'
|
||||
|
||||
import type { ViewFromConfig } from './getViewFromConfig.js'
|
||||
|
||||
import { isPathMatchingRoute } from './isPathMatchingRoute.js'
|
||||
|
||||
export const getCustomViewByRoute = ({
|
||||
config,
|
||||
currentRoute: currentRouteWithAdmin,
|
||||
}: {
|
||||
config: SanitizedConfig
|
||||
currentRoute: string
|
||||
}): {
|
||||
view: ViewFromConfig
|
||||
viewConfig: AdminViewConfig
|
||||
viewKey: string
|
||||
} => {
|
||||
const {
|
||||
admin: {
|
||||
components: { views },
|
||||
},
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
const currentRoute = currentRouteWithAdmin.replace(adminRoute, '')
|
||||
let viewKey: string
|
||||
|
||||
const foundViewConfig =
|
||||
(views &&
|
||||
typeof views === 'object' &&
|
||||
Object.entries(views).find(([key, view]) => {
|
||||
const isMatching = isPathMatchingRoute({
|
||||
currentRoute,
|
||||
exact: view.exact,
|
||||
path: view.path,
|
||||
sensitive: view.sensitive,
|
||||
strict: view.strict,
|
||||
})
|
||||
|
||||
if (isMatching) {
|
||||
viewKey = key
|
||||
}
|
||||
|
||||
return isMatching
|
||||
})?.[1]) ||
|
||||
undefined
|
||||
|
||||
if (!foundViewConfig) {
|
||||
return {
|
||||
view: {
|
||||
Component: null,
|
||||
},
|
||||
viewConfig: null,
|
||||
viewKey: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
view: {
|
||||
payloadComponent: foundViewConfig.Component,
|
||||
},
|
||||
viewConfig: foundViewConfig,
|
||||
viewKey,
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import type { ViewFromConfig } from './getViewFromConfig.js'
|
||||
|
||||
import { isPathMatchingRoute } from './isPathMatchingRoute.js'
|
||||
|
||||
export const getCustomViewByRoute = ({
|
||||
config,
|
||||
currentRoute: currentRouteWithAdmin,
|
||||
}: {
|
||||
config: SanitizedConfig
|
||||
currentRoute: string
|
||||
}): ViewFromConfig => {
|
||||
const {
|
||||
admin: {
|
||||
components: { views },
|
||||
},
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
const currentRoute = currentRouteWithAdmin.replace(adminRoute, '')
|
||||
|
||||
const foundViewConfig =
|
||||
views &&
|
||||
typeof views === 'object' &&
|
||||
Object.entries(views).find(([, view]) => {
|
||||
return isPathMatchingRoute({
|
||||
currentRoute,
|
||||
exact: view.exact,
|
||||
path: view.path,
|
||||
sensitive: view.sensitive,
|
||||
strict: view.strict,
|
||||
})
|
||||
})?.[1]
|
||||
|
||||
if (!foundViewConfig) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
payloadComponent: foundViewConfig.Component,
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export const getViewFromConfig = ({
|
||||
case 1: {
|
||||
// users can override the default routes via `admin.routes` config
|
||||
// i.e.{ admin: { routes: { logout: '/sign-out', inactivity: '/idle' }}}
|
||||
let viewToRender: keyof typeof oneSegmentViews
|
||||
let viewKey: keyof typeof oneSegmentViews
|
||||
|
||||
if (config.admin.routes) {
|
||||
const matchedRoute = Object.entries(config.admin.routes).find(([, route]) => {
|
||||
@@ -111,11 +111,11 @@ export const getViewFromConfig = ({
|
||||
})
|
||||
|
||||
if (matchedRoute) {
|
||||
viewToRender = matchedRoute[0] as keyof typeof oneSegmentViews
|
||||
viewKey = matchedRoute[0] as keyof typeof oneSegmentViews
|
||||
}
|
||||
}
|
||||
|
||||
if (oneSegmentViews[viewToRender]) {
|
||||
if (oneSegmentViews[viewKey]) {
|
||||
// --> /account
|
||||
// --> /create-first-user
|
||||
// --> /forgot
|
||||
@@ -125,12 +125,13 @@ export const getViewFromConfig = ({
|
||||
// --> /unauthorized
|
||||
|
||||
ViewToRender = {
|
||||
Component: oneSegmentViews[viewToRender],
|
||||
Component: oneSegmentViews[viewKey],
|
||||
}
|
||||
templateClassName = baseClasses[viewToRender]
|
||||
|
||||
templateClassName = baseClasses[viewKey]
|
||||
templateType = 'minimal'
|
||||
|
||||
if (viewToRender === 'account') {
|
||||
if (viewKey === 'account') {
|
||||
initPageOptions.redirectUnauthenticatedUser = true
|
||||
templateType = 'default'
|
||||
}
|
||||
@@ -150,17 +151,21 @@ export const getViewFromConfig = ({
|
||||
if (isCollection) {
|
||||
// --> /collections/:collectionSlug
|
||||
initPageOptions.redirectUnauthenticatedUser = true
|
||||
|
||||
ViewToRender = {
|
||||
Component: ListView,
|
||||
}
|
||||
|
||||
templateClassName = `${segmentTwo}-list`
|
||||
templateType = 'default'
|
||||
} else if (isGlobal) {
|
||||
// --> /globals/:globalSlug
|
||||
initPageOptions.redirectUnauthenticatedUser = true
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
}
|
||||
|
||||
templateClassName = 'global-edit'
|
||||
templateType = 'default'
|
||||
}
|
||||
@@ -172,6 +177,7 @@ export const getViewFromConfig = ({
|
||||
ViewToRender = {
|
||||
Component: Verify,
|
||||
}
|
||||
|
||||
templateClassName = 'verify'
|
||||
templateType = 'minimal'
|
||||
} else if (isCollection) {
|
||||
@@ -182,9 +188,11 @@ export const getViewFromConfig = ({
|
||||
// --> /collections/:collectionSlug/:id/versions/:versionId
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
initPageOptions.redirectUnauthenticatedUser = true
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
}
|
||||
|
||||
templateClassName = `collection-default-edit`
|
||||
templateType = 'default'
|
||||
} else if (isGlobal) {
|
||||
@@ -194,9 +202,11 @@ export const getViewFromConfig = ({
|
||||
// --> /globals/:globalSlug/versions/:versionId
|
||||
// --> /globals/:globalSlug/api
|
||||
initPageOptions.redirectUnauthenticatedUser = true
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
}
|
||||
|
||||
templateClassName = `global-edit`
|
||||
templateType = 'default'
|
||||
}
|
||||
@@ -204,7 +214,7 @@ export const getViewFromConfig = ({
|
||||
}
|
||||
|
||||
if (!ViewToRender) {
|
||||
ViewToRender = getCustomViewByRoute({ config, currentRoute })
|
||||
ViewToRender = getCustomViewByRoute({ config, currentRoute })?.view
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -13,6 +13,8 @@ import { generateNotFoundMeta } from '../NotFound/meta.js'
|
||||
import { generateResetPasswordMetadata } from '../ResetPassword/index.js'
|
||||
import { generateUnauthorizedMetadata } from '../Unauthorized/index.js'
|
||||
import { generateVerifyMetadata } from '../Verify/index.js'
|
||||
import { generateCustomViewMetadata } from './generateCustomViewMetadata.js'
|
||||
import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
||||
|
||||
const oneSegmentMeta = {
|
||||
'create-first-user': generateCreateFirstUserMetadata,
|
||||
@@ -38,6 +40,7 @@ export const generatePageMetadata = async ({ config: configPromise, params }: Ar
|
||||
|
||||
const segments = Array.isArray(params.segments) ? params.segments : []
|
||||
|
||||
const currentRoute = `/${segments.join('/')}`
|
||||
const [segmentOne, segmentTwo] = segments
|
||||
|
||||
const isGlobal = segmentOne === 'globals'
|
||||
@@ -130,7 +133,22 @@ export const generatePageMetadata = async ({ config: configPromise, params }: Ar
|
||||
}
|
||||
|
||||
if (!meta) {
|
||||
meta = await generateNotFoundMeta({ config, i18n })
|
||||
const { viewConfig, viewKey } = getCustomViewByRoute({
|
||||
config,
|
||||
currentRoute,
|
||||
})
|
||||
|
||||
if (viewKey) {
|
||||
// Custom Views
|
||||
// --> /:path
|
||||
meta = await generateCustomViewMetadata({
|
||||
config,
|
||||
i18n,
|
||||
viewConfig,
|
||||
})
|
||||
} else {
|
||||
meta = await generateNotFoundMeta({ config, i18n })
|
||||
}
|
||||
}
|
||||
|
||||
return meta
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import type { MetaConfig } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { formatDate } from '@payloadcms/ui/shared'
|
||||
@@ -15,9 +16,9 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
}): Promise<Metadata> => {
|
||||
const { t } = i18n
|
||||
|
||||
let title: string = ''
|
||||
let description: string = ''
|
||||
const keywords: string = ''
|
||||
let metaToUse: MetaConfig = {
|
||||
...(config.admin.meta || {}),
|
||||
}
|
||||
|
||||
const doc: any = {} // TODO: figure this out
|
||||
|
||||
@@ -29,23 +30,30 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
const useAsTitle = collectionConfig?.admin?.useAsTitle || 'id'
|
||||
const entityLabel = getTranslation(collectionConfig.labels.singular, i18n)
|
||||
const titleFromData = doc?.[useAsTitle]
|
||||
title = `${t('version:version')}${formattedCreatedAt ? ` - ${formattedCreatedAt}` : ''}${titleFromData ? ` - ${titleFromData}` : ''} - ${entityLabel}`
|
||||
description = t('version:viewingVersion', { documentTitle: doc[useAsTitle], entityLabel })
|
||||
|
||||
metaToUse = {
|
||||
...(config.admin.meta || {}),
|
||||
description: t('version:viewingVersion', { documentTitle: doc[useAsTitle], entityLabel }),
|
||||
title: `${t('version:version')}${formattedCreatedAt ? ` - ${formattedCreatedAt}` : ''}${titleFromData ? ` - ${titleFromData}` : ''} - ${entityLabel}`,
|
||||
...(collectionConfig?.admin?.meta || {}),
|
||||
...(collectionConfig?.admin?.components?.views?.edit?.version?.meta || {}),
|
||||
}
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
const entityLabel = getTranslation(globalConfig.label, i18n)
|
||||
title = `${t('version:version')}${formattedCreatedAt ? ` - ${formattedCreatedAt}` : ''}${entityLabel}`
|
||||
description = t('version:viewingVersionGlobal', { entityLabel })
|
||||
|
||||
metaToUse = {
|
||||
...(config.admin.meta || {}),
|
||||
description: t('version:viewingVersionGlobal', { entityLabel }),
|
||||
title: `${t('version:version')}${formattedCreatedAt ? ` - ${formattedCreatedAt}` : ''}${entityLabel}`,
|
||||
...(globalConfig?.admin?.meta || {}),
|
||||
...(globalConfig?.admin?.components?.views?.edit?.version?.meta || {}),
|
||||
}
|
||||
}
|
||||
|
||||
return meta({
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords,
|
||||
...metaToUse,
|
||||
serverURL: config.serverURL,
|
||||
title,
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import type { MetaConfig } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
@@ -20,34 +21,40 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
|
||||
? getTranslation(globalConfig.label, i18n)
|
||||
: ''
|
||||
|
||||
let title: string = ''
|
||||
let description: string = ''
|
||||
const keywords: string = ''
|
||||
let metaToUse: MetaConfig = {
|
||||
...(config.admin.meta || {}),
|
||||
}
|
||||
|
||||
const data: any = {} // TODO: figure this out
|
||||
|
||||
if (collectionConfig) {
|
||||
const useAsTitle = collectionConfig?.admin?.useAsTitle || 'id'
|
||||
const titleFromData = data?.[useAsTitle]
|
||||
title = `${t('version:versions')}${titleFromData ? ` - ${titleFromData}` : ''} - ${entityLabel}`
|
||||
description = t('version:viewingVersions', {
|
||||
documentTitle: data?.[useAsTitle],
|
||||
entitySlug: collectionConfig.slug,
|
||||
})
|
||||
|
||||
metaToUse = {
|
||||
...(config.admin.meta || {}),
|
||||
description: t('version:viewingVersions', {
|
||||
documentTitle: data?.[useAsTitle],
|
||||
entitySlug: collectionConfig.slug,
|
||||
}),
|
||||
title: `${t('version:versions')}${titleFromData ? ` - ${titleFromData}` : ''} - ${entityLabel}`,
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(collectionConfig?.admin?.components?.views?.edit?.versions?.meta || {}),
|
||||
}
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
title = `${t('version:versions')} - ${entityLabel}`
|
||||
description = t('version:viewingVersionsGlobal', { entitySlug: globalConfig.slug })
|
||||
metaToUse = {
|
||||
...(config.admin.meta || {}),
|
||||
description: t('version:viewingVersionsGlobal', { entitySlug: globalConfig.slug }),
|
||||
title: `${t('version:versions')} - ${entityLabel}`,
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin?.components?.views?.edit?.versions?.meta || {}),
|
||||
}
|
||||
}
|
||||
|
||||
return meta({
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords,
|
||||
...metaToUse,
|
||||
serverURL: config.serverURL,
|
||||
title,
|
||||
...(collectionConfig?.admin.meta || {}),
|
||||
...(globalConfig?.admin.meta || {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -206,6 +206,7 @@ export type { RowLabel, RowLabelComponent } from './forms/RowLabel.js'
|
||||
|
||||
export type {
|
||||
AdminViewComponent,
|
||||
AdminViewConfig,
|
||||
AdminViewProps,
|
||||
EditViewProps,
|
||||
InitPageResult,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Permissions } from '../../auth/index.js'
|
||||
import type { ImportMap } from '../../bin/generateImportMap/index.js'
|
||||
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
|
||||
import type { ClientConfig } from '../../config/client.js'
|
||||
import type { Locale, PayloadComponent } from '../../config/types.js'
|
||||
import type { Locale, MetaConfig, PayloadComponent } from '../../config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
|
||||
import type { PayloadRequest } from '../../types/index.js'
|
||||
import type { LanguageOptions } from '../LanguageOptions.js'
|
||||
@@ -14,6 +14,7 @@ export type AdminViewConfig = {
|
||||
Component: AdminViewComponent
|
||||
/** Whether the path should be matched exactly or as a prefix */
|
||||
exact?: boolean
|
||||
meta?: MetaConfig
|
||||
path?: string
|
||||
sensitive?: boolean
|
||||
strict?: boolean
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
GeneratePreviewURL,
|
||||
LabelFunction,
|
||||
LivePreviewConfig,
|
||||
MetaConfig,
|
||||
OpenGraphConfig,
|
||||
PayloadComponent,
|
||||
StaticLabel,
|
||||
@@ -329,10 +330,7 @@ export type CollectionAdminOptions = {
|
||||
* Live preview options
|
||||
*/
|
||||
livePreview?: LivePreviewConfig
|
||||
meta?: {
|
||||
description?: string
|
||||
openGraph?: OpenGraphConfig
|
||||
}
|
||||
meta?: MetaConfig
|
||||
pagination?: {
|
||||
defaultLimit?: number
|
||||
limits?: number[]
|
||||
|
||||
@@ -375,7 +375,9 @@ export type Endpoint = {
|
||||
|
||||
export type EditViewComponent = PayloadComponent<ServerSideEditViewProps>
|
||||
|
||||
export type EditViewConfig =
|
||||
export type EditViewConfig = {
|
||||
meta?: MetaConfig
|
||||
} & (
|
||||
| {
|
||||
Component: EditViewComponent
|
||||
path?: string
|
||||
@@ -393,6 +395,7 @@ export type EditViewConfig =
|
||||
*/
|
||||
tab?: DocumentTabConfig
|
||||
}
|
||||
)
|
||||
|
||||
export type ServerProps = {
|
||||
[key: string]: unknown
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
EntityDescriptionComponent,
|
||||
GeneratePreviewURL,
|
||||
LivePreviewConfig,
|
||||
MetaConfig,
|
||||
OpenGraphConfig,
|
||||
} from '../../config/types.js'
|
||||
import type { DBIdentifierName } from '../../database/types.js'
|
||||
@@ -128,10 +129,7 @@ export type GlobalAdminOptions = {
|
||||
* Live preview options
|
||||
*/
|
||||
livePreview?: LivePreviewConfig
|
||||
meta?: {
|
||||
description?: string
|
||||
openGraph?: OpenGraphConfig
|
||||
}
|
||||
meta?: MetaConfig
|
||||
/**
|
||||
* Function to generate custom preview URL
|
||||
*/
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
customCollectionMetaTitle,
|
||||
customCollectionParamViewPath,
|
||||
customCollectionParamViewPathBase,
|
||||
customDefaultTabMetaTitle,
|
||||
customEditLabel,
|
||||
customNestedTabViewPath,
|
||||
customTabLabel,
|
||||
customTabViewPath,
|
||||
customVersionsTabMetaTitle,
|
||||
customViewMetaTitle,
|
||||
} from '../shared.js'
|
||||
import { customViews2CollectionSlug } from '../slugs.js'
|
||||
|
||||
export const CustomViews2: CollectionConfig = {
|
||||
slug: customViews2CollectionSlug,
|
||||
admin: {
|
||||
meta: {
|
||||
title: customCollectionMetaTitle,
|
||||
},
|
||||
components: {
|
||||
views: {
|
||||
edit: {
|
||||
@@ -29,6 +36,9 @@ export const CustomViews2: CollectionConfig = {
|
||||
tab: {
|
||||
label: customEditLabel,
|
||||
},
|
||||
meta: {
|
||||
title: customDefaultTabMetaTitle,
|
||||
},
|
||||
},
|
||||
myCustomView: {
|
||||
Component: '/components/views/CustomTabLabel/index.js#CustomTabLabelView',
|
||||
@@ -37,6 +47,9 @@ export const CustomViews2: CollectionConfig = {
|
||||
label: customTabLabel,
|
||||
},
|
||||
path: '/custom-tab-view',
|
||||
meta: {
|
||||
title: customViewMetaTitle,
|
||||
},
|
||||
},
|
||||
myCustomViewWithCustomTab: {
|
||||
Component: '/components/views/CustomTabComponent/index.js#CustomTabComponentView',
|
||||
@@ -52,9 +65,15 @@ export const CustomViews2: CollectionConfig = {
|
||||
label: 'Custom Nested Tab View',
|
||||
},
|
||||
path: customNestedTabViewPath,
|
||||
meta: {
|
||||
title: 'Custom Nested Meta Title',
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
Component: '/components/views/CustomVersions/index.js#CustomVersionsView',
|
||||
meta: {
|
||||
title: customVersionsTabMetaTitle,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ export const CustomTabComponentClient: React.FC<{
|
||||
|
||||
const params = useParams()
|
||||
|
||||
const baseRoute = (params.segments.slice(0, 2) as string[]).join('/')
|
||||
const baseRoute = (params.segments.slice(0, 3) as string[]).join('/')
|
||||
|
||||
return <Link href={`${adminRoute}/${baseRoute}${path}`}>Custom Tab Component</Link>
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
customAdminRoutes,
|
||||
customNestedViewPath,
|
||||
customParamViewPath,
|
||||
customRootViewMetaTitle,
|
||||
customViewPath,
|
||||
} from './shared.js'
|
||||
export default buildConfigWithDefaults({
|
||||
@@ -60,6 +61,9 @@ export default buildConfigWithDefaults({
|
||||
CustomMinimalView: {
|
||||
Component: '/components/views/CustomMinimal/index.js#CustomMinimalView',
|
||||
path: '/custom-minimal-view',
|
||||
meta: {
|
||||
title: customRootViewMetaTitle,
|
||||
},
|
||||
},
|
||||
CustomNestedView: {
|
||||
Component: '/components/views/CustomViewNested/index.js#CustomNestedView',
|
||||
@@ -97,7 +101,7 @@ export default buildConfigWithDefaults({
|
||||
description: 'This is a custom OG description',
|
||||
title: 'This is a custom OG title',
|
||||
},
|
||||
titleSuffix: '- Custom CMS',
|
||||
titleSuffix: '- Custom Title Suffix',
|
||||
},
|
||||
routes: customAdminRoutes,
|
||||
},
|
||||
|
||||
@@ -21,14 +21,19 @@ import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||
import {
|
||||
customAdminRoutes,
|
||||
customCollectionMetaTitle,
|
||||
customDefaultTabMetaTitle,
|
||||
customEditLabel,
|
||||
customNestedTabViewPath,
|
||||
customNestedTabViewTitle,
|
||||
customNestedViewPath,
|
||||
customNestedViewTitle,
|
||||
customRootViewMetaTitle,
|
||||
customTabLabel,
|
||||
customTabViewPath,
|
||||
customTabViewTitle,
|
||||
customVersionsTabMetaTitle,
|
||||
customViewMetaTitle,
|
||||
customViewPath,
|
||||
customViewTitle,
|
||||
slugPluralLabel,
|
||||
@@ -120,102 +125,154 @@ describe('admin1', () => {
|
||||
})
|
||||
|
||||
describe('metadata', () => {
|
||||
test('should render custom page title suffix', async () => {
|
||||
await page.goto(`${serverURL}/admin`)
|
||||
await expect(page.title()).resolves.toMatch(/- Custom CMS$/)
|
||||
describe('root title and description', () => {
|
||||
test('should render custom page title suffix', async () => {
|
||||
await page.goto(`${serverURL}/admin`)
|
||||
await expect(page.title()).resolves.toMatch(/- Custom Title Suffix$/)
|
||||
})
|
||||
|
||||
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 fallback to root meta for custom root views', async () => {
|
||||
await page.goto(`${serverURL}/admin/custom-default-view`)
|
||||
await expect(page.title()).resolves.toMatch(/- Custom Title Suffix$/)
|
||||
})
|
||||
|
||||
test('should render custom meta title from custom root views', async () => {
|
||||
await page.goto(`${serverURL}/admin/custom-minimal-view`)
|
||||
const pattern = new RegExp(`^${customRootViewMetaTitle}`)
|
||||
await expect(page.title()).resolves.toMatch(pattern)
|
||||
})
|
||||
})
|
||||
|
||||
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/,
|
||||
)
|
||||
describe('favicons', () => {
|
||||
test('should render custom favicons', async () => {
|
||||
await page.goto(postsUrl.admin)
|
||||
const favicons = page.locator('link[rel="icon"]')
|
||||
|
||||
await expect(favicons).toHaveCount(2)
|
||||
await expect(favicons.nth(0)).toHaveAttribute(
|
||||
'href',
|
||||
/\/custom-favicon-dark(\.[a-z\d]+)?\.png/,
|
||||
)
|
||||
await expect(favicons.nth(1)).toHaveAttribute('media', '(prefers-color-scheme: dark)')
|
||||
await expect(favicons.nth(1)).toHaveAttribute(
|
||||
'href',
|
||||
/\/custom-favicon-light(\.[a-z\d]+)?\.png/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
describe('og meta', () => {
|
||||
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/,
|
||||
)
|
||||
})
|
||||
|
||||
await expect(page.locator('meta[name="description"]')).toHaveAttribute(
|
||||
'content',
|
||||
/This is a custom meta description for posts/,
|
||||
)
|
||||
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}`),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test('should render custom favicons', async () => {
|
||||
await page.goto(postsUrl.admin)
|
||||
const favicons = page.locator('link[rel="icon"]')
|
||||
describe('document meta', () => {
|
||||
test('should render custom meta title from collection config', async () => {
|
||||
await page.goto(customViewsURL.list)
|
||||
const pattern = new RegExp(`^${customCollectionMetaTitle}`)
|
||||
await expect(page.title()).resolves.toMatch(pattern)
|
||||
})
|
||||
|
||||
await expect(favicons).toHaveCount(2)
|
||||
await expect(favicons.nth(0)).toHaveAttribute(
|
||||
'href',
|
||||
/\/custom-favicon-dark(\.[a-z\d]+)?\.png/,
|
||||
)
|
||||
await expect(favicons.nth(1)).toHaveAttribute('media', '(prefers-color-scheme: dark)')
|
||||
await expect(favicons.nth(1)).toHaveAttribute(
|
||||
'href',
|
||||
/\/custom-favicon-light(\.[a-z\d]+)?\.png/,
|
||||
)
|
||||
})
|
||||
test('should render custom meta title from default edit view', async () => {
|
||||
await navigateToDoc(page, customViewsURL)
|
||||
const pattern = new RegExp(`^${customDefaultTabMetaTitle}`)
|
||||
await expect(page.title()).resolves.toMatch(pattern)
|
||||
})
|
||||
|
||||
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 meta title from nested edit view', async () => {
|
||||
await navigateToDoc(page, customViewsURL)
|
||||
await page.goto(`${page.url()}/versions`)
|
||||
const pattern = new RegExp(`^${customVersionsTabMetaTitle}`)
|
||||
await expect(page.title()).resolves.toMatch(pattern)
|
||||
})
|
||||
|
||||
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 meta title from nested custom view', async () => {
|
||||
await navigateToDoc(page, customViewsURL)
|
||||
await page.goto(`${page.url()}/custom-tab-view`)
|
||||
const pattern = new RegExp(`^${customViewMetaTitle}`)
|
||||
await expect(page.title()).resolves.toMatch(pattern)
|
||||
})
|
||||
|
||||
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}`),
|
||||
)
|
||||
test('should render fallback meta title from nested custom view', async () => {
|
||||
await navigateToDoc(page, customViewsURL)
|
||||
await page.goto(`${page.url()}${customTabViewPath}`)
|
||||
const pattern = new RegExp(`^${customCollectionMetaTitle}`)
|
||||
await expect(page.title()).resolves.toMatch(pattern)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export const customEditLabel = 'Custom Edit Label'
|
||||
export const customTabLabel = 'Custom Tab Label'
|
||||
|
||||
export const customTabViewPath = '/custom-tab-component'
|
||||
|
||||
export const customTabViewTitle = 'Custom View With Tab Component'
|
||||
|
||||
export const customTabLabelViewTitle = 'Custom Tab Label View'
|
||||
@@ -31,6 +32,16 @@ export const customTabViewComponentTitle = 'Custom View With Tab Component'
|
||||
|
||||
export const customNestedTabViewPath = `${customTabViewPath}/nested-view`
|
||||
|
||||
export const customCollectionMetaTitle = 'Custom Meta Title'
|
||||
|
||||
export const customRootViewMetaTitle = 'Custom Root View Meta Title'
|
||||
|
||||
export const customDefaultTabMetaTitle = 'Custom Default Tab Meta Title'
|
||||
|
||||
export const customVersionsTabMetaTitle = 'Custom Versions Tab Meta Title'
|
||||
|
||||
export const customViewMetaTitle = 'Custom Tab Meta Title'
|
||||
|
||||
export const customNestedTabViewTitle = 'Custom Nested Tab View'
|
||||
|
||||
export const customCollectionParamViewPathBase = '/custom-param'
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": [
|
||||
"./test/_community/config.ts"
|
||||
"./test/admin/config.ts"
|
||||
],
|
||||
"@payloadcms/live-preview": [
|
||||
"./packages/live-preview/src"
|
||||
|
||||
Reference in New Issue
Block a user