feat(seo): configure payload seo plugin, add dynamic metadata generation and inject LocalBusiness json-ld schema

This commit is contained in:
MichalC
2026-09-18 00:05:02 +02:00
parent a8651566c7
commit 12126a0920
5 changed files with 100 additions and 0 deletions
@@ -11,6 +11,57 @@ import type { Page } from '../../../../payload-types'
type FAQFullBlock = Extract<NonNullable<Page['layout']>[0], { blockType: 'faqFull' }> type FAQFullBlock = Extract<NonNullable<Page['layout']>[0], { blockType: 'faqFull' }>
import type { Metadata } from 'next'
export async function generateMetadata(
props: { params: Promise<{ locale: string; slug?: string[] }> }
): Promise<Metadata> {
const params = await props.params
const { locale, slug } = params
const payload = await getPayload({ config: configPromise })
const route = await resolveRoute({ payload, locale, segments: slug || [] })
if (!route) {
return {}
}
const doc = await payload.findByID({
collection: ('collection' in route ? route.collection : 'pages') as 'pages',
id: route.doc.id as string,
locale: locale as 'all' | 'pl' | 'de',
})
// Retrieve meta object injected by @payloadcms/plugin-seo
const meta = (doc as any).meta
const title = meta?.title || doc.title
const description = meta?.description || ''
let imageUrl = ''
if (meta?.image && typeof meta.image === 'object' && meta.image.url) {
imageUrl = meta.image.url
} else if (meta?.image && typeof meta.image === 'string') {
// If it's just an ID, we might not have the full URL without a separate fetch,
// but usually Payload plugin-seo populates the image relationship.
}
return {
title,
description,
openGraph: {
title,
description,
...(imageUrl && { images: [{ url: imageUrl }] }),
},
twitter: {
card: 'summary_large_image',
title,
description,
...(imageUrl && { images: [imageUrl] }),
},
}
}
export default async function Page(props: { export default async function Page(props: {
params: Promise<{ locale: string; slug?: string[] }> params: Promise<{ locale: string; slug?: string[] }>
}) { }) {
+17
View File
@@ -58,8 +58,25 @@ export default async function RootLayout(props: {
// CMS: fallback to generic link if no number is configured. // CMS: fallback to generic link if no number is configured.
const whatsappLink = companyData?.whatsappNumber ? `https://wa.me/${companyData.whatsappNumber}` : '#'; const whatsappLink = companyData?.whatsappNumber ? `https://wa.me/${companyData.whatsappNumber}` : '#';
const schemaOrg = {
"@context": "https://schema.org",
"@type": "AccountingService",
"name": "Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski",
"telephone": companyData?.phone || '',
"email": companyData?.email || '',
"address": companyData?.address || '',
"openingHours": companyData?.hours || '',
"url": "https://www.jkedzierski.de"
};
return ( return (
<html lang={locale} suppressHydrationWarning className={`${publicSans.variable} ${fraunces.variable}`}> <html lang={locale} suppressHydrationWarning className={`${publicSans.variable} ${fraunces.variable}`}>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaOrg) }}
/>
</head>
<body className="font-sans antialiased bg-background text-foreground min-h-screen flex flex-col"> <body className="font-sans antialiased bg-background text-foreground min-h-screen flex flex-col">
<Providers> <Providers>
<LanguageSlugsProvider> <LanguageSlugsProvider>
+10
View File
@@ -21,6 +21,11 @@ import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { OverviewComponent as OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MetaImageComponent as MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
import { MaskedField as MaskedField_630f031911beff97691b963f10a20a86 } from '@intecion/ipal-kit/client' import { MaskedField as MaskedField_630f031911beff97691b963f10a20a86 } from '@intecion/ipal-kit/client'
import { TestEmailButton as TestEmailButton_630f031911beff97691b963f10a20a86 } from '@intecion/ipal-kit/client' import { TestEmailButton as TestEmailButton_630f031911beff97691b963f10a20a86 } from '@intecion/ipal-kit/client'
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
@@ -50,6 +55,11 @@ export const importMap = {
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
"@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860,
"@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860,
"@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860,
"@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
"@intecion/ipal-kit/client#MaskedField": MaskedField_630f031911beff97691b963f10a20a86, "@intecion/ipal-kit/client#MaskedField": MaskedField_630f031911beff97691b963f10a20a86,
"@intecion/ipal-kit/client#TestEmailButton": TestEmailButton_630f031911beff97691b963f10a20a86, "@intecion/ipal-kit/client#TestEmailButton": TestEmailButton_630f031911beff97691b963f10a20a86,
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
+15
View File
@@ -532,6 +532,14 @@ export interface Page {
} }
)[] )[]
| null; | null;
meta?: {
title?: string | null;
description?: string | null;
/**
* Maximum upload file size: 12MB. Recommended file size for images is <500KB.
*/
image?: (number | null) | Media;
};
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@@ -1217,6 +1225,13 @@ export interface PagesSelect<T extends boolean = true> {
blockName?: T; blockName?: T;
}; };
}; };
meta?:
| T
| {
title?: T;
description?: T;
image?: T;
};
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
+7
View File
@@ -1,5 +1,6 @@
import { sqliteAdapter } from '@payloadcms/db-sqlite' import { sqliteAdapter } from '@payloadcms/db-sqlite'
import { mcpPlugin } from '@payloadcms/plugin-mcp' import { mcpPlugin } from '@payloadcms/plugin-mcp'
import { seoPlugin } from '@payloadcms/plugin-seo'
import { ipalKit, panelSmtpAdapter } from '@intecion/ipal-kit' import { ipalKit, panelSmtpAdapter } from '@intecion/ipal-kit'
import { lexicalEditor } from '@payloadcms/richtext-lexical' import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path' import path from 'path'
@@ -46,6 +47,12 @@ export default buildConfig({
email: panelSmtpAdapter(), email: panelSmtpAdapter(),
plugins: [ plugins: [
mcpPlugin({}), mcpPlugin({}),
seoPlugin({
collections: ['pages'],
uploadsCollection: 'media',
generateTitle: ({ doc }) => `Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski — ${doc.title}`,
generateDescription: ({ doc }) => doc.excerpt || '',
}),
ipalKit({ ipalKit({
i18n, i18n,
pages: { slug: 'pages' }, pages: { slug: 'pages' },