import type { Media } from "@/payload-types"; import { mediaUrl } from "@/lib/payload/media"; type MediaRef = Media | number | null | undefined; type SeoGroup = | { title?: string | null; description?: string | null; keywords?: string | null; ogImage?: MediaRef; } | null | undefined; export type ResolvedSeo = { title: string; description: string; keywords?: string; ogImageUrl?: string; }; /** * Priority chain used everywhere on the site (see section 32 of the SEO * brief): manual CMS field → content-derived fallback → nothing invented. * `fallbackDescription` may be a thunk so callers only pay for building an * excerpt (walking richText) when the manual field is actually empty. */ export function resolveSeo(args: { seo?: SeoGroup; fallbackTitle: string; fallbackDescription: string | (() => string); fallbackImage?: MediaRef; defaultOgImage?: MediaRef; }): ResolvedSeo { const title = args.seo?.title?.trim() || args.fallbackTitle; const description = args.seo?.description?.trim() || (typeof args.fallbackDescription === "function" ? args.fallbackDescription() : args.fallbackDescription); const ogImageUrl = mediaUrl(args.seo?.ogImage, "hero") ?? mediaUrl(args.fallbackImage, "hero") ?? mediaUrl(args.defaultOgImage, "hero"); return { title, description, keywords: args.seo?.keywords?.trim() || undefined, ogImageUrl, }; }