import type { CollectionBeforeChangeHook } from 'payload' /** * Maps document fields to SEO meta fields for auto-fill. * Defaults match the Payload website template (title → meta.title). */ export type AutoFillMapping = { /** Document field used to fill meta.description when empty. */ description?: string /** Document field used to fill meta.title when empty. Default: 'title'. */ title?: string } /** * Reads a possibly-localized field value into a plain string. * With localized fields at this hook stage the value is the active-locale * string, so we just coerce defensively. */ function readString(value: unknown): string | undefined { if (typeof value === 'string' && value.trim()) {return value.trim()} return undefined } /** * Builds a beforeChange hook that fills empty SEO meta fields from document * content. Only fills when the meta field is blank — never overwrites what an * editor typed. This runs server-side on every create/update, so editors get * sensible meta without clicking "auto-generate". * * The plugin doesn't know the client's collection shape, so the field mapping * is configurable; defaults follow the website template. */ export function buildAutoFillMetaHook(mapping: AutoFillMapping = {}): CollectionBeforeChangeHook { const titleField = mapping.title ?? 'title' const descriptionField = mapping.description return ({ data }) => { if (!data) {return data} // plugin-seo stores meta as a group under `meta` const meta = (data.meta as Record | undefined) ?? {} // Fill meta.title from the document title when empty if (!readString(meta.title)) { const sourceTitle = readString(data[titleField]) if (sourceTitle) {meta.title = sourceTitle} } // Fill meta.description from a configured field when empty if (descriptionField && !readString(meta.description)) { const sourceDescription = readString(data[descriptionField]) if (sourceDescription) {meta.description = sourceDescription} } data.meta = meta return data } }