feat: custom view and document-level metadata (#7716)
This commit is contained in:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user