Files
einfach-produktiv/app/[slug]/page.tsx
T
Marco 885389b300 Add the page-builder frontend: /[slug] catch-all + block renderer
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.
2026-08-26 20:08:24 +00:00

97 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Metadata> {
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 (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(pageSchema) }} />
<main className="flex flex-col flex-1 bg-bg-base">
{isPreview ? (
<LivePageContent initialPage={page} />
) : (
<>
<Reveal className="flex flex-col gap-4 items-start pt-10 pb-8 px-[var(--layout-padding-x)] w-full max-w-[48rem] mx-auto">
<p className="flex items-center gap-2 text-body-sm text-text-muted">
<Link href="/" className="hover:text-brand transition-colors">Startseite</Link>
<span></span>
<span className="text-text-primary">{page.title}</span>
</p>
<p
className="font-semibold text-[clamp(2.25rem,1.393rem+1.786vw,3rem)] text-text-primary leading-[1.15]"
style={{ fontFamily: "var(--font-playfair)" }}
>
{page.title}
</p>
</Reveal>
<Reveal delay={0.1} className="w-full max-w-[48rem] mx-auto px-[var(--layout-padding-x)] pb-14 flex flex-col gap-5">
<PageBlocks blocks={page.layout} />
</Reveal>
</>
)}
</main>
<Footer />
</>
);
}