feat(seo): configure payload seo plugin, add dynamic metadata generation and inject LocalBusiness json-ld schema
This commit is contained in:
@@ -11,6 +11,57 @@ import type { Page } from '../../../../payload-types'
|
||||
|
||||
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: {
|
||||
params: Promise<{ locale: string; slug?: string[] }>
|
||||
}) {
|
||||
|
||||
@@ -58,8 +58,25 @@ export default async function RootLayout(props: {
|
||||
// CMS: fallback to generic link if no number is configured.
|
||||
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 (
|
||||
<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">
|
||||
<Providers>
|
||||
<LanguageSlugsProvider>
|
||||
|
||||
@@ -21,6 +21,11 @@ import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_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 { 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 { TestEmailButton as TestEmailButton_630f031911beff97691b963f10a20a86 } from '@intecion/ipal-kit/client'
|
||||
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#BoldFeatureClient": BoldFeatureClient_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#TestEmailButton": TestEmailButton_630f031911beff97691b963f10a20a86,
|
||||
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||
|
||||
@@ -532,6 +532,14 @@ export interface Page {
|
||||
}
|
||||
)[]
|
||||
| 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;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -1217,6 +1225,13 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
blockName?: T;
|
||||
};
|
||||
};
|
||||
meta?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
description?: T;
|
||||
image?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
import { mcpPlugin } from '@payloadcms/plugin-mcp'
|
||||
import { seoPlugin } from '@payloadcms/plugin-seo'
|
||||
import { ipalKit, panelSmtpAdapter } from '@intecion/ipal-kit'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import path from 'path'
|
||||
@@ -46,6 +47,12 @@ export default buildConfig({
|
||||
email: panelSmtpAdapter(),
|
||||
plugins: [
|
||||
mcpPlugin({}),
|
||||
seoPlugin({
|
||||
collections: ['pages'],
|
||||
uploadsCollection: 'media',
|
||||
generateTitle: ({ doc }) => `Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski — ${doc.title}`,
|
||||
generateDescription: ({ doc }) => doc.excerpt || '',
|
||||
}),
|
||||
ipalKit({
|
||||
i18n,
|
||||
pages: { slug: 'pages' },
|
||||
|
||||
Reference in New Issue
Block a user