Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.
AuthorNick Vogel

How to install and configure the Payload SEO plugin

Community Guide
AuthorNick Vogel

Learn how to integrate the Payload SEO plugin with your Next.js app—covering everything from installation and basic configuration to adding custom SEO fields for dynamic, optimized meta data.

In this guide, I’m walking you through how to use the Payload SEO Plugin. We’ll cover everything from installation and basic configuration to adding custom SEO fields. If you’re looking to auto‑generate SEO-friendly titles, descriptions, URLs, and more directly from your CMS fields, then you’re in the right place.

You’ll learn how to:

  • Install and configure the SEO plugin in Payload
  • Auto‑generate meta titles, descriptions, and URLs
  • Add custom fields like canonical URLs
  • Structure your SEO data within your Payload collections

About the Payload SEO plugin

In this video, we kick things off by introducing Payload's free SEO plugin. Once you add the plugin to a collection or global settings, a meta field group is automatically injected into your collection or global. By default, this group includes a title, description, and image. You can also extend the plugin to include any extra SEO fields you might need—say, a canonical URL or even schema markup.

I personally like generating my schema markup on the front end, but defining those fields directly in the plugin is a breeze if that’s your style. The idea is to reduce duplication of work while ensuring your SEO data stays in sync.

Beyond extending the fields in the plugin, you can define functions that automatically generate titles, descriptions, and more—making it even easier for you or your editors to populate your website's SEO information into your website without duplicating effort.

Installing the SEO plugin

To get started, all you have to do is install the plugin. Open your terminal and run: pnpm add @payloadcms/plugin-seo

Make sure the plugin’s version matches your other Payload dependencies. In my setup, I adjusted the version if needed in the package.json and then ran: pnpm i

Once the plugin is installed, import it at the top of your Payload config file:

1
import {seoPlugin} from 'payloadcms/plugin-seo';

Then add it to your plugins array—or create one if you haven't already—in the Payload configuration.

Configuring the SEO Plugin

After installation, it’s time to tweak the base configurations. Open your Payload config file and locate the plugins array. You should add an entry for the SEO plugin.

For example:

1
plugins: [
2
seoPlugin({
3
collections: ['posts'],
4
uploadsCollection: 'media',
5
generateTitle: {doc} => doc.title,
6
generateDescription: {doc} => doc.plainText,
7
generateURL: ({doc, collectionSlug}) => `https://example.com/${collectionSlug}/${doc?.slug}`,
8
}),
9
],

This configuration tells Payload to add SEO meta fields (like title, description, and image) to your specified collections—in this case, for the posts collection. It also sets up functions to auto‑generate these fields based on your document content.

Make sure to run:payload generate: importmap and run pnpm dev to spin up our localhost.

Reviewing the Changes to the Admin UI

After logging into your admin panel (e.g., via localhost:3000/admin), navigate to your blog posts, and you’ll now see the SEO fields added automatically at the bottom of the document. You can auto‑generate values for the title and description right from there

Upon publishing a test post using your new fields, you should be able to click the API tab and see the meta fields within the JSON output.

If you want a different arrangement—say, putting these fields on a dedicated SEO tab rather than at the bottom—you can adjust the configuration by adding the tabbedUI: true option.

Using tabbed UI for a cleaner setup

1
plugins: [
2
seoPlugin({
3
collections: ['posts'],
4
uploadsCollection: 'media',
5
generateTitle: {doc} => doc.title,
6
generateDescription: {doc} => doc.plainText,
7
generateURL: ({doc, collectionSlug}) => `https://example.com/${collectionSlug}/${doc?.slug}`,
8
tabbedUI: true, // Allows us to have a tabbed UI for a cleaner interface
9
}),
10
],

Once saved, refresh your document. You’ll notice a new SEO tab at the top, keeping your main content and SEO data cleanly separated.

These functions accept various arguments—like the document id, locale, Collection or Global config, and even the Payload request object—letting you query additional documents or perform other changes as needed. Additionally, by incorporating your own image field in your collection, you can use generateImage to quickly populate a featured meta image with a single click.

Customizing and extending SEO fields

One great thing about the SEO plugin is its flexibility. You can add custom fields if you need more control over your SEO metadata. For example, you might want to include a canonical URL. To do that, extend the plugin config by specifying additional fields:

1
seoPlugin({
2
...,
3
tabbedUI: true,
4
fields: (defaultFields) => [
5
...defaultFields,
6
{
7
name: 'canonicalURL',
8
type: 'text',
9
label: 'Canonical URL',
10
},
11
],
12
}),

This custom field now appears along with the default SEO fields in your admin UI.

Using SEO fields directly in a Collection

For a more tailored setup, you can remove the SEO plugin’s default field grouping (so leave all "generate" functions in place) and integrate the fields directly into your Collection’s schema. For instance, in your posts collection config, you could wrap everything inside a tab field.

1
import {MetaDescriptionField, MetaImageField, MetaTitleField, OverviewField, PreviewField} from '@payloadcms/plugin-seo/fields'
2
// To add the SEO fields into the new 'SEO' tab (row 60),
3
// we need to import them the Payload SEO plugin
4
5
...
6
7
fields: [
8
{
9
type: 'tabs',
10
tabs: [
11
{
12
label: 'Content',
13
fields: [ // In this tab is where we are
14
// wrapping everything (example below)
15
{
16
type: 'blocks',
17
admin: {
18
initCollapsed: true,
19
isSortable: false,
20
},
21
blocks: [
22
ContentWithMedia, TableOfContents,
23
],
24
name: 'blockTest',
25
label: false,
26
minRows: 1,
27
maxRows: 20,
28
},
29
{
30
name: 'title',
31
type: 'text',
32
},
33
{
34
name: 'slug',
35
type: 'text',
36
},
37
{
38
name: 'content',
39
type: 'richText',
40
editor: lexicalEditor({
41
features: ({ defaultFeatures }) => [
42
...defaultFeatures,
43
BlocksFeature({
44
blocks: [ContentWithMedia, TableOfContents],
45
}),
46
FixedToolbarFeature(),
47
],
48
}),
49
},
50
{
51
name: 'plaintext',
52
type: 'textarea',
53
},
54
{ name: 'number', type: 'number' },
55
{
56
name: 'usersArray',
57
type: 'array',
58
fields: [{ name: 'users', type: 'relationship', relationTo: 'users' }],
59
},
60
],
61
},
62
{
63
label: 'SEO',
64
name: 'meta',
65
fields: [
66
MetaTitleField({
67
hasGenerateFn: true,
68
overrides: {
69
label: 'Title'
70
}
71
}),
72
MetaDescriptionField({
73
hasGenerateFn: true,
74
}),
75
MetaImageField({
76
relationTo: 'media'
77
}),
78
{
79
type: 'text',
80
name: 'canonicalUrl',
81
label: 'Canonical URL',
82
hooks: { // More on this below
83
beforeChange: [
84
async ({data, value}) => !value ? `https://example.com/posts/${data?.slug}` : value
85
]
86
}
87
},
88
PreviewField({
89
hasGenerateFn: true,
90
titlePath: 'meta.title',
91
descriptionPath: 'meta.description',
92
}),
93
OverviewField({
94
titlePath: 'meta.title',
95
descriptionPath: 'meta.description',
96
imagePath: 'meta.image',
97
})
98
]
99
},
100
],
101
},
102
],

Leveraging hooks for default values

Sometimes you want the system to take care of defaults. For example, you could use a beforeChange hook to set a default canonical URL if none is provided. In your custom field definition, you can add a hook like so:

1
MetaImageField({
2
relationTo: 'media'
3
}),
4
{
5
type: 'text',
6
name: 'canonicalUrl',
7
label: 'Canonical URL',
8
hooks: {
9
beforeChange: [
10
async ({data, value}) => !value ? `https://example.com/posts/${data?.slug}` : value
11
]
12
}
13
},

By using this hook, any changes won’t overwrite my custom canonical URL—so after saving and publishing, I can see that the canonical URL is correctly populated.

Final thoughts and what’s next

The configuration options for the SEO plugin are endlessly flexible and extensible. Today we covered the basics—installing the plugin, configuring your meta fields, using a tabbed UI for cleaner organization, and even adding custom fields. There’s a ton more you can do, such as incorporating schema markup or (coming soon) dynamic variable injection down the road.