# 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.
```