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,