diff --git a/docs/MEDIEN.md b/docs/MEDIEN.md index 83bce7a..8f435e5 100644 --- a/docs/MEDIEN.md +++ b/docs/MEDIEN.md @@ -22,6 +22,14 @@ Das Logo wird 32 bis 40 px hoch angezeigt. Ein querformatiges Logo ohne Rand wir | `header.jpg` (oder `.webp`, `.png`) | Bild über die ganze Breite oben | Querformat, mindestens 2400 px breit. Ohne Datei erscheint die Luftaufnahme der Villa | | `header.mp4` (optional) | Video statt Standbild, läuft stumm in Schleife | Kurz halten (unter 15 s, unter 10 MB). `header.jpg` dient als Vorschaubild | +## Video unter dem Headerbild + +| Datei | Wofür | Hinweis | +|---|---|---| +| `intro.mp4` | Video direkt unter dem Headerbild, startet erst nach Klick | Quer- oder Hochformat. Ohne Datei entfällt der Abschnitt | +| `intro.jpg` (oder `.webp`, `.png`) | Vorschaubild des Videos | Empfohlen: Mit Vorschaubild lädt vom Video vorab nichts | +| `intro.web.mp4` (optional) | Web-Fassung, wird bevorzugt angezeigt | Erzeugen mit `pnpm media:optimize`, siehe unten | + ## Team (Ordner `team/`) Hochformat 3:4 oder 4:5, mindestens 1200 px breit, Gesicht im oberen Drittel. Das Bild wird oben rund (als Bogen) beschnitten. diff --git a/src/app/page.tsx b/src/app/page.tsx index 92d8b44..ed8bca7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,6 +6,7 @@ import { AccommodationSection } from "@/components/sections/CampSections"; import { ExpectSection, ScheduleSection } from "@/components/sections/ExpectSections"; import { ClosingSection, FaqSection } from "@/components/sections/FaqSection"; import { AboutSection, ReelsSection, TeamSection } from "@/components/sections/PeopleSections"; +import { IntroVideoSection } from "@/components/sections/IntroVideoSection"; import { PricingSection } from "@/components/sections/PricingSection"; import { ReviewsSection } from "@/components/sections/ReviewsSection"; import { SiteFooter } from "@/components/SiteFooter"; @@ -16,7 +17,7 @@ import { loadPublicPage } from "@/lib/public-page"; import { loadSiteMedia } from "@/lib/site-media"; /* - * Gliederung nach der Filmcamp-Referenz: Headerbild, Termin, was dich + * Gliederung nach der Filmcamp-Referenz: Headerbild, Video, Termin, was dich * erwartet (die Villa), Tagesablauf, Unterkunft, Preis, Team, Reels, * Fragen, Abschluss. */ @@ -37,6 +38,7 @@ export default async function HomePage() {
+ diff --git a/src/components/VideoSlot.tsx b/src/components/VideoSlot.tsx index 864faae..facf81e 100644 --- a/src/components/VideoSlot.tsx +++ b/src/components/VideoSlot.tsx @@ -1,17 +1,23 @@ import type { Media } from "@/content/site"; +import { optimizedImageUrl } from "@/lib/image-url"; import { MediaFrame } from "./MediaFrame"; +/** Reels sind hoch und hoechstens ein Drittel breit. */ +const POSTER_WIDTH = 1080; + type Slot = { name: string | null; caption: string | null; poster: Media | null; src: string | null }; /** * Ein Video aus dem eigenen Medienverzeichnis, im eigenen Seitenverhaeltnis - * (quer oder hoch). Es startet nie von selbst; geladen werden vorab nur die - * Metadaten und das erste Bild als Vorschau (#t=0.1), falls kein - * Vorschaubild hinterlegt ist. Kein externer Anbieter. + * (quer oder hoch). Es startet nie von selbst. Mit Vorschaubild laedt vorab + * nichts vom Video, ohne nur Metadaten und erstes Bild (#t=0.1). Kein + * externer Anbieter. */ export function VideoSlot({ slot, index, showDrafts }: { slot: Slot; index: number; showDrafts: boolean }) { const label = slot.name ? `Video von ${slot.name}` : `Video ${index + 1}`; - const poster = slot.poster?.kind === "image" ? slot.poster.src : undefined; + // Vorschaubild ueber die Bildoptimierung statt im Original (ein Reel-PNG + // hatte 1,3 MB); mit Vorschaubild laedt vom Video vorab nichts. + const poster = slot.poster?.kind === "image" ? optimizedImageUrl(slot.poster.src, POSTER_WIDTH) : undefined; return (
@@ -21,7 +27,7 @@ export function VideoSlot({ slot, index, showDrafts }: { slot: Slot; index: numb poster={poster} controls playsInline - preload="metadata" + preload={poster ? "none" : "metadata"} aria-label={label} className="mx-auto block max-h-[80vh] w-full rounded-2xl bg-ink object-contain" > diff --git a/src/components/sections/IntroVideoSection.tsx b/src/components/sections/IntroVideoSection.tsx new file mode 100644 index 0000000..2b606a8 --- /dev/null +++ b/src/components/sections/IntroVideoSection.tsx @@ -0,0 +1,44 @@ +import { optimizedImageUrl } from "@/lib/image-url"; +import type { IntroVideo } from "@/lib/site-media"; + +/** Breite des Vorschaubilds: scharf bis zur vollen Inhaltsbreite. */ +const POSTER_WIDTH = 1920; + +/** + * Video direkt unter dem Headerbild (intro.mp4 im Medienverzeichnis, + * Vorschaubild intro.jpg, docs/MEDIEN.md). Startet nur nach Klick. Mit + * Vorschaubild laedt der Browser vorher gar nichts vom Video + * (preload="none"); ohne wird nur das erste Bild als Vorschau geholt. + */ +export function IntroVideoSection({ video, showDrafts }: { video: IntroVideo; showDrafts: boolean }) { + if (!video.src) { + if (!showDrafts) return null; + return ( +
+

+ Platzhalter: Hier erscheint das Video, sobald intro.mp4 im Medienverzeichnis liegt. +

+
+ ); + } + + const poster = video.poster?.kind === "image" ? optimizedImageUrl(video.poster.src, POSTER_WIDTH) : undefined; + return ( +
+
+ +
+
+ ); +} diff --git a/src/lib/image-url.ts b/src/lib/image-url.ts new file mode 100644 index 0000000..3898c4d --- /dev/null +++ b/src/lib/image-url.ts @@ -0,0 +1,15 @@ +/** + * URL eines Bildes ueber die Next.js-Bildoptimierung (AVIF/WebP, passende + * Breite) fuer Stellen, an denen kein moeglich ist, etwa das + * poster-Attribut von