Website djnizz.at: Hero- und About-Sektion mit Docker-Deployment

Astro 7 mit Tailwind 4 und Node-Adapter. Alle Seiten werden vorgerendert,
der Node-Server ist dabei, damit spaeter einzelne Routen (Booking-Formular)
ohne Umbau am Deployment dynamisch werden koennen.

Inhalt:
- Fullscreen-Hero mit freigestelltem Render, animierten Lasern und
  Genre-Laufband; Typografie als HTML statt eingebranntem Bildtext
- About-Sektion mit Genre-Karten und Eckdaten
- Impressum und Datenschutzerklaerung
- Zentrale Inhaltsdatei src/data/site.ts

Technik:
- Selbst gehostete Schriften, keine Cookies, kein Tracking, keine
  externen Requests
- Docker Multi-Stage (238 MB) hinter Caddy mit automatischem HTTPS,
  Security-Headern und IP-maskierten Logs
- scripts/cutout.mjs stellt den Render ueber eine Sobel-Kantenkarte frei
  und entfernt eingeschlossene Hintergrundflaechen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 23:17:13 +02:00
co-authored by Claude Opus 5
commit 074c87ed79
38 changed files with 7740 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
/**
* Leitet die abgeleiteten Bild-Assets aus den Quelldateien ab.
* Einmalig ausfuehren, wenn sich Banner oder Logo aendern:
* node scripts/assets.mjs
*/
import sharp from 'sharp';
import { statSync } from 'node:fs';
const BANNER = 'src/assets/hero-banner.jpg';
const LOGO = 'src/assets/logo-white.png';
const BRAND_BG = { r: 11, g: 16, b: 32, alpha: 1 };
// 1) Club-/Stage-Ausschnitt des Banners: alles links des eingebrannten
// "DJ NIZZ"-Schriftzugs (dessen linke Kante liegt bei x = 909) und ohne
// die Figur. Dient als Hintergrundebene im Hero, Typo und Figur kommen
// als HTML darueber. Breiter schneiden lassen die Geisterbuchstaben
// des Schriftzugs im Hintergrund auftauchen.
await sharp(BANNER)
.extract({ left: 0, top: 0, width: 880, height: 1536 })
.jpeg({ quality: 86, mozjpeg: true })
.toFile('src/assets/hero-club.jpg');
// 2) Open-Graph-Bild 1200x630 aus dem kompletten Banner
await sharp(BANNER)
.extract({ left: 0, top: 45, width: 2752, height: 1445 })
.resize(1200, 630)
.jpeg({ quality: 88, mozjpeg: true })
.toFile('public/og-image.jpg');
// 3) Favicons: das Logo ist weiss auf transparent und waere im hellen
// Browser-Tab unsichtbar -> auf Markenblau setzen.
const trimmed = await sharp(LOGO)
.trim()
.resize(880, 880, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png()
.toBuffer();
// sharp wendet resize() intern vor composite() an, unabhaengig von der
// Aufrufreihenfolge. Daher erst die 1024er Kachel bauen, dann separat skalieren.
const icon = await sharp({ create: { width: 1024, height: 1024, channels: 4, background: BRAND_BG } })
.composite([{ input: trimmed, gravity: 'center' }])
.png()
.toBuffer();
for (const size of [32, 180, 192, 512]) {
const out = size === 180 ? 'public/apple-touch-icon.png' : `public/favicon-${size}.png`;
await sharp(icon).resize(size, size).png({ compressionLevel: 9 }).toFile(out);
}
const files = [
'src/assets/hero-banner.jpg',
'src/assets/hero-club.jpg',
'src/assets/nizz-cutout.png',
'src/assets/logo-white.png',
'public/og-image.jpg',
'public/favicon-32.png',
'public/favicon-192.png',
'public/favicon-512.png',
'public/apple-touch-icon.png',
];
for (const f of files) {
const m = await sharp(f).metadata();
console.log(f.padEnd(32), `${m.width}x${m.height}`.padEnd(12), `${(statSync(f).size / 1024).toFixed(0)} KB`);
}