fix: SEO- und Barrierefreiheits-Luecken auf Kaufentscheidungsseiten schliessen

Konkrete, verifizierte Luecken auf den Seiten, die Kund:innen von Google
aus oder per Link erreichen:

- Product-JSON-LD auf der Produktdetailseite ergaenzt um "image" (absolute
  URLs der hochgeladenen Produktfotos) — fehlte bisher trotz vorhandener
  Bilder und ist fuer Google Rich-Results/Merchant-Listing mit Sternebewertung
  + Preis im Suchergebnis erforderlich.
- Alt-Text fuer echte (hochgeladene) Bilder aussagekraeftiger: ContentImage
  uebernimmt jetzt das bereits vorhandene, bisher nur auf Platzhalterkacheln
  sichtbare "label" (Ernte-Jahr/Typ, Kategorie, Ort) mit in den Alt-Text —
  wirkt projektweit auf Produktkarten, Ernte-Uebersicht/-Detail und
  Produkt-Teaser. Produktgalerie-Hauptbild zeigt zusaetzlich die
  Geschmacksnoten im Alt-Text statt nur des Produktnamens.
- Checkout: allgemeine Fehlermeldung nach fehlgeschlagenem Absenden bekommt
  role="alert" und Fokus wird aktiv dorthin verschoben — bisher rein farblich
  markiert, Screenreader-Nutzer:innen haetten den Fehler beim geldwerten
  Bestellabschluss sonst gar nicht mitbekommen.
- Cart-Drawer und Warenkorb-Seite: Lagerbestand-Hinweise (Ausverkauft/Nur
  noch N) treffen asynchron erst nach dem ersten Rendern ein — jetzt in
  einem persistenten aria-live-Wrapper, damit der Uebergang tatsaechlich
  angesagt wird statt stumm zu bleiben.

Geprueft und bereits solide befunden (keine Aenderung noetig): Shop-Kanonical
ignoriert bewusst Such-/Filter-Query-Parameter (kein Duplicate-Content-Risiko),
Sitemap deckt Produkt- und Ernte-Detailseiten mit echtem lastmod ab,
Produktgalerie-Tastaturbedienung ueber echte <button>-Kacheln mit
aria-label/aria-current, Cart-Drawer-Fokusfalle/Fokus-Rueckgabe, Mengen-Steller-
aria-live, Farbkontrast von text-honey-dark/text-red-700 auf dem Hintergrund
(~4.9:1 bzw. ~6.1:1, beide AA-konform).

tsc/vitest/build bleiben gruen (142 Tests).
This commit is contained in:
Nick Szaunig committed 2026-09-06 02:46:06 +02:00
1 parent 0b2196460a
commit ac432e261b
6 files changed
+67 -16

No files matched your search

+17 -2
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, type FormEvent, type HTMLAttributes } from "react";
import { useEffect, useRef, useState, type FormEvent, type HTMLAttributes } from "react";
import { useRouter } from "next/navigation";
import { Container, SectionHeading, Button } from "@/components/ui";
import { TrustBadges } from "@/components/trust-badges";
@@ -41,6 +41,17 @@ export function CheckoutForm({
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isGift, setIsGift] = useState(false);
const errorRef = useRef<HTMLParagraphElement>(null);
// Fokus aktiv auf die Fehlermeldung verschieben statt sie nur farblich
// einzublenden — sonst bleibt der Fokus auf dem "Bestellen"-Button stehen
// und Screenreader-Nutzer:innen erfahren beim fehlgeschlagenen, geldwerten
// Checkout-Absenden nichts vom Fehler (role="alert" allein wird von
// manchen Screenreadern bei neu eingefuegten Elementen unzuverlaessig
// vorgelesen, Fokusverschiebung ist der robustere Mechanismus).
useEffect(() => {
if (error) errorRef.current?.focus();
}, [error]);
// Live sichtbar statt erst nach der Weiterleitung zu Stripe — reagiert auf
// die Versand/Abholung-Auswahl, damit die Gesamtsumme immer stimmt.
@@ -234,7 +245,11 @@ export function CheckoutForm({
) : null}
</div>
{error ? <p className="text-sm text-red-700">{error}</p> : null}
{error ? (
<p ref={errorRef} role="alert" tabIndex={-1} className="text-sm text-red-700 outline-none">
{error}
</p>
) : null}
<Button type="submit" disabled={submitting} className="w-full sm:w-auto">
{submitting ? t.checkoutPage.submitting : t.checkoutPage.placeOrder}
+13 -1
View File
@@ -82,6 +82,14 @@ export default async function ProductDetailPage(
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,
@@ -134,7 +142,11 @@ export default async function ProductDetailPage(
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd).replace(/</g, "\\u003c") }}
/>
<Container className="grid md:grid-cols-2 gap-14">
<ProductGallery images={product.images} productName={product.name} />
<ProductGallery
images={product.images}
productName={product.name}
description={product.tasteTags.join(", ")}
/>
<div>
<p className="tracking-wide-caps text-xs text-honey-dark mb-3">
+10 -5
View File
@@ -78,11 +78,16 @@ export function WarenkorbContent({
{item.name}
</Link>
<p className="text-sm text-ink/65">{item.weightGrams} g</p>
{itemStock !== undefined && itemStock === 0 ? (
<p className="mt-1 text-xs text-red-700">{t.product.outOfStock}</p>
) : itemStock !== undefined && itemStock <= 5 ? (
<p className="mt-1 text-xs text-honey-dark">{t.product.onlyLeft(itemStock)}</p>
) : null}
{/* Siehe Kommentar im Cart-Drawer (site-chrome.tsx): persistenter
aria-live-Wrapper, damit der erst nach dem ersten Rendern
eintreffende Live-Bestand als Ansage bei Screenreadern ankommt. */}
<div aria-live="polite">
{itemStock !== undefined && itemStock === 0 ? (
<p className="mt-1 text-xs text-red-700">{t.product.outOfStock}</p>
) : itemStock !== undefined && itemStock <= 5 ? (
<p className="mt-1 text-xs text-honey-dark">{t.product.onlyLeft(itemStock)}</p>
) : null}
</div>
</div>
<div className="flex items-center gap-4 ml-auto">
<QuantityStepper