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
+38
View File
@@ -1441,6 +1441,44 @@ calling out specifically since they add new shared components:
each page's heading — not nested inside the sidebar's `hidden lg:...`
wrapper, which would hide it too regardless of its own `lg:hidden` class.
## Custom post content blocks, and SEO settings (2026-07-24)
`app/components/RichText.tsx` switched from a small hand-rolled Lexical
JSON→JSX walker to Payload's official `@payloadcms/richtext-lexical/react`
renderer + custom `JSXConverters` — needed once the Payload repo's
`Posts.content` gained custom Lexical Blocks (`Bild`/`Bildergalerie`/`Video`/
`Zitat`, see the Payload README's own section on this), which the old
hand-rolled `switch` had no case for at all. Kept the exact same exported
call signature (`RichText({ content, quoteLabel })`), so `LiveRichText.tsx`
and `app/blog/[slug]/components/LivePostContent.tsx` needed zero changes.
`extractHeadings()`/`headingId()` (used by `SectionTOC` on legal pages) stay
an independent, minimal walk over the raw JSON, unrelated to the new
renderer — legal pages don't use Blocks.
Block converters follow this project's established "CMS-sourced image"
convention (`relative` + `aspect-[…]` + `next/image fill` + `object-cover`,
see the blog thumbnail treatment) — a gallery block renders as a
`grid-cols-2` grid, a video block resolves a YouTube/Vimeo URL into an
`<iframe>` embed. The converters object is built fresh per `RichText()` call
(a factory closing over that call's own `quoteLabel`), not a module-level
constant — Server Components can render multiple posts concurrently in the
same process, so a shared mutable variable would be a real race condition.
**SEO settings** — new `getSeoSettings()` in `app/lib/payload.ts` (same
ISR-cached, public-safe-subset-of-CompanySettings pattern as
`getKleinunternehmer()`), with a hardcoded fallback equal to what used to be
directly in `app/layout.tsx`. `app/layout.tsx`'s `metadata` export became
`generateMetadata()` reading it. `PostDetail`/`mapPayloadPost`/
`getPostBySlug()` gained `seoTitle`/`seoDescription`/`seoImage`, each
falling back to the post's own `title`/`excerpt`/`thumbnail` when empty;
`app/blog/[slug]/page.tsx`'s `generateMetadata()` uses them (now also
setting a `twitter` block, previously missing). A few smaller gaps found
while auditing every page's metadata were fixed in the same pass: 3
`konto/*` pages were missing a `description`, `/konto/bestellungen/
[orderNumber]` had a static "Bestelldetails" title despite being a dynamic
route (now uses the real order number), and `/shop`/`/blog` were missing an
Open Graph image.
## Tests
`npm run test:unit` (Vitest, `node` environment, no jsdom/Next.js runtime