098ba79c8f
app/[slug]/page.tsx and LivePageContent.tsx no longer render a hardcoded H1 from page.title — that's now the Hero block's job (see HeroBlock.ts's own comment on why). Breadcrumb stays, still built from page.title. Also backfilled /lebensuhr's already-migrated Pages document with a Hero block (order 0) carrying its real headline — without it the page would have lost its H1 entirely once the hardcoded wrapper was removed.
91 lines
3.2 KiB
TypeScript
91 lines
3.2 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>
|
||
</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 />
|
||
</>
|
||
);
|
||
}
|