13 KiB
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.tsmitcreatePublicClient()undcreateServiceClient()lib/admin-auth.ts(Modul 02):requireAdmin()undgetAdminSession()- Tabelle
adminsmit Spaltenid,email,password_hash(Modul 02)
npm install bcryptjs
npm install -D @types/bcryptjs
Umgebungsvariablen
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-Badgesueber_uns_content— Singleton (Eyebrow, Absätze, Bildpfad)ueber_uns_stats— Liste der Kennzahlen/Statistikengalerie_bilder— Liste der Galeriefotos (storage_path, alt_text, reihenfolge)kontakt_info— Singleton (Telefon, E-Mail, Adresse, Formular-Empfänger)kontakt_oeffnungszeiten— Liste der Öffnungszeitenkontakt_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:
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:
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)
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:
// 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,
}))
<GalerieAnzeige bilder={bilder} />
5. Admin-Navigation ergänzen
<a href="/admin/hero">Hero</a>
<a href="/admin/ueber-uns">Über uns</a>
<a href="/admin/galerie">Galerie</a>
<a href="/admin/kontakt">Kontakt</a>
<a href="/admin/passwort">Passwort</a>
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.