feat: custom view and document-level metadata (#7716)

This commit is contained in:
Jacob Fletcher
2024-08-18 23:22:38 -04:00
committed by GitHub
parent 2835e1d709
commit a526c7becd
25 changed files with 645 additions and 281 deletions

View File

@@ -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,
},
},
},
},

View File

@@ -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>
}

View File

@@ -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,
},

View File

@@ -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)
})
})
})

View File

@@ -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'