885389b300
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.
97 lines
3.4 KiB
TypeScript
97 lines
3.4 KiB
TypeScript
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 />
|
||
</>
|
||
);
|
||
}
|