# Modul: Website-CMS (Hero · Über uns · Galerie · Kontakt · Passwort) > Vollständiges CMS für eine öffentliche Website: Hero-Sektion (Texte, Badges, Hintergrundbild, Logo, Favicon), Über-uns-Sektion (Texte, Statistiken, Sektionsbild), Bildergalerie (Upload, Sortierung, animiertes Grid), Kontakt-Daten (Info, Öffnungszeiten, Social Media) und Admin-Passwort-Änderung. Alle Sektionen lesen direkt aus Supabase; bei DB-Fehler greift ein statischer Fallback. > **Anfrageformular** (Kontaktformular mit E-Mail-Versand) → Modul 07 --- ## Enthaltene Dateien | Ziel im neuen Projekt | Inhalt | |---|---| | `app/api/admin/hero/route.ts` | GET + PATCH: hero_content (UPSERT) + hero_badges (lesen) | | `app/api/admin/hero/bild/route.ts` | POST/DELETE: Hintergrundbild (`hero-bilder` Bucket) | | `app/api/admin/hero/logo/route.ts` | POST/DELETE: Logo (`site-assets` Bucket) | | `app/api/admin/hero/favicon/route.ts` | POST/DELETE: Favicon (`site-assets` Bucket) | | `app/api/admin/hero/badges/route.ts` | POST: Badge hinzufügen | | `app/api/admin/hero/badges/id/route.ts` | PATCH/DELETE: Badge bearbeiten/löschen | | `app/api/admin/galerie/route.ts` | GET/POST/PATCH/DELETE: Galerie-Bilder + Storage | | `app/api/admin/galerie/sort/route.ts` | POST: Reihenfolge speichern | | `app/api/admin/ueber-uns/route.ts` | GET + PATCH: ueber_uns_content (UPSERT) + Stats (lesen) | | `app/api/admin/ueber-uns/upload/route.ts` | POST: Sektionsbild hochladen (`ueber-uns-bilder` Bucket) | | `app/api/admin/ueber-uns/stats/route.ts` | POST: Statistik hinzufügen | | `app/api/admin/ueber-uns/stats/id/route.ts` | PATCH/DELETE: Statistik bearbeiten/löschen | | `app/api/admin/kontakt/route.ts` | GET + PATCH: kontakt_info (UPSERT) + Listen lesen | | `app/api/admin/kontakt/oeffnungszeiten/route.ts` | POST: Öffnungszeit hinzufügen | | `app/api/admin/kontakt/oeffnungszeiten/id/route.ts` | PATCH/DELETE | | `app/api/admin/kontakt/social/route.ts` | POST: Social-Link hinzufügen | | `app/api/admin/kontakt/social/id/route.ts` | PATCH/DELETE | | `app/api/admin/passwort/route.ts` | POST: Admin-Passwort ändern (bcrypt) | | `app/admin/hero/page.tsx` | Admin-Page Hero | | `app/admin/galerie/page.tsx` | Admin-Page Galerie | | `app/admin/ueber-uns/page.tsx` | Admin-Page Über uns | | `app/admin/kontakt/page.tsx` | Admin-Page Kontakt | | `app/admin/passwort/page.tsx` | Admin-Page Passwort | | `components/GalerieAnzeige.tsx` | Öffentliche Galerie mit 3D-Flip-Animation | | `components/admin/HeroVerwaltung.tsx` | Admin-UI: Hero | | `components/admin/GalerieVerwaltung.tsx` | Admin-UI: Galerie | | `components/admin/UeberUnsVerwaltung.tsx` | Admin-UI: Über uns | | `components/admin/KontaktVerwaltung.tsx` | Admin-UI: Kontakt | | `components/admin/PasswortAendern.tsx` | Admin-UI: Passwort ändern | **Hinweis:** Ordner `id/` in `files/` entsprechen Next.js Dynamic-Route-Ordnern `[id]`. --- ## Voraussetzungen Benötigt: - `lib/supabase.ts` mit `createPublicClient()` und `createServiceClient()` - `lib/admin-auth.ts` (Modul 02): `requireAdmin()` und `getAdminSession()` - Tabelle `admins` mit Spalten `id`, `email`, `password_hash` (Modul 02) ```bash npm install bcryptjs npm install -D @types/bcryptjs ``` --- ## Umgebungsvariablen ```env NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co # Für Storage-URL-Konstruktion im Client SUPABASE_INTERNAL_URL=http://supabase-kong:8000 # Nur Docker-intern; API-Routen verwenden diese ``` --- ## Supabase Storage Buckets anlegen Im Supabase Dashboard → Storage → New Bucket für jeden Bucket: | Bucket-Name | Public | Verwendung | |---|---|---| | `hero-bilder` | ✓ | Hero-Hintergrundbild | | `site-assets` | ✓ | Logo + Favicon | | `ueber-uns-bilder` | ✓ | Über-uns-Sektionsbild | | `galerie-bilder` | ✓ | Galerie-Fotos | --- ## Datenbank-Migrationen (Supabase) Die vollständige SQL-Datei liegt unter `migrations/MIGRATIONS_WEBSITE_CMS.sql`. Im Supabase SQL-Editor einmalig ausführen. Sie enthält alle Tabellen, Spalten und RLS-Policies. Erstellt werden: - `hero_content` — Singleton (Seitenname, Tagline, Überschriften, Buttons, Bildpfade) - `hero_badges` — Liste der Trust-Badges - `ueber_uns_content` — Singleton (Eyebrow, Absätze, Bildpfad) - `ueber_uns_stats` — Liste der Kennzahlen/Statistiken - `galerie_bilder` — Liste der Galeriefotos (storage_path, alt_text, reihenfolge) - `kontakt_info` — Singleton (Telefon, E-Mail, Adresse, Formular-Empfänger) - `kontakt_oeffnungszeiten` — Liste der Öffnungszeiten - `kontakt_social` — Liste der Social-Media-Links Alle Tabellen erhalten `public read` RLS-Policy (anonymer Lesezugriff). ### Supabase-Typen (`lib/supabase.ts`) ergänzen In `Database.public.Tables` hinzufügen: ```ts hero_content: { Row: { id: string; site_name: string; site_tagline: string; eyebrow_text: string; headline1: string; headline2: string; subtext1: string; subtext2: string; cta1_text: string; cta1_href: string; cta2_text: string; cta2_href: string; bg_image_path: string | null; logo_path: string | null; favicon_path: string | null; updated_at: string; } } hero_badges: { Row: { id: string; text: string; reihenfolge: number } } ueber_uns_content: { Row: { id: string; eyebrow_text: string; absatz1: string; absatz2: string; bild_url: string | null; updated_at: string; } } ueber_uns_stats: { Row: { id: string; wert: string; label: string; reihenfolge: number } } galerie_bilder: { Row: { id: string; storage_path: string; alt_text: string; reihenfolge: number; } } kontakt_info: { Row: { id: string; telefon: string; email: string; adresse_zeile1: string; adresse_zeile2: string; formular_empfaenger: string; updated_at: string; } } kontakt_oeffnungszeiten: { Row: { id: string; tag: string; von: string; bis: string; reihenfolge: number } } kontakt_social: { Row: { id: string; platform: string; url: string; reihenfolge: number } } ``` --- ## Architektur-Muster ### UPSERT-Singleton (Einzel-Zeilen-Tabellen) `hero_content`, `ueber_uns_content`, `kontakt_info` haben immer genau eine Zeile. API-Route: erst `select('id').limit(1).single()` → bei Treffer `update`, sonst `insert`. ### CRUD-Liste (Tabellen mit `reihenfolge`) `hero_badges`, `ueber_uns_stats`, `galerie_bilder`, `kontakt_oeffnungszeiten`, `kontakt_social` — klassisches INSERT/PATCH/DELETE. Reihenfolge wird als Integer gespeichert. ### Server Component + Fallback Öffentliche Website-Sektionen sind async Server Components. Alle Supabase-Aufrufe in `try/catch`; bei Fehler werden hartcodierte Fallback-Daten gerendert. ### force-dynamic Alle Admin-Pages müssen `export const dynamic = 'force-dynamic'` haben (verhindert statisches Pre-Rendering beim Docker-Build ohne Env-Variablen). ### Galerie: Hydration-Mismatch vermeiden `GalerieAnzeige` initialisiert `slots` ohne Shuffle im `useState()`-Initializer (Server und Client rendern identisch). Der Shuffle + Intervall-Start läuft nur im `useEffect([])` nach dem Mount. --- ## Einbindung Schritt für Schritt ### 1. Dateien kopieren (Dynamic-Route-Ordner umbenennen) ``` files/app/api/admin/hero/badges/id/ → app/api/admin/hero/badges/[id]/ files/app/api/admin/ueber-uns/stats/id/ → app/api/admin/ueber-uns/stats/[id]/ files/app/api/admin/kontakt/oeffnungszeiten/id/ → app/api/admin/kontakt/oeffnungszeiten/[id]/ files/app/api/admin/kontakt/social/id/ → app/api/admin/kontakt/social/[id]/ ``` ### 2. Standardwerte anpassen In `components/admin/HeroVerwaltung.tsx`: ```tsx const DEFAULTS: HeroContent = { site_name: 'Musterfirma', // ← eigenen Firmennamen setzen site_tagline: '', eyebrow_text: 'Ihr Slogan hier', headline1: 'Wir lieben Qualität.', headline2: 'Ihre Kunden auch.', // ... } ``` In `app/api/admin/hero/route.ts` PATCH-Handler dieselben Defaults setzen. ### 3. Fallback-Bild konfigurieren (`components/admin/HeroVerwaltung.tsx`) ```tsx src={bgPreview ?? bgUrl ?? '/hero.jpg'} ``` Das Fallback-Bild `/hero.jpg` muss im `public/`-Ordner liegen (oder durch eigenen Pfad ersetzen). ### 4. Öffentliche Website-Sektionen einbinden Die Galerie-Anzeige-Komponente auf der Hauptseite: ```tsx // In app/page.tsx (async Server Component): import { GalerieAnzeige } from '@/components/GalerieAnzeige' const { data: galerie } = await supabase.from('galerie_bilder').select('*').order('reihenfolge') const bilder = (galerie ?? []).map(b => ({ id: b.id, src: `${supabaseUrl}/storage/v1/object/public/galerie-bilder/${b.storage_path}`, alt: b.alt_text, })) ``` ### 5. Admin-Navigation ergänzen ```tsx Hero Über uns Galerie Kontakt Passwort ``` --- ## Anpassungspunkte | Was | Wo | |---|---| | Firmenname / Standardtexte | `components/admin/HeroVerwaltung.tsx` → `DEFAULTS` + `api/admin/hero/route.ts` | | Fallback-Hero-Bild | `components/admin/HeroVerwaltung.tsx` → `bgPreview ?? bgUrl ?? '/hero.jpg'` | | Galerie-Flip-Geschwindigkeit | `components/GalerieAnzeige.tsx` → `INTERVAL_MS` (Standard: 1500 ms) | | Galerie-Grid-Layout | `components/GalerieAnzeige.tsx` → `VISIBLE`, `ROW_H`, `numGroups`-Berechnung | | Bucket-Namen | Alle `app/api/admin/*/route.ts` → `const BUCKET = '...'` | | Max. Upload-Größe | `app/api/admin/galerie/route.ts` → `MAX_SIZE` | --- ## Integrations-Prompt Kopiere diesen Prompt in eine neue KI-Konversation, nachdem du die `files/` in dein Projekt kopiert und die SQL-Migration ausgeführt hast. Ersetze alle `[PLATZHALTER]`. ``` Ich integriere das Website-CMS-Modul (Hero, Über uns, Galerie, Kontakt, Passwort) in mein Next.js/Supabase-Projekt. PROJEKT-KONTEXT: - Firmenname: [FIRMENNAME] - Tagline (optional): [TAGLINE oder leer lassen] - Hero-Eyebrow-Text: [z. B. "Ihr Ansprechpartner seit 2010"] - Hero-Überschrift 1 (weiß): [z. B. "Wir lieben Qualität."] - Hero-Überschrift 2 (akzentfarbe): [z. B. "Ihre Kunden auch."] - Modul 02 (Admin-Auth) ist bereits integriert (requireAdmin + getAdminSession verfügbar) - Tabelle `admins` mit password_hash-Spalte existiert bereits BEREITS KOPIERTE DATEIEN (aus modules/06-website-cms/files/): Wichtig: Ordner "id/" umbenennen zu "[id]" — betrifft: - app/api/admin/hero/badges/id/ → [id]/ - app/api/admin/ueber-uns/stats/id/ → [id]/ - app/api/admin/kontakt/oeffnungszeiten/id/ → [id]/ - app/api/admin/kontakt/social/id/ → [id]/ AUFGABEN – führe sie der Reihe nach aus: 1. SQL-MIGRATION: Führe modules/06-website-cms/migrations/MIGRATIONS_WEBSITE_CMS.sql im Supabase SQL-Editor aus (enthält alle Tabellen + RLS-Policies). 2. SUPABASE STORAGE BUCKETS anlegen (alle Public): - hero-bilder - site-assets - ueber-uns-bilder - galerie-bilder 3. BCRYPTJS installieren (für Passwort-Änderung): npm install bcryptjs && npm install -D @types/bcryptjs 4. SUPABASE-TYPEN: Lies lib/supabase.ts. Ergänze in Database.public.Tables die Typen für: hero_content, hero_badges, ueber_uns_content, ueber_uns_stats, galerie_bilder, kontakt_info, kontakt_oeffnungszeiten, kontakt_social (Typen aus modules/06-website-cms/TEMPLATE.md kopieren) 5. STANDARDWERTE anpassen: a) Lies components/admin/HeroVerwaltung.tsx. Setze in DEFAULTS: site_name: '[FIRMENNAME]', site_tagline: '[TAGLINE]', eyebrow_text: '[EYEBROW]', headline1: '[H1]', headline2: '[H2]' b) Lies app/api/admin/hero/route.ts. Setze dieselben Werte als Fallback in den PATCH-Handler-fields. 6. FALLBACK-BILD: Lege eine Datei hero.jpg (oder eigenen Namen) in public/ ab. Lies components/admin/HeroVerwaltung.tsx und ersetze '/hero.jpg' durch deinen Pfad. 7. ÖFFENTLICHE SEITE einbinden: a) Lies app/page.tsx (oder deine Hauptseite). Importiere und verwende die Sektionskomponenten (Hero, UeberUns, GalerieAnzeige). Für GalerieAnzeige: Daten aus galerie_bilder laden, Storage-URLs konstruieren, als `bilder: GalerieBild[]`-Array übergeben. b) Kontaktsektion mit Formular → Modul 07 einbinden. 8. ADMIN-NAVIGATION ergänzen: Lies app/admin/AdminNav.tsx (oder deine Admin-Nav). Füge Links zu /admin/hero, /admin/ueber-uns, /admin/galerie, /admin/kontakt, /admin/passwort hinzu. Import-Icons: Home, Info, Grid2x2, Phone, KeyRound (aus lucide-react). 9. TEST: a) /admin/hero → Seitenname + Texte + Hintergrundbild + Logo + Favicon speichern b) /admin/ueber-uns → Texte + Statistik hinzufügen c) /admin/galerie → Bild hochladen, Reihenfolge ändern d) /admin/kontakt → Öffnungszeiten + Social-Links anlegen, Formular-Empfänger eintragen e) /admin/passwort → Passwort ändern (aktuelles Passwort + neues Passwort) f) Öffentliche Hauptseite → alle Sektionen zeigen Daten aus der DB Lies jede Datei vor dem Bearbeiten. Melde wenn alle Schritte abgeschlossen sind. ```