From b417c1f61a83001be006fbc280a43012eec30220 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 31 Oct 2024 00:03:39 -0600 Subject: [PATCH] feat(plugin-seo)!: support overriding default fields via a function instead and fixes bugs regarding localized labels (#8958) ## The SEO plugin now takes in a function to override or add in new fields - `fieldOverrides` has been removed - `fields` is now a function that takes in `defaultFields` and expects an array of fields in return This makes it a lot easier for end users to override and extend existing fields and add new ones. This change also brings this plugin inline with the pattern that we use in our other plugins. ```ts // before seoPlugin({ fieldOverrides: { title: { required: true, }, }, fields: [ { name: 'customField', type: 'text', } ] }) // after seoPlugin({ fields: ({ defaultFields }) => { const modifiedFields = defaultFields.map((field) => { // Override existing fields if ('name' in field && field.name === 'title') { return { ...field, required: true, } } return field }) return [ ...modifiedFields, // Add a new field { name: 'ogTitle', type: 'text', label: 'og:title', }, ] }, }) ``` ## Also fixes - Localization labels not showing up on default fields - The inability to add before and after inputs to default fields https://github.com/payloadcms/payload/issues/8893 --- docs/plugins/seo.mdx | 40 +++++++--------- .../MetaDescriptionComponent.tsx | 7 ++- .../fields/MetaImage/MetaImageComponent.tsx | 3 +- .../fields/MetaTitle/MetaTitleComponent.tsx | 4 +- packages/plugin-seo/src/index.tsx | 48 ++++++++++--------- packages/plugin-seo/src/types.ts | 41 ++++++++++------ test/plugin-seo/components/AfterInput.tsx | 5 ++ test/plugin-seo/components/BeforeInput.tsx | 5 ++ test/plugin-seo/config.ts | 39 ++++++++++----- test/plugin-seo/e2e.spec.ts | 2 +- 10 files changed, 116 insertions(+), 78 deletions(-) create mode 100644 test/plugin-seo/components/AfterInput.tsx create mode 100644 test/plugin-seo/components/BeforeInput.tsx 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()