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
This commit is contained in:
Paul
2024-10-31 00:03:39 -06:00
committed by GitHub
parent 2c6635fe20
commit b417c1f61a
10 changed files with 116 additions and 78 deletions

View File

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

View File

@@ -32,13 +32,14 @@ export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (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<MetaDescriptionProps> = (props)
>
<div className="plugin-seo__field">
<FieldLabel
field={null}
field={fieldFromProps}
Label={Label}
label={label}
required={required}
@@ -183,6 +184,8 @@ export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props)
}}
>
<TextareaInput
afterInput={afterInput}
beforeInput={beforeInput}
Error={{
type: 'client',
Component: null,

View File

@@ -35,6 +35,7 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
relationTo,
required,
},
field: fieldFromProps,
hasGenerateImageFn,
labelProps,
} = props || {}
@@ -125,7 +126,7 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
>
<div className="plugin-seo__field">
<FieldLabel
field={null}
field={fieldFromProps}
Label={Label}
label={label}
required={required}

View File

@@ -33,7 +33,7 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
const {
field: {
admin: {
components: { Label },
components: { afterInput, beforeInput, Label },
},
label,
maxLength: maxLengthFromProps,
@@ -182,6 +182,8 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
}}
>
<TextInput
afterInput={afterInput}
beforeInput={beforeInput}
Error={{
type: 'client',
Component: null,

View File

@@ -1,4 +1,4 @@
import type { Config, GroupField, TabsField, TextField } from 'payload'
import type { Config, Field, GroupField, TabsField } from 'payload'
import { deepMergeSimple } from 'payload/shared'
@@ -20,33 +20,35 @@ import { translations } from './translations/index.js'
export const seoPlugin =
(pluginConfig: SEOPluginConfig) =>
(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',

View File

@@ -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<T = any> = (
) => Promise<string> | string
export type SEOPluginConfig = {
/**
* Collections to include the SEO fields in
*/
collections?: string[]
fieldOverrides?: {
description?: Partial<TextareaField>
image?: Partial<UploadField>
title?: Partial<TextField>
}
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
}

View File

@@ -0,0 +1,5 @@
import React from 'react'
export const AfterInput: React.FC = () => {
return <div>{`Hello this is afterInput`}</div>
}

View File

@@ -0,0 +1,5 @@
import React from 'react'
export const BeforeInput: React.FC = () => {
return <div>{`Hello this is beforeInput`}</div>
}

View File

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

View File

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