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:
parent
b955c48091
commit
f26e5d7537
5 changed files with 215 additions and 8 deletions
|
|
@ -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
99
src/globals/Metadata.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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".
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue