feat: Video unter dem Headerbild aus dem Medienverzeichnis (intro.mp4)

Startet nur nach Klick; mit Vorschaubild (intro.jpg) lädt vorab nichts vom Video. Vorschaubilder von Intro und Reels laufen über die Bildoptimierung statt im Original, Videos bevorzugen eine Web-Fassung <name>.web.mp4.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-06 07:09:46 +02:00
1 parent 51b6d79bbf
commit 33f5096bf1
6 files changed
+99 -10

No files matched your search

+11 -5
View File
@@ -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 (
<figure>
@@ -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"
>
@@ -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 (
<section aria-label="Video" className="bg-ink py-12 text-paper/70 md:py-16">
<p className="container-edge text-center">
<span className="draft-flag">Platzhalter:</span> Hier erscheint das Video, sobald <code>intro.mp4</code> im Medienverzeichnis liegt.
</p>
</section>
);
}
const poster = video.poster?.kind === "image" ? optimizedImageUrl(video.poster.src, POSTER_WIDTH) : undefined;
return (
<section aria-label="Video" className="bg-ink py-12 md:py-20">
<div className="container-edge">
<video
src={poster ? video.src : `${video.src}#t=0.1`}
poster={poster}
controls
playsInline
preload={poster ? "none" : "metadata"}
aria-label="Video ARTISTA Modelcamp"
// Eigenes Seitenverhaeltnis (quer oder hoch), wie bei den Reels.
className="mx-auto block max-h-[85vh] w-full rounded-2xl bg-black object-contain"
>
<track kind="captions" />
</video>
</div>
</section>
);
}