diff --git a/src/components/content-image.tsx b/src/components/content-image.tsx index d35b876..5f528fa 100644 --- a/src/components/content-image.tsx +++ b/src/components/content-image.tsx @@ -14,6 +14,12 @@ interface ContentImageProps { * true setzen — laedt das Bild mit hoher Prioritaet ohne Lazy-Loading, * statt es erst beim Scannen weiter unten im Dokument zu entdecken. */ priority?: boolean; + /** Leicht abgerundete Ecken. Standard true — passt fuer Karten, Teaser + * und Inhaltsbilder, die als eigene Flaeche auf dem Seitenhintergrund + * liegen. Fuer randlose Flaechen (Hero ueber die volle Fensterbreite) + * bewusst false: dort saesse die Rundung direkt an der Fensterkante und + * wirkte wie ein Darstellungsfehler. */ + rounded?: boolean; } /** @@ -34,9 +40,16 @@ export function ContentImage({ label, sizes = "100vw", priority = false, + rounded = true, }: ContentImageProps) { + // Dieselbe Rundung fuer Platzhalter UND echtes Bild — sonst spraenge die + // Form in dem Moment um, in dem ein Bild gepflegt wird. + const roundedClass = rounded ? "rounded-lg" : ""; + if (!src) { - return ; + return ( + + ); } // "label" traegt bereits eine kurze inhaltliche Einordnung (Ernte-Jahr/Typ, // Kategorie, Ort — siehe Aufrufer), die bisher nur auf der Platzhalterkachel @@ -45,8 +58,22 @@ export function ContentImage({ // Bildersuche als der Produkt-/Ernteame allein. const descriptiveAlt = label ? `${alt} – ${label}` : alt; return ( -
- {descriptiveAlt} +
+ {descriptiveAlt}, setzt + // am selbst aber weder fetchpriority noch loading. Gemessen kam + // das Hero-Bild damit trotz "priority" mit niedriger Netzwerk- + // Prioritaet an. Die Dokumentation nennt fuer diesen Fall + // ausdruecklich loading="eager" und fetchPriority="high" als Ersatz. + loading={priority ? "eager" : "lazy"} + fetchPriority={priority ? "high" : "auto"} + className="object-cover" + />
); } diff --git a/src/components/hero.tsx b/src/components/hero.tsx index 5c0d6ea..459a223 100644 --- a/src/components/hero.tsx +++ b/src/components/hero.tsx @@ -5,11 +5,6 @@ import { Container, LinkButton } from "@/components/ui"; import { ContentImage } from "@/components/content-image"; import { useI18n } from "@/i18n/i18n-provider"; -const sectionVariants: Variants = { - hidden: { opacity: 0 }, - visible: { opacity: 1, transition: { duration: 0.6 } }, -}; - const itemContainerVariants: Variants = { hidden: {}, visible: { transition: { delayChildren: 0.3, staggerChildren: 0.12 } }, @@ -41,17 +36,38 @@ export function Hero({ const shouldReduceMotion = useReducedMotion(); return ( - + // Bewusst KEINE Einblend-Animation auf der aeussersten Ebene: eine + // Section, die bei opacity 0 startet, wird erst sichtbar, wenn das + // JS-Bundle geladen und die Animation gelaufen ist. Das Hero-Bild ist + // aber das groesste Element im ersten Viewport (LCP) — es so lange + // zurueckzuhalten verschiebt den gemessenen Ladezeitpunkt der Seite um + // die gesamte Hydrationsdauer. Der gestaffelte Aufbau der Textspalte + // weiter unten bleibt unveraendert, der laeuft unabhaengig davon. +
{video ? (
+ {/* Standbild als eigene Ebene UNTER dem Video statt im + poster-Attribut. poster nimmt nur eine fertige URL und laedt die + hochgeladene Datei damit in voller gespeicherter Groesse (bis + MAX_IMAGE_DIMENSION, siehe lib/uploads.ts) — am Bildoptimierer + vorbei und ohne Ruecksicht auf das Geraet. Als ContentImage + laeuft dasselbe Bild durch next/image: passende Breite je + Bildschirm, WebP statt JPEG, und mit "priority" als erstes + angefordert. Das Video liegt darueber und ist transparent, bis es + eigene Bilddaten hat — der Uebergang ist deshalb nicht sichtbar. */} +
+ +
- +
); } diff --git a/src/components/product-gallery.tsx b/src/components/product-gallery.tsx index b7ce985..0e0ccff 100644 --- a/src/components/product-gallery.tsx +++ b/src/components/product-gallery.tsx @@ -63,7 +63,7 @@ export function ProductGallery({ images, productName, description }: ProductGall return (
@@ -72,14 +72,18 @@ export function ProductGallery({ images, productName, description }: ProductGall // /uploads/... (siehe uploads.ts/isValidUploadPath) — next/image // braucht dafuer KEINEN remotePatterns-Eintrag (nur externe // Bildquellen brauchen den), siehe auch content-image.tsx. Als - // wahrscheinlichstes LCP-Element der Produktseite mit "priority" - // geladen, statt erst beim Scannen des Bild-Tags entdeckt zu werden. + // wahrscheinlichstes LCP-Element der Produktseite sofort und mit + // hoher Netzwerk-Prioritaet geladen, statt erst beim Scannen des + // Bild-Tags entdeckt zu werden. Bewusst NICHT ueber die Prop + // "priority": die ist seit Next.js 16 abgekuendigt und erzeugt nur + // noch einen Preload-Link, ohne fetchpriority am zu setzen. {mainImageAlt} ) : ( @@ -95,7 +99,7 @@ export function ProductGallery({ images, productName, description }: ProductGall onClick={() => setActiveIndex(index)} aria-label={`${isImageSlide(slide) ? `Produktbild ${index + 1}` : slide.label} – Bild ${index + 1} von ${slides.length} anzeigen`} aria-current={index === activeIndex} - className={`relative aspect-square overflow-hidden border-2 transition-colors ${ + className={`relative aspect-square overflow-hidden rounded-lg border-2 transition-colors ${ index === activeIndex ? "border-forest" : "border-transparent hover:border-ink/20" }`} >