Frontend support for the Hero block; H1 moves out of the hardcoded wrapper

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.
This commit is contained in:
Marco
2026-08-26 20:41:21 +00:00
parent f4af0bc15c
commit 098ba79c8f
4 changed files with 39 additions and 14 deletions
-6
View File
@@ -76,12 +76,6 @@ export default async function DynamicPage({
<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">
+3 -8
View File
@@ -8,8 +8,9 @@ import { mapPayloadPage, type PayloadPage, type Page } from "../lib/payload";
const PAYLOAD_URL = process.env.NEXT_PUBLIC_PAYLOAD_URL || "https://payload.mk360.de";
// Live-previewable subset of a Pages document: breadcrumb/H1 and the
// layout blocks — same "editable subset only" scope as LivePostContent.tsx
// Live-previewable subset of a Pages document: the breadcrumb and the
// layout blocks (the H1 now lives in the first block, usually a "hero" —
// see HeroBlock.ts) — same "editable subset only" scope as LivePostContent.tsx
// (see that file's own comment). `testimonialsRef` blocks are skipped here
// (see renderPageBlockSync's own comment) since they need an async fetch a
// client component can't perform inline; editing that block still shows up
@@ -30,12 +31,6 @@ export function LivePageContent({ initialPage }: { initialPage: Page }) {
<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">
+25
View File
@@ -36,6 +36,31 @@ export async function PageBlocks({ blocks }: { blocks: PageBlock[] }) {
// makes for its own author-bio/"Weiterlesen" chrome).
export function renderPageBlockSync(block: PageBlock): React.ReactNode {
switch (block.blockType) {
// Same "font-playfair clamp headline" treatment every hand-built page
// (/lebensuhr, /3x3-system, /ueber-mich) used for its own H1 before
// this block existed — see HeroBlock.ts's own comment on why the
// headline moved out of Pages.title and into here.
case "hero":
return (
<div key={block.id} className="flex flex-col gap-4 items-start">
<p
className="font-semibold text-[clamp(2.25rem,1.393rem+1.786vw,3rem)] text-text-primary leading-[1.15]"
style={{ fontFamily: "var(--font-playfair)" }}
>
{block.headline}
</p>
{block.subline && <p className="text-body text-text-muted">{block.subline}</p>}
{block.ctaLabel && block.ctaHref && (
<Link
href={block.ctaHref}
className="inline-flex items-center gap-2 bg-brand hover:brightness-95 active:scale-[0.97] transition-all rounded-sm px-6 py-3 font-semibold text-body text-text-primary"
>
{block.ctaLabel}
</Link>
)}
</div>
);
case "richTextSection":
return <RichText key={block.id} content={block.content} />;
+11
View File
@@ -1293,6 +1293,7 @@ export async function getTrackingCodes(): Promise<TrackingCode[]> {
// full page section, not an inline prose element.
export type PageBlock =
| { blockType: "hero"; id: string; headline: string; subline: string | null; ctaLabel: string | null; ctaHref: string | null }
| { blockType: "richTextSection"; id: string; content: unknown }
| { blockType: "stepRow"; id: string; items: { id: string; icon: string; title: string; subtitle: string | null; description: string }[] }
| { blockType: "quote"; id: string; text: string; label: string | null }
@@ -1317,6 +1318,7 @@ export type Page = {
type PayloadPageImageField = { url?: string | null } | number | null;
type PayloadPageBlock =
| { blockType: "hero"; id: string; headline: string; subline?: string | null; ctaLabel?: string | null; ctaHref?: string | null }
| { blockType: "richTextSection"; id: string; content: unknown }
| { blockType: "stepRow"; id: string; items: { id: string; icon: string; title: string; subtitle?: string | null; description: string }[] }
| { blockType: "quote"; id: string; text: string; label?: string | null }
@@ -1358,6 +1360,15 @@ function mapPayloadPageBlock(block: PayloadPageBlock): PageBlock {
id: block.id,
items: block.items.map((item) => ({ ...item, subtitle: item.subtitle ?? null })),
};
case "hero":
return {
blockType: "hero",
id: block.id,
headline: block.headline,
subline: block.subline ?? null,
ctaLabel: block.ctaLabel ?? null,
ctaHref: block.ctaHref ?? null,
};
default:
return block;
}