diff --git a/docs/plugins/seo.mdx b/docs/plugins/seo.mdx index 29bc06c9c..627dfa9fa 100644 --- a/docs/plugins/seo.mdx +++ b/docs/plugins/seo.mdx @@ -89,12 +89,23 @@ An array of global slugs to enable SEO. Enabled globals receive a `meta` field w ##### `fields` -An array of fields that allows you to inject your own custom fields onto the `meta` field group. The following fields are provided by default: +A function that takes in the default fields via an object and expects an array of fields in return. You can use this to modify existing fields or add new ones. -- `title`: text -- `description`: textarea -- `image`: upload (if an `uploadsCollection` is provided) -- `preview`: ui +```ts +// payload.config.ts +{ + // ... + seoPlugin({ + fields: ({ defaultFields }) => [ + ...defaultFields, + { + name: 'customField', + type: 'text', + } + ] + }) +} +``` ##### `uploadsCollection` @@ -209,25 +220,6 @@ Rename the meta group interface name that is generated for TypeScript and GraphQ } ``` -#### `fieldOverrides` - -Pass any valid field props to the base fields: Title, Description or Image. - -```ts -// payload.config.ts -seoPlugin({ - // ... - fieldOverrides: { - title: { - required: true, - }, - description: { - localized: true, - }, - }, -}) -``` - ## 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. diff --git a/packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx b/packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx index bb24c7ead..56b021e45 100644 --- a/packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx +++ b/packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx @@ -32,13 +32,14 @@ export const MetaDescriptionComponent: React.FC = (props) const { field: { admin: { - components: { Label }, + components: { afterInput, beforeInput, Label }, }, label, maxLength: maxLengthFromProps, minLength: minLengthFromProps, required, }, + field: fieldFromProps, hasGenerateDescriptionFn, labelProps, } = props @@ -132,7 +133,7 @@ export const MetaDescriptionComponent: React.FC = (props) >
= (props) }} > = (props) => { relationTo, required, }, + field: fieldFromProps, hasGenerateImageFn, labelProps, } = props || {} @@ -125,7 +126,7 @@ export const MetaImageComponent: React.FC = (props) => { >
= (props) => { const { field: { admin: { - components: { Label }, + components: { afterInput, beforeInput, Label }, }, label, maxLength: maxLengthFromProps, @@ -182,6 +182,8 @@ export const MetaTitleComponent: React.FC = (props) => { }} > (config: Config): Config => { + const defaultFields: Field[] = [ + OverviewField({}), + MetaTitleField({ + hasGenerateFn: typeof pluginConfig?.generateTitle === 'function', + }), + MetaDescriptionField({ + hasGenerateFn: typeof pluginConfig?.generateDescription === 'function', + }), + ...(pluginConfig?.uploadsCollection + ? [ + MetaImageField({ + hasGenerateFn: typeof pluginConfig?.generateImage === 'function', + relationTo: pluginConfig.uploadsCollection, + }), + ] + : []), + PreviewField({ + hasGenerateFn: typeof pluginConfig?.generateURL === 'function', + }), + ] + const seoFields: GroupField[] = [ { name: 'meta', type: 'group', fields: [ - OverviewField({}), - MetaTitleField({ - hasGenerateFn: typeof pluginConfig?.generateTitle === 'function', - overrides: pluginConfig?.fieldOverrides?.title, - }), - MetaDescriptionField({ - hasGenerateFn: typeof pluginConfig?.generateDescription === 'function', - overrides: pluginConfig?.fieldOverrides?.description, - }), - ...(pluginConfig?.uploadsCollection - ? [ - MetaImageField({ - hasGenerateFn: typeof pluginConfig?.generateImage === 'function', - overrides: pluginConfig?.fieldOverrides?.image, - relationTo: pluginConfig.uploadsCollection, - }), - ] - : []), - ...(pluginConfig?.fields || []), - PreviewField({ - hasGenerateFn: typeof pluginConfig?.generateURL === 'function', - }), + ...(pluginConfig?.fields && typeof pluginConfig.fields === 'function' + ? pluginConfig.fields({ defaultFields }) + : defaultFields), ], interfaceName: pluginConfig.interfaceName, label: 'SEO', diff --git a/packages/plugin-seo/src/types.ts b/packages/plugin-seo/src/types.ts index f160557b3..76746eaa9 100644 --- a/packages/plugin-seo/src/types.ts +++ b/packages/plugin-seo/src/types.ts @@ -1,13 +1,7 @@ import type { DocumentInfoContext } from '@payloadcms/ui' -import type { - CollectionConfig, - Field, - GlobalConfig, - PayloadRequest, - TextareaField, - TextField, - UploadField, -} from 'payload' +import type { CollectionConfig, Field, GlobalConfig, PayloadRequest } from 'payload' + +export type FieldsOverride = (args: { defaultFields: Field[] }) => Field[] export type PartialDocumentInfoContext = Pick< DocumentInfoContext, @@ -66,20 +60,37 @@ export type GenerateURL = ( ) => Promise | string export type SEOPluginConfig = { + /** + * Collections to include the SEO fields in + */ collections?: string[] - fieldOverrides?: { - description?: Partial - image?: Partial - title?: Partial - } - fields?: Field[] + /** + * Override the default fields inserted by the SEO plugin via a function that receives the default fields and returns the new fields + * + * If you need more flexibility you can insert the fields manually as needed. @link https://payloadcms.com/docs/beta/plugins/seo#direct-use-of-fields + */ + fields?: FieldsOverride generateDescription?: GenerateDescription generateImage?: GenerateImage generateTitle?: GenerateTitle + /** + * + */ generateURL?: GenerateURL + /** + * Globals to include the SEO fields in + */ globals?: string[] interfaceName?: string + /** + * Group fields into tabs, your content will be automatically put into a general tab and the SEO fields into an SEO tab + * + * If you need more flexibility you can insert the fields manually as needed. @link https://payloadcms.com/docs/beta/plugins/seo#direct-use-of-fields + */ tabbedUI?: boolean + /** + * The slug of the collection used to handle image uploads + */ uploadsCollection?: string } diff --git a/test/plugin-seo/components/AfterInput.tsx b/test/plugin-seo/components/AfterInput.tsx new file mode 100644 index 000000000..2104b657a --- /dev/null +++ b/test/plugin-seo/components/AfterInput.tsx @@ -0,0 +1,5 @@ +import React from 'react' + +export const AfterInput: React.FC = () => { + return
{`Hello this is afterInput`}
+} diff --git a/test/plugin-seo/components/BeforeInput.tsx b/test/plugin-seo/components/BeforeInput.tsx new file mode 100644 index 000000000..65b5454df --- /dev/null +++ b/test/plugin-seo/components/BeforeInput.tsx @@ -0,0 +1,5 @@ +import React from 'react' + +export const BeforeInput: React.FC = () => { + return
{`Hello this is beforeInput`}
+} diff --git a/test/plugin-seo/config.ts b/test/plugin-seo/config.ts index eae2af59c..c1d7e4611 100644 --- a/test/plugin-seo/config.ts +++ b/test/plugin-seo/config.ts @@ -3,6 +3,7 @@ import path from 'path' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) import type { GenerateDescription, GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types' +import type { Field } from 'payload' import type { Page } from 'plugin-seo/payload-types.js' import { seoPlugin } from '@payloadcms/plugin-seo' @@ -68,18 +69,34 @@ export default buildConfigWithDefaults({ plugins: [ seoPlugin({ collections: ['pages'], - fieldOverrides: { - title: { - required: true, - }, + fields: ({ defaultFields }) => { + const modifiedFields = defaultFields.map((field) => { + if ('name' in field && field.name === 'title') { + return { + ...field, + required: true, + admin: { + ...field.admin, + components: { + ...field.admin.components, + afterInput: '/components/AfterInput.js#AfterInput', + beforeInput: '/components/BeforeInput.js#BeforeInput', + }, + }, + } as Field + } + return field + }) + + return [ + ...modifiedFields, + { + name: 'ogTitle', + type: 'text', + label: 'og:title', + }, + ] }, - fields: [ - { - name: 'ogTitle', - type: 'text', - label: 'og:title', - }, - ], generateDescription, generateTitle, generateURL, diff --git a/test/plugin-seo/e2e.spec.ts b/test/plugin-seo/e2e.spec.ts index 86d688cc5..49ea3423d 100644 --- a/test/plugin-seo/e2e.spec.ts +++ b/test/plugin-seo/e2e.spec.ts @@ -116,7 +116,7 @@ describe('SEO Plugin', () => { const autoGenerateButtonClass = '.group-field__wrap .render-fields div:nth-of-type(1) button' const metaDescriptionClass = '#field-meta__description' const previewClass = - '#field-meta > div > div.render-fields.render-fields--margins-small > div:nth-child(6)' + '#field-meta > div > div.render-fields.render-fields--margins-small > div:nth-child(5)' const secondTab = page.locator(contentTabsClass).nth(1) await secondTab.click()