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