From 7489f8356450567c42f1b995f34cb08066034d89 Mon Sep 17 00:00:00 2001
From: Marco
Date: Wed, 26 Aug 2026 22:04:28 +0000
Subject: [PATCH] Follow Products.description consolidation to richText-only
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Backend dropped the plain-text description field in favor of a single
richText one (see docker/payload commit 1ba8d07). Renders formatted
(bold/italic/multiple paragraphs) via the shared RichText component on
the PDP (der-eine, tasse-die-pause); everywhere else (Passend-dazu cards,
Product JSON-LD, homepage spotlight fallback) derives plain text at read
time via a new extractPlainText() helper instead of a second field.
Removes the description line from cart line items entirely — it only
bloated the cart with no real benefit there.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_01J1Hu5bZ1kZUgKhab6yNwCt
---
app/blog/[slug]/page.tsx | 2 +-
app/cart/components/CartContent.tsx | 1 -
app/components/ProductSpotlight.tsx | 2 +-
app/der-eine/components/PasstDazu.tsx | 2 +-
app/der-eine/page.tsx | 2 +-
app/lib/__tests__/cartTotals.test.ts | 3 ++-
app/lib/payload.ts | 29 +++++++++++++++++++---
app/lib/structuredData.ts | 2 +-
app/tasse-die-pause/components/Hero.tsx | 3 ++-
app/tasse-die-pause/components/Pricing.tsx | 3 ++-
app/tasse-die-pause/page.tsx | 2 +-
11 files changed, 38 insertions(+), 13 deletions(-)
diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx
index 2e6184d..60d077a 100644
--- a/app/blog/[slug]/page.tsx
+++ b/app/blog/[slug]/page.tsx
@@ -162,7 +162,7 @@ export default async function BlogDetailPage({
>
{post.relatedProduct.name}
- {post.relatedProduct.description}
+ {post.relatedProduct.descriptionText}
Entdecken
diff --git a/app/cart/components/CartContent.tsx b/app/cart/components/CartContent.tsx
index d088039..27d8469 100644
--- a/app/cart/components/CartContent.tsx
+++ b/app/cart/components/CartContent.tsx
@@ -276,7 +276,6 @@ export function CartContent({
equal-height pressure from neighboring lines, so a
plain conditional line is enough here. */}
{lowStock && Nur noch wenige verfügbar
}
- {product.description}
Einzelpreis
diff --git a/app/components/ProductSpotlight.tsx b/app/components/ProductSpotlight.tsx
index 782b3c8..ba8d5ad 100644
--- a/app/components/ProductSpotlight.tsx
+++ b/app/components/ProductSpotlight.tsx
@@ -134,7 +134,7 @@ export async function ProductSpotlight() {
>
{product.spotlightHeadline || product.name}
-
+
{/* MwSt./Versand disclosure on its own line, not crammed into
the price row itself — same reasoning as todo-cards'
Pricing.tsx (identical text, same narrow-column risk). */}
diff --git a/app/der-eine/components/PasstDazu.tsx b/app/der-eine/components/PasstDazu.tsx
index e9709bd..8119771 100644
--- a/app/der-eine/components/PasstDazu.tsx
+++ b/app/der-eine/components/PasstDazu.tsx
@@ -36,7 +36,7 @@ export async function PasstDazu({ productSlug }: { productSlug: string }) {
>
{related.name}
-
{related.description}
+
{related.descriptionText}
Entdecken
diff --git a/app/der-eine/page.tsx b/app/der-eine/page.tsx
index dc103c4..3b0d65b 100644
--- a/app/der-eine/page.tsx
+++ b/app/der-eine/page.tsx
@@ -18,7 +18,7 @@ const title = "Der Eine. – Für die eine Sache, die gerade zählt.";
// the cart/checkout/JSON-LD elsewhere) — same field, one source now.
export async function generateMetadata(): Promise {
const product = await getProductBySlug("stift-kugelschreiber");
- const description = product?.description ?? undefined;
+ const description = product?.descriptionText || undefined;
return {
title,
description,
diff --git a/app/lib/__tests__/cartTotals.test.ts b/app/lib/__tests__/cartTotals.test.ts
index d264d29..540a408 100644
--- a/app/lib/__tests__/cartTotals.test.ts
+++ b/app/lib/__tests__/cartTotals.test.ts
@@ -6,7 +6,8 @@ const product = (overrides: Partial = {}): Product => ({
id: "todo-karten",
numericId: 1,
name: "ToDo-Karten",
- description: "",
+ description: null,
+ descriptionText: "",
sku: null,
price: 12.9,
compareAtPrice: null,
diff --git a/app/lib/payload.ts b/app/lib/payload.ts
index f100736..867e871 100644
--- a/app/lib/payload.ts
+++ b/app/lib/payload.ts
@@ -181,7 +181,12 @@ export type Product = {
// slug-based call site.
numericId: number;
name: string;
- description: string;
+ // Raw Lexical richText JSON — rendered formatted (bold/italic/multiple
+ // paragraphs) on the PDP via the shared component. Plain-text
+ // consumers (Passend-dazu cards, JSON-LD) use `descriptionText` below
+ // instead of re-deriving text from this themselves.
+ description: unknown | null;
+ descriptionText: string;
// Not shown anywhere in the UI today — only consumed by
// buildProductSchema (JSON-LD's Product.sku), a real Schema.org property
// Google's rich-result validator looks for.
@@ -267,7 +272,7 @@ type PayloadProduct = {
id: number;
name: string;
slug: string;
- description: string | null;
+ description: unknown | null;
sku: string | null;
price: number;
compareAtPrice: number | null;
@@ -325,16 +330,34 @@ function maxPurchasableQty(trackInventory: boolean, stock: number | null, allowB
return trackInventory && !allowBackorder ? (stock ?? 0) : null;
}
+type LexicalTextNode = {
+ text?: string;
+ children?: LexicalTextNode[];
+};
+
+// Mirrors the backend's own Posts.ts extractPlainText (same shape, same
+// space-joined fallback for multiple paragraphs/children) — kept as a
+// separate copy rather than a shared package since it's a few lines and
+// the two run in different runtimes (Payload server vs. this frontend).
+function extractPlainText(node: LexicalTextNode | null | undefined): string {
+ if (!node) return "";
+ if (typeof node.text === "string") return node.text;
+ if (Array.isArray(node.children)) return node.children.map(extractPlainText).join(" ");
+ return "";
+}
+
// Shared by getProducts() and getPostBySlug()'s relatedProduct — kept in
// one place instead of duplicating the same field mapping, which is
// exactly the kind of drift this session's Shipping Settings work was
// about eliminating elsewhere.
export function mapPayloadProduct(product: PayloadProduct): Product {
+ const descriptionRoot = (product.description as { root?: LexicalTextNode } | null)?.root;
return {
id: product.slug,
numericId: product.id,
name: product.name,
- description: product.description ?? "",
+ description: product.description ?? null,
+ descriptionText: extractPlainText(descriptionRoot),
sku: product.sku || null,
price: product.price,
compareAtPrice: product.compareAtPrice ?? null,
diff --git a/app/lib/structuredData.ts b/app/lib/structuredData.ts
index 94fe8dd..9a073a8 100644
--- a/app/lib/structuredData.ts
+++ b/app/lib/structuredData.ts
@@ -53,7 +53,7 @@ export function buildProductSchema(product: Product, url: string, seller: Compan
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
- description: product.description,
+ description: product.descriptionText,
image: product.gallery.length > 0 ? [product.image, ...product.gallery] : product.image,
url,
...(product.sku ? { sku: product.sku } : {}),
diff --git a/app/tasse-die-pause/components/Hero.tsx b/app/tasse-die-pause/components/Hero.tsx
index 6339bc7..82248fe 100644
--- a/app/tasse-die-pause/components/Hero.tsx
+++ b/app/tasse-die-pause/components/Hero.tsx
@@ -2,6 +2,7 @@ import Link from "next/link";
import Image from "next/image";
import { AddToCartButton } from "../../components/AddToCartButton";
import { Reveal } from "../../components/Reveal";
+import { RichText } from "../../components/RichText";
import { getProductBySlug, getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer } from "../../lib/payload";
import { formatPrice, discountPercent } from "../../lib/format";
import { effectiveTaxRate } from "../../lib/cartTotals";
@@ -50,7 +51,7 @@ export async function Hero() {
{product?.name ?? "Tasse"}
- {product?.description && {product.description}
}
+ {product?.description ? : null}
{product && (
diff --git a/app/tasse-die-pause/components/Pricing.tsx b/app/tasse-die-pause/components/Pricing.tsx
index 9bfe5e8..5bab60a 100644
--- a/app/tasse-die-pause/components/Pricing.tsx
+++ b/app/tasse-die-pause/components/Pricing.tsx
@@ -1,6 +1,7 @@
import Image from "next/image";
import { AddToCartButton } from "../../components/AddToCartButton";
import { Reveal } from "../../components/Reveal";
+import { RichText } from "../../components/RichText";
import { getProductBySlug, getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer } from "../../lib/payload";
import { formatPrice, discountPercent } from "../../lib/format";
import { effectiveTaxRate } from "../../lib/cartTotals";
@@ -48,7 +49,7 @@ export async function Pricing() {
>
{product.name}
- {product.description &&
{product.description}
}
+ {product.description ?
: null}
diff --git a/app/tasse-die-pause/page.tsx b/app/tasse-die-pause/page.tsx
index f764a1a..c8b3040 100644
--- a/app/tasse-die-pause/page.tsx
+++ b/app/tasse-die-pause/page.tsx
@@ -11,7 +11,7 @@ import { buildProductSchema } from "../lib/structuredData";
export async function generateMetadata(): Promise {
const product = await getProductBySlug("tasse-die-pause");
const title = product ? `${product.name} – einfach produktiv.` : "Tasse – einfach produktiv.";
- const description = product?.description ?? undefined;
+ const description = product?.descriptionText || undefined;
return {
title,
description,