Search Engine Optimisation
SEO is wired up out of the box: every content collection can expose an SEO tab in the admin, metadata is rendered on the frontend, and sitemaps and robots.txt are generated automatically. This page covers what you need to configure — global settings, per-collection setup, and the editor workflow.
What you get out of the box
Section titled “What you get out of the box”- An SEO tab in the admin (title, description, image) per configured collection
- Auto-generated meta fields when an editor leaves them blank
- Frontend metadata (
<title>, description, OpenGraph) on each route - A paginated sitemap (
/sitemap_index.xml) androbots.txt
Global configuration
Section titled “Global configuration”Set these environment variables — they feed titles, canonical URLs, sitemaps, and robots.txt.
| Variable | Used for |
|---|---|
NEXT_PUBLIC_SERVER_URL | Absolute URLs in metadata, sitemaps, and robots.txt |
NEXT_PUBLIC_SITE_NAME | Suffix on auto-generated meta titles |
Adding SEO to a collection
Section titled “Adding SEO to a collection”-
Register the collection’s URL prefix in
src/lib/url/slug.ts. This is the single source of truth for how the collection’s paths are built across metadata, sitemaps, and internal links.src/lib/url/slug.ts export const slugs = {pages: '/',posts: '/posts/',events: '/events/', // Add new collection} satisfies Partial<Record<CollectionSlug, string>>; -
Add the SEO tab and hooks to the collection. The
metagroup andbeforeChangehook give you the admin fields plus auto-generation;afterChangerevalidates the frontend on save.src/collections/events.ts import {MetaDescriptionField,MetaImageField,MetaTitleField,OverviewField,PreviewField,} from '@payloadcms/plugin-seo/fields';import { autoGenerateHook } from '@/plugins/payload-seo';export const events: CollectionConfig = {slug: 'events',fields: [{ name: 'title', type: 'text', required: true },...slugField('title'),{ name: 'excerpt', type: 'textarea' },{ name: 'featuredImage', type: 'upload', relationTo: 'media' },{type: 'tabs',tabs: [{label: 'SEO',name: 'meta',fields: [OverviewField({titlePath: 'meta.title',descriptionPath: 'meta.description',imagePath: 'meta.image',}),MetaTitleField({ hasGenerateFn: true }),MetaImageField({ hasGenerateFn: true, relationTo: 'media' }),MetaDescriptionField({ hasGenerateFn: true }),PreviewField({hasGenerateFn: true,titlePath: 'meta.title',descriptionPath: 'meta.description',}),],},],},],hooks: {beforeChange: [(context) =>autoGenerateHook<Events>(context,'title','excerpt','featuredImage',{object: 'meta',title: 'title',description: 'description',image: 'image',},),],afterChange: [({ doc }) => {if (doc.slug) {revalidateCollection('events', doc.slug);}},],},};The
autoGenerateHookarguments map your source fields (title, excerpt, image) onto themetagroup, so blank SEO fields fall back to the document’s content. -
Add the collection to the sitemap in
src/app/(frontend)/sitemap.ts. Each entry becomes its own paginated sitemap file (/sitemap-0.xml,/sitemap-1.xml, …).// src/app/(frontend)/sitemap.tsexport const siteMapIndex: Record<number, ValidCollections> = {0: 'pages',1: 'posts',2: 'events', // Add new collection}; -
Create the frontend route at
src/app/(frontend)/events/[slug]/page.tsxand exportgenerateMetadata(), reading from the document’smetagroup.// src/app/(frontend)/events/[slug]/page.tsxexport async function generateMetadata({ params }: Props): Promise<Metadata> {const event = await getEventBySlug(params.slug);return {title: event.meta?.title,description: event.meta?.description,openGraph: {images: [event.meta?.image?.url || ''],},};}
Editing SEO content
Section titled “Editing SEO content”In the admin SEO tab, editors can:
- Fill Meta Title, Meta Description, and Meta Image manually
- Press Generate on any field to derive it from the document content (title, excerpt, featured image)
- Use the Preview field to see how the result appears in search and social cards
Leaving a field blank on save triggers auto-generation, so SEO is never empty even if an editor skips the tab.
Robots.txt
Section titled “Robots.txt”Crawl rules live in src/app/robots.ts. Edit the rules to change what is allowed or disallowed; the sitemap reference is wired automatically.
rules: { userAgent: '*', allow: '/', disallow: '/admin/',},Best practices
Section titled “Best practices”| Field | Guidance |
|---|---|
| Meta Title | 50–60 chars, unique per page, format Page Title - Site Name |
| Meta Description | 150–160 chars, primary keyword + call-to-action |
| Meta Image | 1200×630 px (OpenGraph standard), branded, high quality |
| URL slug | Short, descriptive, hyphen-separated |
Verification tools
Section titled “Verification tools”| Tool | Purpose |
|---|---|
| Google Search Console | Indexing, URL testing |
| Bing Webmaster Tools | Microsoft search indexing |
| opengraph.xyz | Social card preview |
| Lighthouse | SEO audit |
Troubleshooting
Section titled “Troubleshooting”Meta tags not appearing
Section titled “Meta tags not appearing”- Verify the document is published (
_status: 'published') - Check the
metafields are populated in the admin - Ensure
generateMetadata()fetches the correct collection
Sitemap missing pages
Section titled “Sitemap missing pages”- Confirm the collection is in
siteMapIndex - Verify documents have
slugandupdatedAtfields - Check the documents are published
Incorrect URLs
Section titled “Incorrect URLs”- Verify the prefix in
src/lib/url/slug.tsis correct - Check
slugfield values on the documents - Ensure
NEXT_PUBLIC_SERVER_URLis set correctly