Skip to content

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.

  • 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) and robots.txt

Set these environment variables — they feed titles, canonical URLs, sitemaps, and robots.txt.

VariableUsed for
NEXT_PUBLIC_SERVER_URLAbsolute URLs in metadata, sitemaps, and robots.txt
NEXT_PUBLIC_SITE_NAMESuffix on auto-generated meta titles
  1. 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>>;
  2. Add the SEO tab and hooks to the collection. The meta group and beforeChange hook give you the admin fields plus auto-generation; afterChange revalidates 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 autoGenerateHook arguments map your source fields (title, excerpt, image) onto the meta group, so blank SEO fields fall back to the document’s content.

  3. 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.ts
    export const siteMapIndex: Record<number, ValidCollections> = {
    0: 'pages',
    1: 'posts',
    2: 'events', // Add new collection
    };
  4. Create the frontend route at src/app/(frontend)/events/[slug]/page.tsx and export generateMetadata(), reading from the document’s meta group.

    // src/app/(frontend)/events/[slug]/page.tsx
    export 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 || ''],
    },
    };
    }

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.

Crawl rules live in src/app/robots.ts. Edit the rules to change what is allowed or disallowed; the sitemap reference is wired automatically.

src/app/robots.ts
rules: {
userAgent: '*',
allow: '/',
disallow: '/admin/',
},
FieldGuidance
Meta Title50–60 chars, unique per page, format Page Title - Site Name
Meta Description150–160 chars, primary keyword + call-to-action
Meta Image1200×630 px (OpenGraph standard), branded, high quality
URL slugShort, descriptive, hyphen-separated
ToolPurpose
Google Search ConsoleIndexing, URL testing
Bing Webmaster ToolsMicrosoft search indexing
opengraph.xyzSocial card preview
LighthouseSEO audit
  1. Verify the document is published (_status: 'published')
  2. Check the meta fields are populated in the admin
  3. Ensure generateMetadata() fetches the correct collection
  1. Confirm the collection is in siteMapIndex
  2. Verify documents have slug and updatedAt fields
  3. Check the documents are published
  1. Verify the prefix in src/lib/url/slug.ts is correct
  2. Check slug field values on the documents
  3. Ensure NEXT_PUBLIC_SERVER_URL is set correctly