From 21c4e9f007f64309b31dec423496a14c62d16050 Mon Sep 17 00:00:00 2001 From: Marco Date: Thu, 23 Jul 2026 11:10:02 +0000 Subject: [PATCH] Link Impressum's seller identity to company-settings, not hand-typed prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "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 --- app/components/RichText.tsx | 8 +- app/impressum/components/AnbieterAngaben.tsx | 92 ++++++++++++++++++++ app/impressum/page.tsx | 18 +++- 3 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 app/impressum/components/AnbieterAngaben.tsx diff --git a/app/components/RichText.tsx b/app/components/RichText.tsx index 6fbac34..129ac05 100644 --- a/app/components/RichText.tsx +++ b/app/components/RichText.tsx @@ -35,7 +35,13 @@ function plainText(node: LexicalNode): string { // "section-1" id from the leading number — immune to copy edits changing // the heading text later, unlike a text-derived slug. Anything else // (headings with no leading number) falls back to a plain slugify. -function headingId(text: string): string { +// Exported — the Impressum page renders some of its own headings outside +// this CMS-driven richText (the "Angaben zum Anbieter"/"Umsatzsteuer"/ +// "Verantwortlich für den Inhalt" sections come straight from +// company-settings, not the richText field, see app/impressum/page.tsx) +// and needs the exact same id-assignment logic so its SectionTOC entries +// actually match the ids those headings render with. +export function headingId(text: string): string { const numbered = text.match(/^(\d+)\./); if (numbered) return `section-${numbered[1]}`; return text diff --git a/app/impressum/components/AnbieterAngaben.tsx b/app/impressum/components/AnbieterAngaben.tsx new file mode 100644 index 0000000..d774c9f --- /dev/null +++ b/app/impressum/components/AnbieterAngaben.tsx @@ -0,0 +1,92 @@ +import type { ReactNode } from "react"; +import { headingId } from "../../components/RichText"; +import type { CompanySettings } from "../../lib/payload"; +import type { TOCSection } from "../../components/SectionTOC"; + +// Renders "Angaben zum Anbieter"/"Umsatzsteuer"/(conditionally) +// "Handelsregister"/"Geschäftsführung"/"Verantwortlich für den Inhalt" +// straight from company-settings, matching RichText.tsx's own heading/ +// paragraph classes so it reads as one continuous page with the CMS +// content below it, not a bolted-on block. This used to be hand-typed +// prose baked into the Impressum's richText (seed-legal-pages.ts on the +// Payload side) — duplicated, and silently out of date the moment an +// admin changed company-settings without also remembering to re-edit the +// Impressum text by hand. Single-sourced here instead, same "structural/ +// brand elements in code, only pull the actual numbers/copy that need +// single-sourcing from data" pattern this page's own Nachhaltigkeit card +// already uses (see page.tsx's comment on that). +// +// Also closes a real compliance gap the old hand-typed text had: it never +// showed Handelsregister/Geschäftsführung at all, even though +// company-settings already models both (§37a HGB/§35a GmbHG) — those +// fields just weren't wired into the Impressum. A sole proprietorship +// (this shop's current legalForm) has neither, so neither section shows +// today, but the moment that changes in company-settings, the Impressum +// picks it up automatically instead of needing a second manual edit. +export function anbieterAngabenHeadings(seller: CompanySettings | null): TOCSection[] { + if (!seller) return []; + const sections = ["Angaben zum Anbieter", "Umsatzsteuer"]; + if (seller.registerCourt && seller.registerNumber) sections.push("Handelsregister"); + if (seller.managingDirector) sections.push("Geschäftsführung"); + sections.push("Verantwortlich für den Inhalt"); + return sections.map((title) => ({ id: headingId(title), title })); +} + +function Heading({ children }: { children: string }) { + return ( +

+ {children} + +

+ ); +} + +function P({ children }: { children: ReactNode }) { + return

{children}

; +} + +export function AnbieterAngaben({ seller }: { seller: CompanySettings }) { + return ( +
+ Angaben zum Anbieter +

{seller.sellerName}

+

{seller.sellerStreet}

+

+ {seller.sellerZip} {seller.sellerCity} +

+

{seller.sellerCountry}

+

E-Mail: {seller.sellerEmail}

+ + Umsatzsteuer +

Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz:

+

{seller.vatId}

+ + {seller.registerCourt && seller.registerNumber && ( + <> + Handelsregister +

{seller.registerCourt}

+

{seller.registerNumber}

+ + )} + + {seller.managingDirector && ( + <> + Geschäftsführung +

{seller.managingDirector}

+ + )} + + Verantwortlich für den Inhalt +

{seller.managingDirector || seller.sellerName}

+

{seller.sellerStreet}

+

+ {seller.sellerZip} {seller.sellerCity} +

+

{seller.sellerCountry}

+
+ ); +} diff --git a/app/impressum/page.tsx b/app/impressum/page.tsx index 7d1c533..0452957 100644 --- a/app/impressum/page.tsx +++ b/app/impressum/page.tsx @@ -7,7 +7,8 @@ import { Footer } from "../components/Footer"; import { RichText, extractHeadings } from "../components/RichText"; import { LiveRichText } from "../components/LiveRichText"; import { SectionTOC } from "../components/SectionTOC"; -import { getLegalPage } from "../lib/payload"; +import { getLegalPage, getCompanySettings } from "../lib/payload"; +import { AnbieterAngaben, anbieterAngabenHeadings } from "./components/AnbieterAngaben"; export const metadata: Metadata = { title: "Impressum", @@ -17,8 +18,11 @@ export const metadata: Metadata = { export default async function ImpressumPage() { const { isEnabled: isPreview } = await draftMode(); - const page = await getLegalPage("impressum", { draft: isPreview }); - const headings = page ? extractHeadings(page.content) : []; + 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 ( <> @@ -66,7 +70,13 @@ export default async function ImpressumPage() { -
+
+ {/* 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 && } {page ? ( isPreview ? : ) : (