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:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
5
test/plugin-seo/components/AfterInput.tsx
Normal file
5
test/plugin-seo/components/AfterInput.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
export const AfterInput: React.FC = () => {
|
||||
return <div>{`Hello this is afterInput`}</div>
|
||||
}
|
||||
5
test/plugin-seo/components/BeforeInput.tsx
Normal file
5
test/plugin-seo/components/BeforeInput.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
export const BeforeInput: React.FC = () => {
|
||||
return <div>{`Hello this is beforeInput`}</div>
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user