@fusionary/payload-plugin-site-meta
v1.0.7
Published
Opinionated Payload CMS plugin that owns your site's public-facing identity, SEO, redirects, and structured data in one global:
Readme
@fusionary/payload-plugin-site-meta
Opinionated Payload CMS plugin that owns your site's public-facing identity, SEO, redirects, and structured data in one global:
- Brand — businessName, legalName, tagline, description, logo, OG image, Twitter handle, businessType (for LocalBusiness schema)
- SEO Settings — canonical URL, robots mode, sitemap toggle, GTM ID,
title template, default description (wraps
@payloadcms/plugin-seo) - Contact (opt-in) — primary location, departments
- Social (opt-in) — social links
- Automatic Redirects — field-change rules that create redirects
automatically (wraps
@payloadcms/plugin-redirects)
Plus helpers for generating Next.js Metadata and schema-dts-typed JSON-LD
(Organization, WebSite, LocalBusiness).
Install
pnpm add @fusionary/payload-plugin-site-meta
pnpm add @payloadcms/plugin-seo @payloadcms/plugin-redirects payload
# if using the metadata helper:
pnpm add nextMinimal config
import { siteMetaPlugin } from '@fusionary/payload-plugin-site-meta'
buildConfig({
plugins: [
siteMetaPlugin({
seoPluginOptions: {
collections: ['pages', 'posts'],
},
}),
],
})This creates a site-meta global under the Site Meta admin group with
Brand, SEO Settings, and Automatic Redirects tabs, and adds SEO meta fields
(including keywords, ogTitle, ogDescription) to the listed collections.
Full config
siteMetaPlugin({
// Defaults to 'site-meta'. Set to 'seo' during a staged migration from
// @fusionary/payload-plugin-seo-plus.
globalSlug: 'site-meta',
// Payload collection slug for upload fields (logo, defaultOgImage).
// Defaults to 'media'.
uploadsSlug: 'media',
// Opt-in per collection. Omit the whole key or a sub-key to skip.
businessInfo: {
locations: { slug: 'locations' },
hoursOfOperation: { slug: 'hours-of-operation' },
phoneNumbers: { slug: 'phone-numbers' },
departments: { slug: 'departments' },
socialLinks: { slug: 'social-links' },
},
// Passthrough to @payloadcms/plugin-seo
seoPluginOptions: { collections: ['pages', 'posts'] },
// Passthrough to @payloadcms/plugin-redirects
redirectsPluginOptions: {
/* ... */
},
// Passthrough to Payload global config (admin overrides, access, etc.)
seoGlobalOptions: {
/* ... */
},
})Enabling businessInfo.locations or businessInfo.departments adds a
Contact tab to the global. Enabling businessInfo.socialLinks adds a
Social tab. Fields inside those tabs are gated on their respective
collections being enabled, so no dangling relationships.
Plugin ordering
Register siteMetaPlugin before plugins that expect the site-meta global
to exist, and ensure any collection your redirects plugin covers is already
declared when site-meta runs (it inspects config.collections).
Helpers
generateSiteMetadata
Produces a Next.js Metadata object from the site global and an optional
per-page doc.
// app/layout.tsx
import { generateSiteMetadata } from '@fusionary/payload-plugin-site-meta'
import { getPayload } from 'payload'
import config from '@payload-config'
export async function generateMetadata() {
const payload = await getPayload({ config })
const site = await payload.findGlobal({ slug: 'site-meta', depth: 2 })
return generateSiteMetadata({ site })
}
// app/(site)/[...slug]/page.tsx
export async function generateMetadata({ params }) {
const [site, doc] = await Promise.all([
payload.findGlobal({ slug: 'site-meta', depth: 2 }),
payload.findByID({ collection: 'pages', id: params.slug, depth: 1 }),
])
return generateSiteMetadata({ site, doc })
}- When
robotsSettingis not'prod', returns{ index: false, follow: false }regardless of page-level settings. - When
docis provided, usesdoc.meta.titleas the absolute title anddoc.meta.descriptionas the description (with fallbacks). Otherwise usesbrand.businessName+seoSettings.metadataTemplate. - Open Graph image:
doc.meta.image→brand.defaultOgImage. - Twitter card:
summary_large_image;site/creator=@{twitterHandle}.
JSON-LD helpers
Two forms — plain object (for composition) and a React component that
renders a <script type="application/ld+json">. All return null when
required fields are missing.
import {
OrganizationLdScript,
WebSiteLdScript,
LocalBusinessLdScript,
getOrganizationLd,
getWebSiteLd,
getLocalBusinessLd,
} from '@fusionary/payload-plugin-site-meta'
// Drop into a layout head:
<OrganizationLdScript site={site} />
<WebSiteLdScript site={site} />
<LocalBusinessLdScript site={site} />
// Or build custom JSON-LD using schema-dts types re-exported from this package:
import type { Article, WithContext } from '@fusionary/payload-plugin-site-meta'
const article: WithContext<Article> = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: doc.title,
// ...
}| Helper | Requires |
| -------------------- | ------------------------------------------------------------------------------------------------- |
| getOrganizationLd | brand.businessName |
| getWebSiteLd | brand.businessName + seoSettings.canonicalUrl |
| getLocalBusinessLd | brand.businessName + brand.businessType + populated contact.primaryLocation with an address |
LocalBusiness also picks up coordinates from the location and emits
GeoCoordinates. Don't render both Organization and LocalBusiness on
the same page — pick one.
Per-collection meta fields
On every collection listed in seoPluginOptions.collections, this plugin
extends @payloadcms/plugin-seo's default meta group with:
seoNoIndex(checkbox)seoNoFollow(checkbox)keywords(text, comma-separated)ogTitle(text, falls back tometa.title)ogDescription(textarea, falls back tometa.description)
Migrating from seo-plus + business-info
See the deprecation notes on @fusionary/payload-plugin-seo-plus and
@fusionary/payload-plugin-business-info. A migration skill for Claude Code
is planned to automate the import swaps and generate the DB migration for
the seo → site-meta global rename.
