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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user