feat: Hero-Hintergrundvideo im Admin, ScrollReveal + Bild-Perf auf Unterseiten
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m31s

Neues admin-editierbares Hintergrundvideo fuer den Startseiten-Hero
(home.hero.video, MP4/WebM) mit eigenem, magic-byte-validiertem
Upload-Endpunkt (app/api/admin/upload-video) analog zur bestehenden
Bild-Upload-Pipeline. Ausserdem: ScrollReveal-Animation von der
Startseite auf Imkerei, Herkunft, Unternehmen, Ernten, Wissen, Kontakt,
Shop sowie die Produkt-/Ernte-/Chargen-Detailseiten ausgerollt (bisher
nur Startseite), priority auf bislang lazy geladene Above-the-fold-Bilder
(Herkunft/Unternehmen) gesetzt, und passende sizes-Hints auf Grid-Bilder
(Produktkarten, Origin/Regional/Business-Teaser, Ernten-Liste) ergaenzt
statt voller Viewport-Breite.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-11 14:53:14 +02:00
1 parent 398db5c5ec
commit 6814e9e2c8
25 files changed
+524 -119

No files matched your search

+48 -3
View File
@@ -32,7 +32,18 @@ export const UPLOAD_DIR = path.resolve(/*turbopackIgnore: true*/ process.env.UPL
*/
export const MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
/**
* 40 MB: ein kurzes (wenige Sekunden), fuer Web komprimiertes Hero-Video
* passt bequem darunter. Grosszuegiger als MAX_UPLOAD_BYTES (Bilder), weil
* Video-Dateien inhaerent groesser sind — Videos werden aber (anders als
* Bilder) NICHT neu enkodiert (dafuer gibt es hier keine sharp-Entsprechung
* ohne eine ffmpeg-Abhaengigkeit), die Groessengrenze ist daher die einzige
* Bremse gegen uebermaessig grosse Uploads.
*/
export const MAX_UPLOAD_VIDEO_BYTES = 40 * 1024 * 1024;
export type AllowedImageExtension = "jpg" | "png" | "webp" | "avif";
export type AllowedVideoExtension = "mp4" | "webm";
/**
* Kanonische Dateiendung je von sharp erkanntem Bildformat (Magic-Bytes,
@@ -52,6 +63,30 @@ export function extensionForFormat(format: string | undefined): AllowedImageExte
return EXTENSION_BY_FORMAT[format] ?? null;
}
/**
* Erkennt MP4/WebM ausschliesslich anhand der tatsaechlichen Magic-Bytes am
* Dateianfang (analog zu extensionForFormat fuer Bilder via sharp) — nie
* ueber den Client-Dateinamen oder den vom Client gesendeten Content-Type,
* beide sind frei faelschbar. MP4 traegt seine "ftyp"-Box-Kennung ab Byte 4
* (die ersten 4 Bytes sind eine variable Box-Groesse), WebM beginnt mit dem
* fixen EBML-Header 0x1A45DFA3.
*/
export function detectVideoExtension(buffer: Buffer): AllowedVideoExtension | null {
if (buffer.length >= 12 && buffer.toString("ascii", 4, 8) === "ftyp") {
return "mp4";
}
if (
buffer.length >= 4 &&
buffer[0] === 0x1a &&
buffer[1] === 0x45 &&
buffer[2] === 0xdf &&
buffer[3] === 0xa3
) {
return "webm";
}
return null;
}
/**
* Content-Type ausschliesslich aus dieser festen Zuordnung nach (bereits
* validierter) Dateiendung — nie aus dem Dateiinhalt oder einem Client-
@@ -59,15 +94,17 @@ export function extensionForFormat(format: string | undefined): AllowedImageExte
* interpretiert werden koennte (gespeicherter XSS ueber Bild-Uploads), siehe
* app/uploads/[...path]/route.ts.
*/
const CONTENT_TYPE_BY_EXTENSION: Record<AllowedImageExtension, string> = {
const CONTENT_TYPE_BY_EXTENSION: Record<AllowedImageExtension | AllowedVideoExtension, string> = {
jpg: "image/jpeg",
png: "image/png",
webp: "image/webp",
avif: "image/avif",
mp4: "video/mp4",
webm: "video/webm",
};
export function contentTypeForExtension(extension: string): string | null {
return CONTENT_TYPE_BY_EXTENSION[extension as AllowedImageExtension] ?? null;
return CONTENT_TYPE_BY_EXTENSION[extension as AllowedImageExtension | AllowedVideoExtension] ?? null;
}
/**
@@ -76,11 +113,12 @@ export function contentTypeForExtension(extension: string): string | null {
* (Path-Traversal-/Overwrite-Risiko). 16 zufaellige Bytes (32 Hex-Zeichen)
* sind praktisch kollisionsfrei fuer die Upload-Frequenz dieser Seite.
*/
export function generateUploadFilename(extension: AllowedImageExtension): string {
export function generateUploadFilename(extension: AllowedImageExtension | AllowedVideoExtension): string {
return `${randomBytes(16).toString("hex")}.${extension}`;
}
const UPLOAD_PATH_PATTERN = /^\/uploads\/[a-f0-9]{32}\.(jpg|png|webp|avif)$/;
const VIDEO_UPLOAD_PATH_PATTERN = /^\/uploads\/[a-f0-9]{32}\.(mp4|webm)$/;
/**
* Prueft, ob ein gespeicherter Bildpfad exakt der Form entspricht, die
@@ -95,6 +133,13 @@ export function isValidUploadPath(value: string): boolean {
return UPLOAD_PATH_PATTERN.test(value);
}
/** Wie isValidUploadPath, aber fuer Video-Feldwerte (siehe home.hero.video
* in lib/content-registry.ts) — eigenes Endungs-Set, damit die Bild-
* Validierung oben nicht mit-gelockert wird. */
export function isValidVideoUploadPath(value: string): boolean {
return VIDEO_UPLOAD_PATH_PATTERN.test(value);
}
/**
* Loest Catch-all-Segmente aus app/uploads/[...path]/route.ts sicher gegen
* UPLOAD_DIR auf. path.join() allein wuerde ".." zwar rechnerisch weg-