Files
einfach-produktiv/app/impressum/page.tsx
T
Marco 21c4e9f007 Link Impressum's seller identity to company-settings, not hand-typed prose
"Angaben zum Anbieter"/"Umsatzsteuer"/"Handelsregister"/"Geschäftsführung"/
"Verantwortlich für den Inhalt" used to be hand-typed into the Impressum's
richText content (seed-legal-pages.ts) with no connection to the same
seller data the invoice PDFs and every email footer already pull from
company-settings — an admin updating one had no reason to remember the
other existed, and the old text was already stale in one concrete way:
it never showed Handelsregister/Geschäftsführung at all even though
company-settings has modeled both since the legal-form work shipped.

Now rendered by a new AnbieterAngaben component, straight from
getCompanySettings(), positioned above the CMS richText (which keeps
only genuinely editorial content: Kontakt, Haftung für Inhalte, Haftung
für Links, Urheberrecht). Same "structural/brand elements in code, only
pull the actual numbers/copy that need single-sourcing from data"
pattern this page's own Nachhaltigkeit sidebar card already used.

RichText.tsx's headingId() is now exported so the new block's headings
get the exact same id-assignment logic as CMS-driven ones, keeping the
SectionTOC sidebar's ids in sync with both sources.

/impressum moves from static to dynamic rendering (it now fetches live
company-settings data, cache: "no-store") — an acceptable tradeoff for
a legally-required page to never show stale seller info.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 11:10:02 +00:00

92 lines
4.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 Image from "next/image";
import { draftMode } from "next/headers";
import { Reveal } from "../components/Reveal";
import { Footer } from "../components/Footer";
import { RichText, extractHeadings } from "../components/RichText";
import { LiveRichText } from "../components/LiveRichText";
import { SectionTOC } from "../components/SectionTOC";
import { getLegalPage, getCompanySettings } from "../lib/payload";
import { AnbieterAngaben, anbieterAngabenHeadings } from "./components/AnbieterAngaben";
export const metadata: Metadata = {
title: "Impressum",
description: "Angaben gemäß § 5 TMG für einfach produktiv.",
alternates: { canonical: "/impressum" },
};
export default async function ImpressumPage() {
const { isEnabled: isPreview } = await draftMode();
const [page, seller] = await Promise.all([getLegalPage("impressum", { draft: isPreview }), getCompanySettings()]);
// Anbieter-Angaben headings first — that block renders above the CMS
// content below, so its TOC entries need to lead too, or the sidebar
// would list sections in a different order than they actually appear.
const headings = [...anbieterAngabenHeadings(seller), ...(page ? extractHeadings(page.content) : [])];
return (
<>
<main className="flex flex-col flex-1 bg-bg-base">
<Reveal className="flex flex-col gap-3 items-start pb-6 pt-10 px-[var(--layout-padding-x)] w-full">
<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">Impressum</span>
</p>
<p
className="font-semibold text-h-feature text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
>
Impressum
</p>
<p className="text-body text-text-muted">Angaben gemäß § 5 TMG</p>
</Reveal>
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 items-start pb-16 pt-2 px-[var(--layout-padding-x)] w-full">
<div className="hidden lg:flex flex-col gap-6 w-[22.5rem] shrink-0 lg:sticky lg:top-32 lg:self-start">
<SectionTOC sections={headings} />
{/* Static, not part of the CMS content — the LegalPages
collection's richText field is deliberately generic
(shared shape across Impressum/Datenschutz/AGB/Widerruf),
so a bespoke brand callout like this doesn't belong inside
it; hardcoding it here matches how VersandSections.tsx
keeps structural/brand elements in code and only pulls
the actual numbers/copy that need single-sourcing from
data. */}
<div className="bg-bg-muted flex flex-col gap-3 items-start p-6 rounded-md w-full">
<Image alt="" src="/icon-trust-leaf.png" width={28} height={28} className="size-7 object-contain" />
<p
className="font-semibold text-body text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
>
Nachhaltigkeit
</p>
<p className="text-body-sm text-text-muted">
Diese Website wird mit Ökostrom betrieben und ist klimafreundlich gehostet.
</p>
<p className="text-body-sm text-text-muted">Für eine bessere Zukunft für uns alle.</p>
<div className="h-[0.125rem] w-8 bg-brand" />
</div>
</div>
<div className="w-full lg:flex-1 min-w-0 flex flex-col gap-8">
{/* Seller identity (name/address/USt-ID/Handelsregister/
Geschäftsführung) comes straight from company-settings, not
the CMS richText below — single-sourced so it can never
drift out of sync with the same data the invoice PDFs and
every email footer already use. See AnbieterAngaben.tsx. */}
{seller && <AnbieterAngaben seller={seller} />}
{page ? (
isPreview ? <LiveRichText initialContent={page.content} /> : <RichText content={page.content} />
) : (
<p className="text-body text-text-muted">Inhalte werden gerade aktualisiert.</p>
)}
</div>
</div>
</main>
<Footer />
</>
);
}