From 885389b300ad15ce78c8f520e44346b18a36cfae Mon Sep 17 00:00:00 2001 From: Marco Date: Wed, 26 Aug 2026 20:08:24 +0000 Subject: [PATCH] Add the page-builder frontend: /[slug] catch-all + block renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pages/getPageBySlug/mapPayloadPage in payload.ts follow the exact getPostBySlug/mapPayloadPost pattern. PageBlocks.tsx renders a Pages doc's `layout` field, reusing existing components (RichText, StepArrow, STEP_ICONS, TestimonialsGrid) rather than reinventing per- block styling — matches what /lebensuhr, /3x3-system, and /7-tage-klarheits-check already hand-built. LivePageContent.tsx mirrors LivePostContent.tsx's live-preview pattern, using a synchronous subset of the block renderer (testimonialsRef needs an async fetch a client component can't perform inline, so it's skipped in preview only — same "editable subset" scope-cut LivePostContent.tsx already makes). buildWebPageSchema in structuredData.ts is the generic JSON-LD fallback for content types with no bespoke schema (Article/ Product don't fit a page-builder page). Verified end-to-end: inserted a real Pages test document (SQL, since no admin auth available here) covering richTextSection/quote/ctaCard, confirmed /[slug] renders it correctly, confirmed notFound() after deleting it, then cleaned up the test row. --- app/[slug]/page.tsx | 96 +++++++++++++++++ app/components/LivePageContent.tsx | 46 ++++++++ app/components/PageBlocks.tsx | 168 +++++++++++++++++++++++++++++ app/components/Quote.tsx | 15 +++ app/lib/payload.ts | 106 ++++++++++++++++++ app/lib/structuredData.ts | 20 ++++ 6 files changed, 451 insertions(+) create mode 100644 app/[slug]/page.tsx create mode 100644 app/components/LivePageContent.tsx create mode 100644 app/components/PageBlocks.tsx create mode 100644 app/components/Quote.tsx diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx new file mode 100644 index 0000000..ec66659 --- /dev/null +++ b/app/[slug]/page.tsx @@ -0,0 +1,96 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { draftMode } from "next/headers"; +import { Reveal } from "../components/Reveal"; +import { Footer } from "../components/Footer"; +import { PageBlocks } from "../components/PageBlocks"; +import { LivePageContent } from "../components/LivePageContent"; +import { getPageBySlug, getCompanySettings } from "../lib/payload"; +import { buildWebPageSchema } from "../lib/structuredData"; + +// First generic catch-all content route in this repo — every other page +// (blog posts excepted, which have their own [slug]) is its own static +// directory under app/. Powers Payload's new Pages collection (the page +// builder): a doc's `layout` blocks field renders via PageBlocks.tsx, +// structurally parallel to how RichText.tsx renders Posts.content's own +// (differently-shaped) Blocks. Next.js resolves any matching static route +// (e.g. app/lebensuhr/page.tsx) before ever reaching this catch-all, so a +// Pages document only actually serves a URL once the static file for that +// slug, if any, is removed. +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await params; + const page = await getPageBySlug(slug); + if (!page) return { title: "Seite nicht gefunden" }; + + const title = page.seoTitle || page.title; + const description = page.seoDescription ?? undefined; + + return { + title, + description, + alternates: { canonical: `/${page.slug}` }, + openGraph: { + title, + description, + url: `/${page.slug}`, + type: "website", + images: page.seoImage ? [{ url: page.seoImage }] : undefined, + }, + twitter: { + title, + description, + images: page.seoImage ? [page.seoImage] : undefined, + }, + }; +} + +export default async function DynamicPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const { isEnabled: isPreview } = await draftMode(); + const page = await getPageBySlug(slug, { draft: isPreview }); + if (!page) notFound(); + + const seller = await getCompanySettings(); + const pageSchema = buildWebPageSchema(page, seller); + + return ( + <> +