Die letzte Stelle, an der ein hochgeladenes Bild noch als rohes <img>
ausgeliefert wurde. Der Warenkorb lud damit fuer JEDE Position das
vollstaendig gespeicherte Bild — bis MAX_IMAGE_DIMENSION Kantenlaenge, also
durchaus ein paar hundert Kilobyte — nur um es auf 64 bzw. 80 Pixel zu
verkleinern. Bei mehreren Positionen summierte sich das spuerbar, gerade im
Mobilfunknetz, und betraf sowohl die Warenkorb-Schublade als auch die
Warenkorbseite.
Der bisherige eslint-disable-Kommentar begruendete das rohe <img> mit einer
"noch unbekannten externen Bildquelle". Das trifft nicht zu: das Bild kommt
aus Product.images[0] und liegt damit immer same-origin unter /uploads/...
(beim Speichern geprueft von isValidUploadPath in lib/uploads.ts).
next/image ist dafuer zustaendig und liefert jetzt eine 80px-Variante statt
der Originaldatei.
Beide Verwendungsstellen bekommen dieselbe leichte Eckenrundung wie die
uebrigen Bilder; vorher war die Schublade rounded-sm und die Warenkorbseite
gar nicht gerundet.
Vier offene Befunde standen einem sauberen Produktionslauf im Weg:
- recently-viewed-section und site-chrome riefen setState direkt im
Effektrumpf auf, um einen Leerzustand herzustellen ("keine angesehenen
Produkte" bzw. "Eingabe kuerzer als zwei Zeichen"). Beides ergibt sich
unmittelbar aus vorhandenen Werten und wird jetzt beim Rendern
abgeleitet — das spart je einen zusaetzlichen Renderdurchlauf. Bei der
Suche verschwindet die Vorschlagsliste dadurch sofort beim Leeren des
Feldes statt erst nach dem naechsten Effektlauf.
- STAT_KEYS im Admin-Dashboard wurde ausschliesslich als Typ verwendet,
landete als `as const`-Array aber trotzdem im Bundle. Ersetzt durch
einen Typ-Alias.
- Ein DHL-Test importierte DhlApiError, ohne es zu benutzen, und pruefte
nur den Meldungstext — er haette damit jeden beliebigen anderen Fehler
mit passendem Text durchgehen lassen. Er prueft jetzt zusaetzlich den
Fehlertyp, wie seine beiden Geschwistertests.
Stand danach: eslint ohne Beanstandung (vorher 2 Fehler, 2 Warnungen),
tsc ohne Fehler, 201 Tests gruen, Produktions-Build erfolgreich.
Live Lighthouse + performance-trace audits against the running dev app
found two real, measurable defects:
- /checkout and /warenkorb rendered the cart-store's default empty
state before Zustand's persist middleware finished reading
localStorage, so any customer who already had items in their cart
saw an "empty cart" flash before the real form appeared (CLS 0.82).
Fixed by tracking hasHydrated in the store and rendering the
existing route skeleton until rehydration completes. Verified via
Chrome DevTools performance trace: CLS 0.82 -> 0.00 on both pages.
- axe-core (via Lighthouse) flagged text-ink/60 on the cream
background at 4.09:1 contrast, under the 4.5:1 WCAG AA minimum for
normal-size text. Bumped to text-ink/70 (5.61:1) everywhere the same
token is used at text-xs/text-sm across 7 files.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- format.ts: formatPrice/formatDate/formatDateTime/formatMonthYear nehmen
jetzt ein optionales Locale-Argument (Default "de" fuer unveraenderte
Aufrufer wie Rechnungen/Admin) und formatieren fuer "en" als en-GB statt
hart codiert als de-DE -- englischsprachige Kundschaft sah bisher trotz
gewaehlter Sprache "9,90 €" und "10. Mai 2026" statt "€9.90"/"10 May 2026".
Alle kundenseitigen Aufrufer (Produktseite, Warenkorb, Checkout, Bestell-
verfolgung, Konto-Bestellungen, Ernten-/Chargenseiten, Live-Suchvorschlaege,
Bestell-/Versand-E-Mails) reichen jetzt die aktive Locale durch.
- en.ts: openCart/shippingWithCount/shippingLabel pluralisieren "item(s)"
jetzt korrekt (vorher z.B. "Open cart, 1 items").
- product-gallery.tsx: Haupt- und Vorschaubilder nutzen jetzt next/image
statt <img> (hochgeladene Produktfotos sind immer same-origin unter
/uploads/..., brauchen also keine remotePatterns-Konfiguration wie der
entfernte Kommentar behauptete) -- Hauptbild mit priority/responsivem
sizes, da es auf der Produktseite das wahrscheinlichste LCP-Element ist.
- content-image.tsx: neue optionale priority-Prop; hero.tsx setzt sie fuer
das Startseiten-Hero-Bild (ebenfalls LCP-Kandidat).
- Sticky-Mobile-CTA-Leiste und useCartStock-Polling gegengeprueft: bereits
korrekt (pb-28-Platzreservierung bzw. Fetch nur bei Warenkorb-Aenderung
ohne Intervall) -- keine Aenderung noetig.
Verifiziert: tsc --noEmit clean, pnpm test (142/142 gruen), pnpm build erfolgreich.
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).
Konkrete Reibungspunkte entlang der Kauf-Journey behoben:
- Shop-Suche als sichtbares, editierbares Feld im Filter-Formular statt nur
in der kleinen Kopfzeilen-Suche — Suche/Kategorie/Sortierung/Verfuegbarkeit
lassen sich jetzt in beide Richtungen kombinieren, ohne sich gegenseitig
zurueckzusetzen; plus "Suche zuruecksetzen"-Link.
- Versandkosten- und Lieferzeit-Hinweis direkt auf der Produktdetailseite
(bisher erst im Warenkorb/Checkout sichtbar).
- Produktgalerie per Touch-Wisch bedienbar, nicht nur ueber die
Vorschau-Kacheln (mobile Kaufentscheidung).
- Immer sichtbare mobile Kaufleiste auf der Produktseite, damit der
Warenkorb-Button auf langen Seiten (Bewertungen, verwandte Produkte) nicht
wieder erst erscrollt werden muss.
- Live-Lagerbestand (Ausverkauft/Nur noch N) jetzt auch im Cart-Drawer und
auf der Warenkorb-Seite sichtbar, nicht nur auf der Produktkarte — neuer
Endpunkt /api/shop/cart-stock + useCartStock-Hook.
- Checkout- und Adressformulare: autoComplete/inputMode/type auf allen
Feldern (given-name, tel, postal-code, ...) fuer Mobil-Autofill und
passende Bildschirmtastaturen.
- Bestellpositionen in "Meine Bestellungen" und der Bestellverfolgung
verlinken jetzt zur Produktseite (samt Bewertungs-Anker) — bisher war die
Bewertungsfunktion nach dem Kauf nur durch manuelles Zurueeknavigieren
auffindbar.
Neue Tests fuer die Lagerbestand-Hinweise im Warenkorb; tsc/vitest/build
bleiben gruen (142 Tests).
Datenbank (prisma/migrations/20260904120000_add_performance_indexes):
7 fehlende Indizes auf Fremdschluessel-/Filter-Spalten ergaenzt
(Order.customerId+createdAt, Order.status, Order.createdAt,
Order.discountCodeId, OrderItem.orderId, Address.customerId,
AuditLog.createdAt) — bislang volle Tabellenscans bei Bestelluebersicht,
Admin-Dashboard, Checkout-Gutscheinpruefung und Audit-Log.
Bundle-Groesse: Footer (TikTok/YouTube/Instagram-Icons, Copyright-Zeile)
aus dem client-seitigen site-chrome.tsx in eine eigene Server Component
(site-footer.tsx) ausgelagert — brauchte "use client" nur wegen
useI18n(), jetzt getDictionary()-Server-Aufruf wie auf jeder Admin-Seite.
Entfernt ungenutztes clientseitiges JS/Hydration aus jedem Seitenaufruf
(Footer steht im Root-Layout).
UX: app/not-found.tsx und app/error.tsx ergaenzt (Next.js-Konventionen
fuer 404 bzw. Laufzeitfehler) — beide fehlten komplett, unbekannte Routen
und Rendering-Fehler zeigten bisher Next.js' generische, unbrandete
Standardseiten statt einer zur Seite passenden Nutzerfuehrung.
Dictionary-Kollision dabei aufgefallen und aufgeloest: eine bereits
bestehende, admin-interne errorPage-Uebersetzung (src/app/admin/error.tsx,
seit dem urspruenglichen App-Grundgeruest vorhanden) blieb unangetastet,
die neue site-weite Fehlerseite nutzt einen eigenen Schluessel
(siteErrorPage).
Verifiziert: tsc --noEmit clean, 118/118 Tests gruen, pnpm build clean.
Co-Authored-By: Claude Sonnet 5 <[email protected]>