Skip to content

Commit b417c1f

Browse files
authored
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 #8893
1 parent 2c6635f commit b417c1f

File tree

10 files changed

+116
-78
lines changed

10 files changed

+116
-78
lines changed

docs/plugins/seo.mdx

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,23 @@ An array of global slugs to enable SEO. Enabled globals receive a `meta` field w
8989

9090
##### `fields`
9191

92-
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:
92+
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.
9393

94-
- `title`: text
95-
- `description`: textarea
96-
- `image`: upload (if an `uploadsCollection` is provided)
97-
- `preview`: ui
94+
```ts
95+
// payload.config.ts
96+
{
97+
// ...
98+
seoPlugin({
99+
fields: ({ defaultFields }) => [
100+
...defaultFields,
101+
{
102+
name: 'customField',
103+
type: 'text',
104+
}
105+
]
106+
})
107+
}
108+
```
98109

99110
##### `uploadsCollection`
100111

@@ -209,25 +220,6 @@ Rename the meta group interface name that is generated for TypeScript and GraphQ
209220
}
210221
```
211222

212-
#### `fieldOverrides`
213-
214-
Pass any valid field props to the base fields: Title, Description or Image.
215-
216-
```ts
217-
// payload.config.ts
218-
seoPlugin({
219-
// ...
220-
fieldOverrides: {
221-
title: {
222-
required: true,
223-
},
224-
description: {
225-
localized: true,
226-
},
227-
},
228-
})
229-
```
230-
231223
## Direct use of fields
232224

233225
There is the option to directly import any of the fields from the plugin so that you can include them anywhere as needed.

packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props)
3232
const {
3333
field: {
3434
admin: {
35-
components: { Label },
35+
components: { afterInput, beforeInput, Label },
3636
},
3737
label,
3838
maxLength: maxLengthFromProps,
3939
minLength: minLengthFromProps,
4040
required,
4141
},
42+
field: fieldFromProps,
4243
hasGenerateDescriptionFn,
4344
labelProps,
4445
} = props
@@ -132,7 +133,7 @@ export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props)
132133
>
133134
<div className="plugin-seo__field">
134135
<FieldLabel
135-
field={null}
136+
field={fieldFromProps}
136137
Label={Label}
137138
label={label}
138139
required={required}
@@ -183,6 +184,8 @@ export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props)
183184
}}
184185
>
185186
<TextareaInput
187+
afterInput={afterInput}
188+
beforeInput={beforeInput}
186189
Error={{
187190
type: 'client',
188191
Component: null,

packages/plugin-seo/src/fields/MetaImage/MetaImageComponent.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
3535
relationTo,
3636
required,
3737
},
38+
field: fieldFromProps,
3839
hasGenerateImageFn,
3940
labelProps,
4041
} = props || {}
@@ -125,7 +126,7 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
125126
>
126127
<div className="plugin-seo__field">
127128
<FieldLabel
128-
field={null}
129+
field={fieldFromProps}
129130
Label={Label}
130131
label={label}
131132
required={required}

packages/plugin-seo/src/fields/MetaTitle/MetaTitleComponent.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
3333
const {
3434
field: {
3535
admin: {
36-
components: { Label },
36+
components: { afterInput, beforeInput, Label },
3737
},
3838
label,
3939
maxLength: maxLengthFromProps,
@@ -182,6 +182,8 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
182182
}}
183183
>
184184
<TextInput
185+
afterInput={afterInput}
186+
beforeInput={beforeInput}
185187
Error={{
186188
type: 'client',
187189
Component: null,

packages/plugin-seo/src/index.tsx

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Config, GroupField, TabsField, TextField } from 'payload'
1+
import type { Config, Field, GroupField, TabsField } from 'payload'
22

33
import { deepMergeSimple } from 'payload/shared'
44

@@ -20,33 +20,35 @@ import { translations } from './translations/index.js'
2020
export const seoPlugin =
2121
(pluginConfig: SEOPluginConfig) =>
2222
(config: Config): Config => {
23+
const defaultFields: Field[] = [
24+
OverviewField({}),
25+
MetaTitleField({
26+
hasGenerateFn: typeof pluginConfig?.generateTitle === 'function',
27+
}),
28+
MetaDescriptionField({
29+
hasGenerateFn: typeof pluginConfig?.generateDescription === 'function',
30+
}),
31+
...(pluginConfig?.uploadsCollection
32+
? [
33+
MetaImageField({
34+
hasGenerateFn: typeof pluginConfig?.generateImage === 'function',
35+
relationTo: pluginConfig.uploadsCollection,
36+
}),
37+
]
38+
: []),
39+
PreviewField({
40+
hasGenerateFn: typeof pluginConfig?.generateURL === 'function',
41+
}),
42+
]
43+
2344
const seoFields: GroupField[] = [
2445
{
2546
name: 'meta',
2647
type: 'group',
2748
fields: [
28-
OverviewField({}),
29-
MetaTitleField({
30-
hasGenerateFn: typeof pluginConfig?.generateTitle === 'function',
31-
overrides: pluginConfig?.fieldOverrides?.title,
32-
}),
33-
MetaDescriptionField({
34-
hasGenerateFn: typeof pluginConfig?.generateDescription === 'function',
35-
overrides: pluginConfig?.fieldOverrides?.description,
36-
}),
37-
...(pluginConfig?.uploadsCollection
38-
? [
39-
MetaImageField({
40-
hasGenerateFn: typeof pluginConfig?.generateImage === 'function',
41-
overrides: pluginConfig?.fieldOverrides?.image,
42-
relationTo: pluginConfig.uploadsCollection,
43-
}),
44-
]
45-
: []),
46-
...(pluginConfig?.fields || []),
47-
PreviewField({
48-
hasGenerateFn: typeof pluginConfig?.generateURL === 'function',
49-
}),
49+
...(pluginConfig?.fields && typeof pluginConfig.fields === 'function'
50+
? pluginConfig.fields({ defaultFields })
51+
: defaultFields),
5052
],
5153
interfaceName: pluginConfig.interfaceName,
5254
label: 'SEO',

packages/plugin-seo/src/types.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import type { DocumentInfoContext } from '@payloadcms/ui'
2-
import type {
3-
CollectionConfig,
4-
Field,
5-
GlobalConfig,
6-
PayloadRequest,
7-
TextareaField,
8-
TextField,
9-
UploadField,
10-
} from 'payload'
2+
import type { CollectionConfig, Field, GlobalConfig, PayloadRequest } from 'payload'
3+
4+
export type FieldsOverride = (args: { defaultFields: Field[] }) => Field[]
115

126
export type PartialDocumentInfoContext = Pick<
137
DocumentInfoContext,
@@ -66,20 +60,37 @@ export type GenerateURL<T = any> = (
6660
) => Promise<string> | string
6761

6862
export type SEOPluginConfig = {
63+
/**
64+
* Collections to include the SEO fields in
65+
*/
6966
collections?: string[]
70-
fieldOverrides?: {
71-
description?: Partial<TextareaField>
72-
image?: Partial<UploadField>
73-
title?: Partial<TextField>
74-
}
75-
fields?: Field[]
67+
/**
68+
* Override the default fields inserted by the SEO plugin via a function that receives the default fields and returns the new fields
69+
*
70+
* 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
71+
*/
72+
fields?: FieldsOverride
7673
generateDescription?: GenerateDescription
7774
generateImage?: GenerateImage
7875
generateTitle?: GenerateTitle
76+
/**
77+
*
78+
*/
7979
generateURL?: GenerateURL
80+
/**
81+
* Globals to include the SEO fields in
82+
*/
8083
globals?: string[]
8184
interfaceName?: string
85+
/**
86+
* Group fields into tabs, your content will be automatically put into a general tab and the SEO fields into an SEO tab
87+
*
88+
* 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
89+
*/
8290
tabbedUI?: boolean
91+
/**
92+
* The slug of the collection used to handle image uploads
93+
*/
8394
uploadsCollection?: string
8495
}
8596

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import React from 'react'
2+
3+
export const AfterInput: React.FC = () => {
4+
return <div>{`Hello this is afterInput`}</div>
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import React from 'react'
2+
3+
export const BeforeInput: React.FC = () => {
4+
return <div>{`Hello this is beforeInput`}</div>
5+
}

test/plugin-seo/config.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'path'
33
const filename = fileURLToPath(import.meta.url)
44
const dirname = path.dirname(filename)
55
import type { GenerateDescription, GenerateTitle, GenerateURL } from '@payloadcms/plugin-seo/types'
6+
import type { Field } from 'payload'
67
import type { Page } from 'plugin-seo/payload-types.js'
78

89
import { seoPlugin } from '@payloadcms/plugin-seo'
@@ -68,18 +69,34 @@ export default buildConfigWithDefaults({
6869
plugins: [
6970
seoPlugin({
7071
collections: ['pages'],
71-
fieldOverrides: {
72-
title: {
73-
required: true,
74-
},
72+
fields: ({ defaultFields }) => {
73+
const modifiedFields = defaultFields.map((field) => {
74+
if ('name' in field && field.name === 'title') {
75+
return {
76+
...field,
77+
required: true,
78+
admin: {
79+
...field.admin,
80+
components: {
81+
...field.admin.components,
82+
afterInput: '/components/AfterInput.js#AfterInput',
83+
beforeInput: '/components/BeforeInput.js#BeforeInput',
84+
},
85+
},
86+
} as Field
87+
}
88+
return field
89+
})
90+
91+
return [
92+
...modifiedFields,
93+
{
94+
name: 'ogTitle',
95+
type: 'text',
96+
label: 'og:title',
97+
},
98+
]
7599
},
76-
fields: [
77-
{
78-
name: 'ogTitle',
79-
type: 'text',
80-
label: 'og:title',
81-
},
82-
],
83100
generateDescription,
84101
generateTitle,
85102
generateURL,

test/plugin-seo/e2e.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ describe('SEO Plugin', () => {
116116
const autoGenerateButtonClass = '.group-field__wrap .render-fields div:nth-of-type(1) button'
117117
const metaDescriptionClass = '#field-meta__description'
118118
const previewClass =
119-
'#field-meta > div > div.render-fields.render-fields--margins-small > div:nth-child(6)'
119+
'#field-meta > div > div.render-fields.render-fields--margins-small > div:nth-child(5)'
120120

121121
const secondTab = page.locator(contentTabsClass).nth(1)
122122
await secondTab.click()

0 commit comments

Comments
 (0)