feat(plugin-seo): export fields from plugin seo so that they can be imported freely in a collection fields config (#6996)
Exports the fields from the SEO plugin so that they can be used anywhere
inside a collection, new exports:
```ts
import { MetaDescriptionField, MetaImageField, MetaTitleField, OverviewField, PreviewField } from '@payloadcms/plugin-seo/fields'
// Used as fields
MetaImageField({
relationTo: 'media',
hasGenerateFn: true,
})
MetaDescriptionField({
hasGenerateFn: true,
})
MetaTitleField({
hasGenerateFn: true,
})
PreviewField({
hasGenerateFn: true,
titlePath: 'meta.title',
descriptionPath: 'meta.description',
})
OverviewField({
titlePath: 'meta.title',
descriptionPath: 'meta.description',
imagePath: 'meta.image',
})
```
This commit is contained in:
@@ -68,7 +68,7 @@ const config = buildConfig({
|
||||
'pages',
|
||||
],
|
||||
uploadsCollection: 'media',
|
||||
generateTitle: ({ doc }) => `Website.com — ${doc.title.value}`,
|
||||
generateTitle: ({ doc }) => `Website.com — ${doc.title}`,
|
||||
generateDescription: ({ doc }) => doc.excerpt
|
||||
})
|
||||
]
|
||||
@@ -119,7 +119,7 @@ A function that allows you to return any meta title, including from document's c
|
||||
{
|
||||
// ...
|
||||
seoPlugin({
|
||||
generateTitle: ({ ...docInfo, doc, locale }) => `Website.com — ${doc?.title?.value}`,
|
||||
generateTitle: ({ ...docInfo, doc, locale }) => `Website.com — ${doc?.title}`,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -133,7 +133,7 @@ A function that allows you to return any meta description, including from docume
|
||||
{
|
||||
// ...
|
||||
seoPlugin({
|
||||
generateDescription: ({ ...docInfo, doc, locale }) => doc?.excerpt?.value,
|
||||
generateDescription: ({ ...docInfo, doc, locale }) => doc?.excerpt,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -147,7 +147,7 @@ A function that allows you to return any meta image, including from document's c
|
||||
{
|
||||
// ...
|
||||
seoPlugin({
|
||||
generateImage: ({ ...docInfo, doc, locale }) => doc?.featuredImage?.value,
|
||||
generateImage: ({ ...docInfo, doc, locale }) => doc?.featuredImage,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -162,7 +162,7 @@ A function called by the search preview component to display the actual URL of y
|
||||
// ...
|
||||
seoPlugin({
|
||||
generateURL: ({ ...docInfo, doc, locale }) =>
|
||||
`https://yoursite.com/${collection?.slug}/${doc?.slug?.value}`,
|
||||
`https://yoursite.com/${collection?.slug}/${doc?.slug}`,
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -200,6 +200,54 @@ seoPlugin({
|
||||
})
|
||||
```
|
||||
|
||||
## Direct use of fields
|
||||
|
||||
There is the option to directly import any of the fields from the plugin so that you can include them anywhere as needed.
|
||||
|
||||
<Banner type="info">
|
||||
You will still need to configure the plugin in the Payload config in order to configure the generation functions.
|
||||
Since these fields are imported and used directly, they don't have access to the plugin config so they may need additional arguments to work the same way.
|
||||
</Banner>
|
||||
|
||||
```ts
|
||||
import { MetaDescriptionField, MetaImageField, MetaTitleField, OverviewField, PreviewField } from '@payloadcms/plugin-seo/fields'
|
||||
|
||||
// Used as fields
|
||||
MetaImageField({
|
||||
// the upload collection slug
|
||||
relationTo: 'media',
|
||||
|
||||
// if the `generateImage` function is configured
|
||||
hasGenerateFn: true,
|
||||
})
|
||||
|
||||
MetaDescriptionField({
|
||||
// if the `generateDescription` function is configured
|
||||
hasGenerateFn: true,
|
||||
})
|
||||
|
||||
MetaTitleField({
|
||||
// if the `generateTitle` function is configured
|
||||
hasGenerateFn: true,
|
||||
})
|
||||
|
||||
PreviewField({
|
||||
// if the `generateUrl` function is configured
|
||||
hasGenerateFn: true,
|
||||
|
||||
// field paths to match the target field for data
|
||||
titlePath: 'meta.title',
|
||||
descriptionPath: 'meta.description',
|
||||
})
|
||||
|
||||
OverviewField({
|
||||
// field paths to match the target field for data
|
||||
titlePath: 'meta.title',
|
||||
descriptionPath: 'meta.description',
|
||||
imagePath: 'meta.image',
|
||||
})
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
All types can be directly imported:
|
||||
@@ -213,6 +261,18 @@ import {
|
||||
} from '@payloadcms/plugin-seo/types';
|
||||
```
|
||||
|
||||
You can then pass the collections from your generated Payload types into the generation types, for example:
|
||||
|
||||
```ts
|
||||
import { Page } from './payload-types.ts';
|
||||
|
||||
import { GenerateTitle } from '@payloadcms/plugin-seo/types';
|
||||
|
||||
const generateTitle: GenerateTitle<Page> = async ({ doc, locale }) => {
|
||||
return `Website.com — ${doc?.title}`
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) contains an official [Website Template](https://github.com/payloadcms/payload/tree/main/templates/website) and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommere) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end.
|
||||
|
||||
@@ -29,6 +29,11 @@
|
||||
"import": "./src/exports/types.ts",
|
||||
"types": "./src/exports/types.ts",
|
||||
"default": "./src/exports/types.ts"
|
||||
},
|
||||
"./fields": {
|
||||
"import": "./src/exports/fields.ts",
|
||||
"types": "./src/exports/fields.ts",
|
||||
"default": "./src/exports/fields.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.tsx",
|
||||
@@ -73,6 +78,11 @@
|
||||
"import": "./dist/exports/types.js",
|
||||
"types": "./dist/exports/types.d.ts",
|
||||
"default": "./dist/exports/types.js"
|
||||
},
|
||||
"./fields": {
|
||||
"import": "./dist/exports/fields.js",
|
||||
"types": "./dist/exports/fields.d.ts",
|
||||
"default": "./dist/exports/fields.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
|
||||
5
packages/plugin-seo/src/exports/fields.ts
Normal file
5
packages/plugin-seo/src/exports/fields.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { MetaDescriptionField } from '../fields/MetaDescription/index.js'
|
||||
export { MetaImageField } from '../fields/MetaImage/index.js'
|
||||
export { MetaTitleField } from '../fields/MetaTitle/index.js'
|
||||
export { OverviewField } from '../fields/Overview/index.js'
|
||||
export { PreviewField } from '../fields/Preview/index.js'
|
||||
@@ -14,11 +14,11 @@ import {
|
||||
} from '@payloadcms/ui'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
||||
import type { GenerateDescription } from '../types.js'
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js'
|
||||
import type { GenerateDescription } from '../../types.js'
|
||||
|
||||
import { defaults } from '../defaults.js'
|
||||
import { LengthIndicator } from '../ui/LengthIndicator.js'
|
||||
import { defaults } from '../../defaults.js'
|
||||
import { LengthIndicator } from '../../ui/LengthIndicator.js'
|
||||
|
||||
const { maxLength, minLength } = defaults.description
|
||||
|
||||
@@ -28,8 +28,8 @@ type MetaDescriptionProps = FormFieldBase & {
|
||||
path: string
|
||||
}
|
||||
|
||||
export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
|
||||
const { CustomLabel, hasGenerateDescriptionFn, label, labelProps, path, required } = props
|
||||
export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props) => {
|
||||
const { CustomLabel, hasGenerateDescriptionFn, label, labelProps, required } = props
|
||||
const { path: pathFromContext } = useFieldProps()
|
||||
|
||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||
@@ -39,7 +39,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
|
||||
const docInfo = useDocumentInfo()
|
||||
|
||||
const field: FieldType<string> = useField({
|
||||
path,
|
||||
path: pathFromContext,
|
||||
} as Options)
|
||||
|
||||
const { errorMessage, setValue, showError, value } = field
|
||||
35
packages/plugin-seo/src/fields/MetaDescription/index.ts
Normal file
35
packages/plugin-seo/src/fields/MetaDescription/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { TextareaField } from 'payload'
|
||||
|
||||
import { withMergedProps } from '@payloadcms/ui/shared'
|
||||
|
||||
import { MetaDescriptionComponent } from './MetaDescriptionComponent.js'
|
||||
|
||||
interface FieldFunctionProps {
|
||||
/**
|
||||
* Tell the component if the generate function is available as configured in the plugin config
|
||||
*/
|
||||
hasGenerateFn?: boolean
|
||||
overrides?: Partial<TextareaField>
|
||||
}
|
||||
|
||||
type FieldFunction = ({ hasGenerateFn, overrides }: FieldFunctionProps) => TextareaField
|
||||
|
||||
export const MetaDescriptionField: FieldFunction = ({ hasGenerateFn = false, overrides }) => {
|
||||
return {
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
admin: {
|
||||
components: {
|
||||
Field: withMergedProps({
|
||||
Component: MetaDescriptionComponent,
|
||||
sanitizeServerOnlyProps: true,
|
||||
toMergeIntoProps: {
|
||||
hasGenerateDescriptionFn: hasGenerateFn,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
localized: true,
|
||||
...((overrides as unknown as TextareaField) ?? {}),
|
||||
}
|
||||
}
|
||||
@@ -8,26 +8,28 @@ import {
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useField,
|
||||
useFieldProps,
|
||||
useForm,
|
||||
useLocale,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
||||
import type { GenerateImage } from '../types.js'
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js'
|
||||
import type { GenerateImage } from '../../types.js'
|
||||
|
||||
import { Pill } from '../ui/Pill.js'
|
||||
import { Pill } from '../../ui/Pill.js'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
type MetaImageProps = UploadInputProps & {
|
||||
hasGenerateImageFn: boolean
|
||||
}
|
||||
|
||||
export const MetaImage: React.FC<MetaImageProps> = (props) => {
|
||||
export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
|
||||
const { CustomLabel, hasGenerateImageFn, label, labelProps, relationTo, required } = props || {}
|
||||
const { path: pathFromContext } = useFieldProps()
|
||||
|
||||
const field: FieldType<string> = useField(props as Options)
|
||||
const field: FieldType<string> = useField({ ...props, path: pathFromContext } as Options)
|
||||
|
||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||
|
||||
39
packages/plugin-seo/src/fields/MetaImage/index.ts
Normal file
39
packages/plugin-seo/src/fields/MetaImage/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { UploadField } from 'payload'
|
||||
|
||||
import { withMergedProps } from '@payloadcms/ui/shared'
|
||||
|
||||
import { MetaImageComponent } from './MetaImageComponent.js'
|
||||
|
||||
interface FieldFunctionProps {
|
||||
/**
|
||||
* Tell the component if the generate function is available as configured in the plugin config
|
||||
*/
|
||||
hasGenerateFn?: boolean
|
||||
overrides?: Partial<UploadField>
|
||||
relationTo: string
|
||||
}
|
||||
|
||||
type FieldFunction = ({ hasGenerateFn, overrides }: FieldFunctionProps) => UploadField
|
||||
|
||||
export const MetaImageField: FieldFunction = ({ hasGenerateFn = false, overrides, relationTo }) => {
|
||||
return {
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
admin: {
|
||||
components: {
|
||||
Field: withMergedProps({
|
||||
Component: MetaImageComponent,
|
||||
sanitizeServerOnlyProps: true,
|
||||
toMergeIntoProps: {
|
||||
hasGenerateImageFn: hasGenerateFn,
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Maximum upload file size: 12MB. Recommended file size for images is <500KB.',
|
||||
},
|
||||
label: 'Meta Image',
|
||||
localized: true,
|
||||
relationTo,
|
||||
...((overrides as unknown as UploadField) ?? {}),
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,12 @@ import {
|
||||
} from '@payloadcms/ui'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
||||
import type { GenerateTitle } from '../types.js'
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js'
|
||||
import type { GenerateTitle } from '../../types.js'
|
||||
|
||||
import { defaults } from '../defaults.js'
|
||||
import { LengthIndicator } from '../ui/LengthIndicator.js'
|
||||
import './index.scss'
|
||||
import { defaults } from '../../defaults.js'
|
||||
import { LengthIndicator } from '../../ui/LengthIndicator.js'
|
||||
import '../index.scss'
|
||||
|
||||
const { maxLength, minLength } = defaults.title
|
||||
|
||||
@@ -28,14 +28,14 @@ type MetaTitleProps = FormFieldBase & {
|
||||
hasGenerateTitleFn: boolean
|
||||
}
|
||||
|
||||
export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
|
||||
const { CustomLabel, hasGenerateTitleFn, label, labelProps, path, required } = props || {}
|
||||
export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
|
||||
const { CustomLabel, hasGenerateTitleFn, label, labelProps, required } = props || {}
|
||||
const { path: pathFromContext } = useFieldProps()
|
||||
|
||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||
|
||||
const field: FieldType<string> = useField({
|
||||
path,
|
||||
path: pathFromContext,
|
||||
} as Options)
|
||||
|
||||
const locale = useLocale()
|
||||
35
packages/plugin-seo/src/fields/MetaTitle/index.ts
Normal file
35
packages/plugin-seo/src/fields/MetaTitle/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { TextField } from 'payload'
|
||||
|
||||
import { withMergedProps } from '@payloadcms/ui/shared'
|
||||
|
||||
import { MetaTitleComponent } from './MetaTitleComponent.js'
|
||||
|
||||
interface FieldFunctionProps {
|
||||
/**
|
||||
* Tell the component if the generate function is available as configured in the plugin config
|
||||
*/
|
||||
hasGenerateFn?: boolean
|
||||
overrides?: Partial<TextField>
|
||||
}
|
||||
|
||||
type FieldFunction = ({ hasGenerateFn, overrides }: FieldFunctionProps) => TextField
|
||||
|
||||
export const MetaTitleField: FieldFunction = ({ hasGenerateFn = false, overrides }) => {
|
||||
return {
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Field: withMergedProps({
|
||||
Component: MetaTitleComponent,
|
||||
sanitizeServerOnlyProps: true,
|
||||
toMergeIntoProps: {
|
||||
hasGenerateTitleFn: hasGenerateFn,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
localized: true,
|
||||
...((overrides as unknown as TextField) ?? {}),
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,44 @@
|
||||
'use client'
|
||||
|
||||
import type { FormField } from 'payload'
|
||||
import type { FormField, UIField } from 'payload'
|
||||
|
||||
import { useAllFormFields, useForm, useTranslation } from '@payloadcms/ui'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js'
|
||||
|
||||
import { defaults } from '../defaults.js'
|
||||
import { defaults } from '../../defaults.js'
|
||||
|
||||
const {
|
||||
description: { maxLength: maxDesc, minLength: minDesc },
|
||||
title: { maxLength: maxTitle, minLength: minTitle },
|
||||
} = defaults
|
||||
|
||||
export const Overview: React.FC = () => {
|
||||
type OverviewProps = UIField & {
|
||||
descriptionPath?: string
|
||||
imagePath?: string
|
||||
titlePath?: string
|
||||
}
|
||||
|
||||
export const OverviewComponent: React.FC<OverviewProps> = ({
|
||||
descriptionPath: descriptionPathFromContext,
|
||||
imagePath: imagePathFromContext,
|
||||
titlePath: titlePathFromContext,
|
||||
}) => {
|
||||
const {
|
||||
// dispatchFields,
|
||||
getFields,
|
||||
} = useForm()
|
||||
|
||||
const descriptionPath = descriptionPathFromContext || 'meta.description'
|
||||
const titlePath = titlePathFromContext || 'meta.title'
|
||||
const imagePath = imagePathFromContext || 'meta.image'
|
||||
|
||||
const [
|
||||
{
|
||||
'meta.description': { value: metaDesc } = {} as FormField,
|
||||
'meta.image': { value: metaImage } = {} as FormField,
|
||||
'meta.title': { value: metaTitle } = {} as FormField,
|
||||
[descriptionPath]: { value: metaDesc } = {} as FormField,
|
||||
[imagePath]: { value: metaImage } = {} as FormField,
|
||||
[titlePath]: { value: metaTitle } = {} as FormField,
|
||||
},
|
||||
] = useAllFormFields()
|
||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||
56
packages/plugin-seo/src/fields/Overview/index.tsx
Normal file
56
packages/plugin-seo/src/fields/Overview/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { UIField } from 'payload'
|
||||
|
||||
import { withMergedProps } from '@payloadcms/ui/shared'
|
||||
|
||||
import { OverviewComponent } from './OverviewComponent.js'
|
||||
|
||||
interface FieldFunctionProps {
|
||||
/**
|
||||
* Path to the description field to use for the preview
|
||||
*
|
||||
* @default 'meta.description'
|
||||
*/
|
||||
descriptionPath?: string
|
||||
/**
|
||||
* Path to the image field to use for the preview
|
||||
*
|
||||
* @default 'meta.image'
|
||||
*/
|
||||
imagePath?: string
|
||||
overrides?: Partial<UIField>
|
||||
/**
|
||||
* Path to the title field to use for the preview
|
||||
*
|
||||
* @default 'meta.title'
|
||||
*/
|
||||
titlePath?: string
|
||||
}
|
||||
|
||||
type FieldFunction = ({ overrides }: FieldFunctionProps) => UIField
|
||||
|
||||
export const OverviewField: FieldFunction = ({
|
||||
descriptionPath,
|
||||
imagePath,
|
||||
overrides,
|
||||
titlePath,
|
||||
}) => {
|
||||
return {
|
||||
name: 'overview',
|
||||
type: 'ui',
|
||||
admin: {
|
||||
components: {
|
||||
Field: withMergedProps({
|
||||
Component: OverviewComponent,
|
||||
sanitizeServerOnlyProps: true,
|
||||
toMergeIntoProps: {
|
||||
descriptionPath,
|
||||
imagePath,
|
||||
titlePath,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
label: 'Overview',
|
||||
...((overrides as unknown as UIField) ?? {}),
|
||||
}
|
||||
}
|
||||
@@ -9,16 +9,23 @@ import {
|
||||
useLocale,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { get } from 'http'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../translations/index.js'
|
||||
import type { GenerateURL } from '../types.js'
|
||||
import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../translations/index.js'
|
||||
import type { GenerateURL } from '../../types.js'
|
||||
|
||||
type PreviewProps = UIField & {
|
||||
descriptionPath?: string
|
||||
hasGenerateURLFn: boolean
|
||||
titlePath?: string
|
||||
}
|
||||
|
||||
export const Preview: React.FC<PreviewProps> = ({ hasGenerateURLFn }) => {
|
||||
export const PreviewComponent: React.FC<PreviewProps> = ({
|
||||
descriptionPath: descriptionPathFromContext,
|
||||
hasGenerateURLFn,
|
||||
titlePath: titlePathFromContext,
|
||||
}) => {
|
||||
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
|
||||
|
||||
const locale = useLocale()
|
||||
@@ -26,9 +33,12 @@ export const Preview: React.FC<PreviewProps> = ({ hasGenerateURLFn }) => {
|
||||
const { getData } = useForm()
|
||||
const docInfo = useDocumentInfo()
|
||||
|
||||
const descriptionPath = descriptionPathFromContext || 'meta.description'
|
||||
const titlePath = titlePathFromContext || 'meta.title'
|
||||
|
||||
const {
|
||||
'meta.description': { value: metaDescription } = {} as FormField,
|
||||
'meta.title': { value: metaTitle } = {} as FormField,
|
||||
[descriptionPath]: { value: metaDescription } = {} as FormField,
|
||||
[titlePath]: { value: metaTitle } = {} as FormField,
|
||||
} = fields
|
||||
|
||||
const [href, setHref] = useState<string>()
|
||||
54
packages/plugin-seo/src/fields/Preview/index.tsx
Normal file
54
packages/plugin-seo/src/fields/Preview/index.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { UIField } from 'payload'
|
||||
|
||||
import { withMergedProps } from '@payloadcms/ui/shared'
|
||||
|
||||
import { PreviewComponent } from './PreviewComponent.js'
|
||||
|
||||
interface FieldFunctionProps {
|
||||
/**
|
||||
* Path to the description field to use for the preview
|
||||
*
|
||||
* @default 'meta.description'
|
||||
*/
|
||||
descriptionPath?: string
|
||||
/**
|
||||
* Tell the component if the generate function is available as configured in the plugin config
|
||||
*/
|
||||
hasGenerateFn?: boolean
|
||||
overrides?: Partial<UIField>
|
||||
/**
|
||||
* Path to the title field to use for the preview
|
||||
*
|
||||
* @default 'meta.title'
|
||||
*/
|
||||
titlePath?: string
|
||||
}
|
||||
|
||||
type FieldFunction = ({ hasGenerateFn, overrides }: FieldFunctionProps) => UIField
|
||||
|
||||
export const PreviewField: FieldFunction = ({
|
||||
descriptionPath,
|
||||
hasGenerateFn = false,
|
||||
overrides,
|
||||
titlePath,
|
||||
}) => {
|
||||
return {
|
||||
name: 'preview',
|
||||
type: 'ui',
|
||||
admin: {
|
||||
components: {
|
||||
Field: withMergedProps({
|
||||
Component: PreviewComponent,
|
||||
sanitizeServerOnlyProps: true,
|
||||
toMergeIntoProps: {
|
||||
descriptionPath,
|
||||
hasGenerateURLFn: hasGenerateFn,
|
||||
titlePath,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
label: 'Preview',
|
||||
...((overrides as unknown as UIField) ?? {}),
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,12 @@ import type {
|
||||
SEOPluginConfig,
|
||||
} from './types.js'
|
||||
|
||||
import { MetaDescription } from './fields/MetaDescription.js'
|
||||
import { MetaImage } from './fields/MetaImage.js'
|
||||
import { MetaTitle } from './fields/MetaTitle.js'
|
||||
import { MetaDescriptionComponent } from './fields/MetaDescription/MetaDescriptionComponent.js'
|
||||
import { MetaImageComponent } from './fields/MetaImage/MetaImageComponent.js'
|
||||
import { MetaTitleComponent } from './fields/MetaTitle/MetaTitleComponent.js'
|
||||
import { OverviewComponent } from './fields/Overview/OverviewComponent.js'
|
||||
import { PreviewComponent } from './fields/Preview/PreviewComponent.js'
|
||||
import { translations } from './translations/index.js'
|
||||
import { Overview } from './ui/Overview.js'
|
||||
import { Preview } from './ui/Preview.js'
|
||||
|
||||
export const seoPlugin =
|
||||
(pluginConfig: SEOPluginConfig) =>
|
||||
@@ -32,7 +32,7 @@ export const seoPlugin =
|
||||
type: 'ui',
|
||||
admin: {
|
||||
components: {
|
||||
Field: Overview,
|
||||
Field: OverviewComponent,
|
||||
},
|
||||
},
|
||||
label: 'Overview',
|
||||
@@ -43,7 +43,7 @@ export const seoPlugin =
|
||||
admin: {
|
||||
components: {
|
||||
Field: withMergedProps({
|
||||
Component: MetaTitle,
|
||||
Component: MetaTitleComponent,
|
||||
sanitizeServerOnlyProps: true,
|
||||
toMergeIntoProps: {
|
||||
hasGenerateTitleFn: typeof pluginConfig?.generateTitle === 'function',
|
||||
@@ -60,7 +60,7 @@ export const seoPlugin =
|
||||
admin: {
|
||||
components: {
|
||||
Field: withMergedProps({
|
||||
Component: MetaDescription,
|
||||
Component: MetaDescriptionComponent,
|
||||
sanitizeServerOnlyProps: true,
|
||||
toMergeIntoProps: {
|
||||
hasGenerateDescriptionFn:
|
||||
@@ -81,7 +81,7 @@ export const seoPlugin =
|
||||
admin: {
|
||||
components: {
|
||||
Field: withMergedProps({
|
||||
Component: MetaImage,
|
||||
Component: MetaImageComponent,
|
||||
sanitizeServerOnlyProps: true,
|
||||
toMergeIntoProps: {
|
||||
hasGenerateImageFn: typeof pluginConfig?.generateImage === 'function',
|
||||
@@ -105,7 +105,7 @@ export const seoPlugin =
|
||||
admin: {
|
||||
components: {
|
||||
Field: withMergedProps({
|
||||
Component: Preview,
|
||||
Component: PreviewComponent,
|
||||
sanitizeServerOnlyProps: true,
|
||||
toMergeIntoProps: {
|
||||
hasGenerateURLFn: typeof pluginConfig?.generateURL === 'function',
|
||||
|
||||
98
test/plugin-seo/collections/PagesWithImportedFields.ts
Normal file
98
test/plugin-seo/collections/PagesWithImportedFields.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
MetaDescriptionField,
|
||||
MetaImageField,
|
||||
MetaTitleField,
|
||||
OverviewField,
|
||||
PreviewField,
|
||||
} from '@payloadcms/plugin-seo/fields'
|
||||
|
||||
import { pagesWithImportedFieldsSlug } from '../shared.js'
|
||||
|
||||
export const PagesWithImportedFields: CollectionConfig = {
|
||||
slug: pagesWithImportedFieldsSlug,
|
||||
labels: {
|
||||
singular: 'Page with imported fields',
|
||||
plural: 'Pages with imported fields',
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
OverviewField({
|
||||
titlePath: 'metaAndSEO.title',
|
||||
descriptionPath: 'metaAndSEO.innerMeta.description',
|
||||
imagePath: 'metaAndSEO.innerMedia.image',
|
||||
}),
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'General',
|
||||
fields: [
|
||||
{
|
||||
name: 'excerpt',
|
||||
label: 'Excerpt',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
// NOTE: in order for position: 'sidebar' to work here,
|
||||
// the first field of this config must be of type `tabs`,
|
||||
// and this field must be a sibling of it
|
||||
// See `./Posts` or the `../../README.md` for more info
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Meta',
|
||||
name: 'metaAndSEO',
|
||||
fields: [
|
||||
MetaTitleField({
|
||||
hasGenerateFn: true,
|
||||
}),
|
||||
PreviewField({
|
||||
hasGenerateFn: true,
|
||||
titlePath: 'metaAndSEO.title',
|
||||
descriptionPath: 'metaAndSEO.innerMeta.description',
|
||||
}),
|
||||
{
|
||||
type: 'group',
|
||||
name: 'innerMeta',
|
||||
fields: [
|
||||
MetaDescriptionField({
|
||||
hasGenerateFn: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'innerMedia',
|
||||
fields: [
|
||||
MetaImageField({
|
||||
relationTo: 'media',
|
||||
hasGenerateFn: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { Media } from './collections/Media.js'
|
||||
import { Pages } from './collections/Pages.js'
|
||||
import { PagesWithImportedFields } from './collections/PagesWithImportedFields.js'
|
||||
import { Users } from './collections/Users.js'
|
||||
import { seed } from './seed/index.js'
|
||||
|
||||
@@ -29,7 +30,7 @@ const generateURL: GenerateURL<Page> = ({ doc, locale }) => {
|
||||
}
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [Users, Pages, Media],
|
||||
collections: [Users, Pages, Media, PagesWithImportedFields],
|
||||
i18n: {
|
||||
supportedLanguages: {
|
||||
en,
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface Config {
|
||||
users: User;
|
||||
pages: Page;
|
||||
media: Media;
|
||||
pagesWithImportedFields: PagesWithImportedField;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
@@ -90,6 +91,28 @@ export interface Media {
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pagesWithImportedFields".
|
||||
*/
|
||||
export interface PagesWithImportedField {
|
||||
id: string;
|
||||
title: string;
|
||||
excerpt?: string | null;
|
||||
slug: string;
|
||||
metaAndSEO?: {
|
||||
title?: string | null;
|
||||
innerMeta?: {
|
||||
description?: string | null;
|
||||
};
|
||||
innerMedia?: {
|
||||
image?: string | Media | null;
|
||||
};
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export const pagesSlug = 'pages'
|
||||
|
||||
export const pagesWithImportedFieldsSlug = 'pagesWithImportedFields'
|
||||
|
||||
export const mediaSlug = 'media'
|
||||
|
||||
Reference in New Issue
Block a user