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>
This commit is contained in:
@@ -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 (
|
||||
<h2
|
||||
id={headingId(children)}
|
||||
className="font-semibold text-h-small text-text-primary mt-2 scroll-mt-32 first:mt-0"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
{children}
|
||||
<span className="block h-[0.125rem] w-8 bg-brand mt-2" aria-hidden />
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
function P({ children }: { children: ReactNode }) {
|
||||
return <p className="text-body text-text-body">{children}</p>;
|
||||
}
|
||||
|
||||
export function AnbieterAngaben({ seller }: { seller: CompanySettings }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<Heading>Angaben zum Anbieter</Heading>
|
||||
<P>{seller.sellerName}</P>
|
||||
<P>{seller.sellerStreet}</P>
|
||||
<P>
|
||||
{seller.sellerZip} {seller.sellerCity}
|
||||
</P>
|
||||
<P>{seller.sellerCountry}</P>
|
||||
<P>E-Mail: {seller.sellerEmail}</P>
|
||||
|
||||
<Heading>Umsatzsteuer</Heading>
|
||||
<P>Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz:</P>
|
||||
<P>{seller.vatId}</P>
|
||||
|
||||
{seller.registerCourt && seller.registerNumber && (
|
||||
<>
|
||||
<Heading>Handelsregister</Heading>
|
||||
<P>{seller.registerCourt}</P>
|
||||
<P>{seller.registerNumber}</P>
|
||||
</>
|
||||
)}
|
||||
|
||||
{seller.managingDirector && (
|
||||
<>
|
||||
<Heading>Geschäftsführung</Heading>
|
||||
<P>{seller.managingDirector}</P>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Heading>Verantwortlich für den Inhalt</Heading>
|
||||
<P>{seller.managingDirector || seller.sellerName}</P>
|
||||
<P>{seller.sellerStreet}</P>
|
||||
<P>
|
||||
{seller.sellerZip} {seller.sellerCity}
|
||||
</P>
|
||||
<P>{seller.sellerCountry}</P>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+14
-4
@@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:flex-1 min-w-0">
|
||||
<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} />
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user