import type { Metadata } from "next"; import { Fragment } from "react"; import { notFound } from "next/navigation"; import Link from "next/link"; import { Container, Button } from "@/components/ui"; import { ScrollReveal } from "@/components/scroll-reveal"; import { ProductGallery } from "@/components/product-gallery"; import { TasteScale, AddToCartWithQuantity, ProductCard } from "@/components/product-card"; import { RecordProductView } from "@/components/record-product-view"; import { RecentlyViewedSection } from "@/components/recently-viewed-section"; import { StarRating } from "@/components/star-rating"; import { TrustBadges } from "@/components/trust-badges"; import { StickyAddToCartBar } from "@/components/sticky-add-to-cart-bar"; import { getProductBySlug, getRelatedProducts } from "@/lib/data"; import { formatPrice, formatPricePerKg, formatDate } from "@/lib/format"; import { siteConfig } from "@/lib/site-config"; import { getCompanyInfo } from "@/lib/company-info"; import { getCurrentCustomer } from "@/lib/auth"; import { getApprovedReviews, getReviewSummary, getCustomerReview, hasCustomerPurchased } from "@/lib/reviews"; import { isProductWishlisted } from "@/lib/wishlist"; import { getShippingTiers } from "@/lib/shipping"; import { resolveShippingCostCents } from "@/lib/shipping-calc"; import { submitReviewAction, toggleWishlistAction, subscribeStockNotificationAction } from "../actions"; import { getLocale } from "@/i18n/locale"; import { getDictionary } from "@/i18n/get-dictionary"; import type { WissenSlug } from "@/i18n/dictionary-type"; // Produktdaten (Preis, Bestand, Charge) aendern sich laufend — Rendering pro // Request statt statischem Build entkoppelt zudem den Docker-Build-Schritt // von einer zur Build-Zeit erreichbaren Datenbank. export const dynamic = "force-dynamic"; export async function generateMetadata( props: PageProps<"/honig/[slug]"> ): Promise { const { slug } = await props.params; const product = await getProductBySlug(slug); if (!product) return {}; return { title: product.name, description: `${product.name} — ${product.tasteTags.join(", ")}. ${product.weightGrams} g aus eigener Imkerei in Altenmünster.`, alternates: { canonical: `/honig/${slug}` }, }; } export default async function ProductDetailPage( props: PageProps<"/honig/[slug]"> ) { const locale = await getLocale(); const t = getDictionary(locale); const { slug } = await props.params; const { error, message, notifyError, notifyMessage } = await props.searchParams; const product = await getProductBySlug(slug); if (!product) notFound(); const isHoney = product.category === "HONIG" && product.harvest && product.batch; // Verlinkt die Ernteart auf den passenden Honigwissen-Artikel, sofern der // Ernte-Typ (freier Admin-Text, siehe Harvest.type) eindeutig als Frueh- // oder Sommertracht erkennbar ist — sonst einfach kein zusaetzlicher Link, // statt eine falsche Zuordnung zu raten. const harvestTypeLower = product.harvest?.type.toLowerCase() ?? ""; const harvestWissenSlug: WissenSlug | null = harvestTypeLower.includes("früh") ? "was-bedeutet-fruehtracht" : harvestTypeLower.includes("sommer") ? "was-bedeutet-sommertracht" : null; const stock = product.inventory?.quantity ?? 0; const inStock = stock > 0; const lowStock = inStock && stock <= 5; const customer = await getCurrentCustomer(); const [reviews, reviewSummary, relatedProducts, ownReview, canReview, isWishlisted, company, shippingTiers] = await Promise.all([ getApprovedReviews(product.id), getReviewSummary(product.id), getRelatedProducts(product.id, product.category), customer ? getCustomerReview(product.id, customer.id) : Promise.resolve(null), customer ? hasCustomerPurchased(product.id, customer.id) : Promise.resolve(false), customer ? isProductWishlisted(product.id, customer.id) : Promise.resolve(false), getCompanyInfo(), getShippingTiers(), ]); // "Ab"-Preis fuer eine einzelne Einheit — die Staffel, die beim Kauf genau // dieses einen Produkts greifen wuerde. Macht Versandkosten schon auf der // Produktseite sichtbar statt erst im Warenkorb/Checkout (Kaufentscheidung // haengt bei einem kleinen Versandhandel oft direkt daran). const baseShippingCents = resolveShippingCostCents(shippingTiers, 1); const jsonLd = { "@context": "https://schema.org", "@type": "Product", name: product.name, description: product.description, sku: product.batch?.code ?? product.slug, brand: { "@type": "Brand", name: company.name }, category: t.common.categories[product.category], // Nur bei echter Produktfotografie gesetzt (siehe ProductGallery) — ohne // hochgeladene Bilder faellt die Galerie auf reine CSS-Platzhalterkacheln // zurueck, die als "image" im strukturierten Datensatz falsche Tatsachen // vortaeuschen wuerden (Google Rich-Results/Merchant-Anforderung: absolute // URL zu einem echten Produktfoto, keine Platzhaltergrafik). ...(product.images.length > 0 ? { image: product.images.map((src) => `https://${siteConfig.domain}${src}`) } : {}), offers: { "@type": "Offer", priceCurrency: product.currency, price: (product.priceCents / 100).toFixed(2), availability: inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock", url: `https://wabenhain.com/honig/${product.slug}`, }, ...(reviewSummary.count > 0 ? { aggregateRating: { "@type": "AggregateRating", ratingValue: reviewSummary.average?.toFixed(1), reviewCount: reviewSummary.count, }, } : {}), }; const breadcrumbJsonLd = { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: [ { "@type": "ListItem", position: 1, name: "Shop", item: `https://${siteConfig.domain}/shop` }, { "@type": "ListItem", position: 2, name: t.common.categories[product.category], item: `https://${siteConfig.domain}/shop?kategorie=${product.category}`, }, { "@type": "ListItem", position: 3, name: product.name, item: `https://${siteConfig.domain}/honig/${product.slug}`, }, ], }; return (