From 56fae6ff13e9a4a28f5c9cf4d22b785f524fdf0d Mon Sep 17 00:00:00 2001 From: Marco Date: Wed, 26 Aug 2026 21:20:44 +0000 Subject: [PATCH] Product JSON-LD: add sku + gallery images, single-source the meta description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sku was tracked in Payload but never exposed to the frontend at all — added to the Product type and wired into buildProductSchema (a real Schema.org property Google's rich-result validator checks for). image is now an array (main + gallery) when a product has gallery photos, instead of always just the one main image. /der-eine's page metadata description was a separately hardcoded string that had drifted from Products.description (the one JSON-LD/ cart/checkout actually use) — now reads from the live product via generateMetadata, same pattern /tasse-die-pause already uses. Updated Payload's own description field to the more product-descriptive text that used to live only in that hardcoded string. --- app/der-eine/page.tsx | 31 ++++++++++++++-------------- app/lib/__tests__/cartTotals.test.ts | 1 + app/lib/payload.ts | 6 ++++++ app/lib/structuredData.ts | 3 ++- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/app/der-eine/page.tsx b/app/der-eine/page.tsx index 29014a2..dc103c4 100644 --- a/app/der-eine/page.tsx +++ b/app/der-eine/page.tsx @@ -9,25 +9,24 @@ import { getProductBySlug, getCompanySettings } from "../lib/payload"; import { buildProductSchema } from "../lib/structuredData"; const title = "Der Eine. – Für die eine Sache, die gerade zählt."; -const description = - "Der Eine. ist ein schlichter Kugelschreiber aus Metall. Angenehm in der Hand, klein genug für die Tasche und gemacht für alles, was kurz raus aus dem Kopf und irgendwo hin muss."; -export const metadata: Metadata = { - title, - description, - alternates: { - canonical: "/der-eine", - }, - openGraph: { +// Description reads from the live product instead of a separately +// hardcoded string, unlike this page's own `title` — matches +// /tasse-die-pause's own pattern (see that page's comment). Two +// hand-maintained copies of the same text used to drift apart: this one +// used to say something different from Products.description (which feeds +// 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; + return { title, description, - url: "/der-eine", - }, - twitter: { - title, - description, - }, -}; + alternates: { canonical: "/der-eine" }, + openGraph: { title, description, url: "/der-eine" }, + twitter: { title, description }, + }; +} export default async function DerEinePage() { const [product, seller] = await Promise.all([ diff --git a/app/lib/__tests__/cartTotals.test.ts b/app/lib/__tests__/cartTotals.test.ts index 7423766..d264d29 100644 --- a/app/lib/__tests__/cartTotals.test.ts +++ b/app/lib/__tests__/cartTotals.test.ts @@ -7,6 +7,7 @@ const product = (overrides: Partial = {}): Product => ({ numericId: 1, name: "ToDo-Karten", description: "", + sku: null, price: 12.9, compareAtPrice: null, image: "", diff --git a/app/lib/payload.ts b/app/lib/payload.ts index 5d58524..f100736 100644 --- a/app/lib/payload.ts +++ b/app/lib/payload.ts @@ -182,6 +182,10 @@ export type Product = { numericId: number; name: string; description: 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. + sku: string | null; price: number; compareAtPrice: number | null; image: string; @@ -264,6 +268,7 @@ type PayloadProduct = { name: string; slug: string; description: string | null; + sku: string | null; price: number; compareAtPrice: number | null; image: { url: string } | number | null; @@ -330,6 +335,7 @@ export function mapPayloadProduct(product: PayloadProduct): Product { numericId: product.id, name: product.name, description: product.description ?? "", + sku: product.sku || null, price: product.price, compareAtPrice: product.compareAtPrice ?? null, image: typeof product.image === "object" && product.image ? product.image.url : "", diff --git a/app/lib/structuredData.ts b/app/lib/structuredData.ts index 8a81194..94fe8dd 100644 --- a/app/lib/structuredData.ts +++ b/app/lib/structuredData.ts @@ -54,8 +54,9 @@ export function buildProductSchema(product: Product, url: string, seller: Compan "@type": "Product", name: product.name, description: product.description, - image: product.image, + image: product.gallery.length > 0 ? [product.image, ...product.gallery] : product.image, url, + ...(product.sku ? { sku: product.sku } : {}), // No reviews/ratings system exists yet — `aggregateRating` is // optional in the spec and deliberately omitted rather than faked; // add it here once real reviews exist, not before.