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

+61
View File
@@ -7,6 +7,8 @@ import {
generateUploadFilename,
isValidUploadPath,
resolveUploadPath,
detectVideoExtension,
isValidVideoUploadPath,
} from "./uploads";
describe("extensionForFormat", () => {
@@ -39,6 +41,11 @@ describe("contentTypeForExtension", () => {
expect(contentTypeForExtension("avif")).toBe("image/avif");
});
test("returns the correct MIME type for the video extensions", () => {
expect(contentTypeForExtension("mp4")).toBe("video/mp4");
expect(contentTypeForExtension("webm")).toBe("video/webm");
});
test("returns null for an extension outside the fixed allow-list", () => {
// Verhindert, dass eine Datei je als text/html o.ae. ausgeliefert wird —
// siehe app/uploads/[...path]/route.ts.
@@ -48,6 +55,33 @@ describe("contentTypeForExtension", () => {
});
});
describe("detectVideoExtension", () => {
function bufferWithFtypAt4(): Buffer {
// Reale MP4-Dateien haben eine variable Box-Groesse in den ersten 4
// Bytes, gefolgt von der literalen ASCII-Kennung "ftyp" — die konkreten
// ersten 4 Bytes sind fuer die Erkennung irrelevant.
return Buffer.concat([Buffer.from([0, 0, 0, 24]), Buffer.from("ftypisom", "ascii")]);
}
test("recognizes an MP4 file via its ftyp box", () => {
expect(detectVideoExtension(bufferWithFtypAt4())).toBe("mp4");
});
test("recognizes a WebM file via its EBML header", () => {
expect(detectVideoExtension(Buffer.from([0x1a, 0x45, 0xdf, 0xa3, 0x01, 0x02]))).toBe("webm");
});
test("rejects a buffer that is neither MP4 nor WebM (e.g. a JPEG or arbitrary bytes)", () => {
expect(detectVideoExtension(Buffer.from([0xff, 0xd8, 0xff, 0xe0]))).toBeNull();
expect(detectVideoExtension(Buffer.from("not a video", "ascii"))).toBeNull();
});
test("rejects a buffer too short to contain either signature", () => {
expect(detectVideoExtension(Buffer.from([0x1a, 0x45]))).toBeNull();
expect(detectVideoExtension(Buffer.alloc(0))).toBeNull();
});
});
describe("generateUploadFilename", () => {
test("produces a 32-char lowercase hex name with the given extension", () => {
const name = generateUploadFilename("webp");
@@ -90,6 +124,33 @@ describe("isValidUploadPath", () => {
});
});
describe("isValidVideoUploadPath", () => {
test("accepts a path in exactly the shape the video upload route produces", () => {
expect(isValidVideoUploadPath(`/uploads/${generateUploadFilename("mp4")}`)).toBe(true);
expect(isValidVideoUploadPath(`/uploads/${generateUploadFilename("webm")}`)).toBe(true);
});
test("rejects an image path (different endpoint's output)", () => {
expect(isValidVideoUploadPath(`/uploads/${generateUploadFilename("png")}`)).toBe(false);
});
test("rejects a path traversal attempt", () => {
expect(isValidVideoUploadPath("/uploads/../../etc/passwd")).toBe(false);
});
test("rejects an absolute external URL (not an upload-endpoint path)", () => {
expect(isValidVideoUploadPath("https://attacker.example/x.mp4")).toBe(false);
});
test("rejects a client-chosen filename instead of the generated random one", () => {
expect(isValidVideoUploadPath("/uploads/my-video.mp4")).toBe(false);
});
test("rejects an empty string", () => {
expect(isValidVideoUploadPath("")).toBe(false);
});
});
describe("resolveUploadPath", () => {
test("resolves a plain filename to a path inside UPLOAD_DIR", () => {
const resolved = resolveUploadPath(["abc123.png"]);