feat(cms): add Payload-driven site metadata global and wire Next.js SEO output

- add new metadata global with URL, title, description, keywords, Open Graph, Twitter, and image fields
- register Metadata global in payload.config.ts
- switch frontend layout to generateMetadata() and load metadata values from Payload globals
- resolve metadata image relation (ID or populated media) for OG/Twitter images
- update live preview globals config to use SiteSettings.slug and HomePage.slug
- keep SiteSettings revalidation hook in consistent position within config
- regenerate payload-types.ts for new global/schema types
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-02-15 20:14:42 -03:00
parent b955c48091
commit f26e5d7537
5 changed files with 215 additions and 8 deletions

View file

@ -4,10 +4,59 @@ import React from 'react'
import './styles.css'
import { RefreshRouteOnSave } from '@/utilities/RefreshRouteOnSave'
import { Metadata } from 'next'
import { getCachedGlobal } from '@/utilities/globals'
import { Media } from '@/payload-types'
import { getCachedDocumentById } from '@/utilities/documents'
export const metadata: Metadata = {
description: 'Um template base usando Payload em uma aplicação Next.js.',
title: 'Template Base do Payload',
export async function generateMetadata(): Promise<Metadata> {
const payload = await getCachedGlobal('metadata')()
let image = payload.image
if (typeof image === 'number') {
image = await getCachedDocumentById('media', String(payload.image))()
}
return {
title: payload.title,
description: payload.description,
keywords: payload.keywords.filter((keyword) => typeof keyword === 'string'),
authors: [
{ name: 'Francisco Solla', url: 'https://solla.dev' },
{ name: 'Teqo', url: 'https://teqo.app' },
],
creator: 'Francisco Solla',
publisher: 'Teqo',
robots: {
index: true,
follow: true,
},
openGraph: {
type: 'website',
locale: 'pt-BR',
url: payload.URL,
siteName: payload.openGraph.siteName,
title: payload.title,
description: payload.description,
images: image?.url
? [
{
url: image.url,
width: image.width!,
height: image.height!,
alt: image.alt,
},
]
: [],
},
twitter: {
card: 'summary_large_image',
title: payload.title,
description: payload.twitter.description,
creator: payload.twitter.creator,
images: image?.url ? [image.url] : [],
},
}
}
export default async function RootLayout(props: { children: React.ReactNode }) {

99
src/globals/Metadata.ts Normal file
View file

@ -0,0 +1,99 @@
import { revalidateGlobal } from '@/utilities/globals'
import { GlobalConfig } from 'payload'
const slug = 'metadata'
const revalidate = async () => revalidateGlobal(slug)
export const Metadata: GlobalConfig = {
slug,
label: 'Metadados',
admin: {
group: 'Configurações',
},
access: {
read: () => true,
update: ({ req: { user } }) => Boolean(user),
},
hooks: {
afterChange: [revalidate],
},
fields: [
{
name: 'URL',
label: 'URL',
type: 'text',
required: true,
},
{
name: 'title',
label: 'Título',
type: 'text',
required: true,
},
{
name: 'description',
label: 'Descrição',
type: 'textarea',
required: true,
},
{
name: 'keywords',
label: 'Palavras-chave',
type: 'array',
required: true,
fields: [
{
name: 'keyword',
label: 'Palavra-chave',
type: 'text',
required: true,
},
],
},
{
name: 'openGraph',
label: 'Open Graph',
type: 'group',
fields: [
{
name: 'siteName',
label: 'Nome do Site',
type: 'text',
required: true,
},
{
name: 'description',
label: 'Descrição',
type: 'textarea',
required: true,
},
],
},
{
name: 'twitter',
label: 'Twitter',
type: 'group',
fields: [
{
name: 'creator',
label: 'Criador',
type: 'text',
required: true,
},
{
name: 'description',
label: 'Descrição',
type: 'textarea',
required: true,
},
],
},
{
name: 'image',
label: 'Imagem',
type: 'upload',
relationTo: 'media',
},
],
}

View file

@ -16,6 +16,9 @@ export const SiteSettings: GlobalConfig = {
read: () => true,
update: ({ req: { user } }) => Boolean(user),
},
hooks: {
afterChange: [revalidate],
},
fields: [
{
name: 'headerTitle',
@ -77,9 +80,6 @@ export const SiteSettings: GlobalConfig = {
],
},
],
hooks: {
afterChange: [revalidate],
},
versions: {
drafts: {
autosave: {

View file

@ -92,10 +92,12 @@ export interface Config {
globals: {
'site-settings': SiteSetting;
home: Home;
metadata: Metadatum;
};
globalsSelect: {
'site-settings': SiteSettingsSelect<false> | SiteSettingsSelect<true>;
home: HomeSelect<false> | HomeSelect<true>;
metadata: MetadataSelect<false> | MetadataSelect<true>;
};
locale: null;
user: User;
@ -485,6 +487,31 @@ export interface Home {
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "metadata".
*/
export interface Metadatum {
id: number;
URL: string;
title: string;
description: string;
keywords: {
keyword: string;
id?: string | null;
}[];
openGraph: {
siteName: string;
description: string;
};
twitter: {
creator: string;
description: string;
};
image?: (number | null) | Media;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "site-settings_select".
@ -515,6 +542,37 @@ export interface HomeSelect<T extends boolean = true> {
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "metadata_select".
*/
export interface MetadataSelect<T extends boolean = true> {
URL?: T;
title?: T;
description?: T;
keywords?:
| T
| {
keyword?: T;
id?: T;
};
openGraph?:
| T
| {
siteName?: T;
description?: T;
};
twitter?:
| T
| {
creator?: T;
description?: T;
};
image?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "TaskSchedulePublish".

View file

@ -10,6 +10,7 @@ import { Media } from './collections/Media'
import { SiteSettings } from './globals/SiteSettings'
import { pt } from 'payload/i18n/pt'
import { HomePage } from './globals/HomePage'
import { Metadata } from './globals/Metadata'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@ -22,11 +23,11 @@ export default buildConfig({
},
livePreview: {
url: 'http://localhost:3000',
globals: ['site-settings', 'home'],
globals: [SiteSettings.slug, HomePage.slug],
},
},
collections: [Users, Media],
globals: [SiteSettings, HomePage],
globals: [SiteSettings, HomePage, Metadata],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
typescript: {