Custom post content blocks (images/gallery/video/quote) + backend-driven SEO settings

RichText.tsx switched to Payload's official React renderer + custom
JSXConverters (same call signature, LiveRichText/LivePostContent
untouched) — needed to render the new Lexical Blocks the Payload repo's
Posts.content just gained. Converters follow the existing CMS-image
convention (relative + aspect-[...] + fill + object-cover); the video
block resolves YouTube/Vimeo links to an iframe embed.

New getSeoSettings() fetcher (same pattern as getKleinunternehmer()),
app/layout.tsx now generateMetadata() reading it with the same fallback
values it used to hardcode. Per-post SEO overrides (seoTitle/
seoDescription/seoImage) wired into the blog detail page's metadata,
falling back to title/excerpt/thumbnail when empty.

Also fixed while auditing every page's metadata: missing descriptions on
3 konto pages, a static title on the dynamic order-detail route, and
missing OG images on /shop and /blog.
This commit is contained in:
Marco
2026-07-24 21:10:33 +00:00
parent b1b1aa2037
commit 797d9d42fe
13 changed files with 4249 additions and 184 deletions
+27 -17
View File
@@ -4,7 +4,7 @@ import "./globals.css";
import { Navbar } from "./components/Navbar";
import { CartFlyProvider } from "./components/CartFly";
import { CartSync } from "./components/CartSync";
import { getProducts } from "./lib/payload";
import { getProducts, getSeoSettings } from "./lib/payload";
const inter = Inter({
variable: "--font-inter",
@@ -30,22 +30,32 @@ const lora = Lora({
weight: ["400", "600"],
});
export const metadata: Metadata = {
metadataBase: new URL("https://einfach-produktiv.mk360.de"),
title: {
default: "einfach produktiv. Werkzeuge und Impulse für einen leichteren Alltag",
template: "%s | einfach produktiv.",
},
description: "Werkzeuge, Impulse und ein Blog für mehr Klarheit im Alltag.",
openGraph: {
siteName: "einfach produktiv.",
locale: "de_DE",
type: "website",
},
twitter: {
card: "summary_large_image",
},
};
// Backend-driven since 2026-07-24 (CompanySettings' "SEO" tab) — the
// literal strings below are only the fallback getSeoSettings() returns if
// that field is empty or unreachable, kept identical to what used to be
// hardcoded here so nothing changes until an admin actually fills in the
// new fields.
export async function generateMetadata(): Promise<Metadata> {
const seo = await getSeoSettings();
return {
metadataBase: new URL("https://einfach-produktiv.mk360.de"),
title: {
default: seo.defaultTitle ?? "einfach produktiv.",
template: seo.titleTemplate ?? "%s | einfach produktiv.",
},
description: seo.defaultDescription ?? undefined,
openGraph: {
siteName: "einfach produktiv.",
locale: "de_DE",
type: "website",
images: seo.defaultOgImage ? [{ url: seo.defaultOgImage }] : undefined,
},
twitter: {
card: "summary_large_image",
images: seo.defaultOgImage ? [seo.defaultOgImage] : undefined,
},
};
}
export default async function RootLayout({
children,