36 KiB
Modul-Integration & Supabase-Anbindung — Implementierungsplan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Alle 6 Module vollständig einbinden — DB-Tabellen anlegen, CMS-Code integrieren, öffentliche Sektionen aus Supabase laden.
Architecture: Next.js 15 App Router + selbst-gehostetes Supabase (Docker VM). Öffentliche Seiten sind async Server Components; Client-Komponenten (Hero, Contact) nehmen Props entgegen. Alle Supabase-Aufrufe in try/catch mit hartkodiertem Fallback.
Tech Stack: Next.js 15, Supabase (PostgreSQL + Storage + Auth), Nodemailer, bcryptjs, @supabase/ssr
Dateiübersicht
Neue Dateien (Modul 06-CMS kopieren):
app/api/admin/hero/route.ts
app/api/admin/hero/bild/route.ts
app/api/admin/hero/logo/route.ts
app/api/admin/hero/favicon/route.ts
app/api/admin/hero/badges/route.ts
app/api/admin/hero/badges/[id]/route.ts
app/api/admin/galerie/route.ts
app/api/admin/galerie/sort/route.ts
app/api/admin/ueber-uns/route.ts
app/api/admin/ueber-uns/upload/route.ts
app/api/admin/ueber-uns/stats/route.ts
app/api/admin/ueber-uns/stats/[id]/route.ts
app/api/admin/kontakt/route.ts
app/api/admin/kontakt/oeffnungszeiten/route.ts
app/api/admin/kontakt/oeffnungszeiten/[id]/route.ts
app/api/admin/kontakt/social/route.ts
app/api/admin/kontakt/social/[id]/route.ts
app/api/admin/passwort/route.ts
app/admin/hero/page.tsx
app/admin/galerie/page.tsx
app/admin/ueber-uns/page.tsx
app/admin/kontakt/page.tsx
app/admin/passwort/page.tsx
components/GalerieAnzeige.tsx
components/admin/HeroVerwaltung.tsx
components/admin/GalerieVerwaltung.tsx
components/admin/UeberUnsVerwaltung.tsx
components/admin/KontaktVerwaltung.tsx
components/admin/PasswortAendern.tsx
Geänderte Dateien:
lib/supabase.ts — CMS-Tabellentypen ergänzen
app/page.tsx — async, lädt aus Supabase
components/Hero.tsx — Props entgegennehmen
components/About.tsx — Props entgegennehmen
components/Contact.tsx — Props entgegennehmen
components/admin/AdminNav.tsx — CMS-Links ergänzen
app/api/admin/passwort/route.ts — 'admins' → 'admin_users'
Bereits vorhanden (kein Handlungsbedarf):
instrumentation.ts ✅ Email-Queue-Worker bereits konfiguriert
app/layout.tsx ✅ PageTracker bereits eingebunden
lib/mailer.ts, lib/email-queue.ts, lib/admin-auth.ts ✅
Task 1: DB-Migrationen — Basis-Tabellen
Auszuführen im Supabase SQL-Editor (https://supabase.mbo-tech-it.de)
- Schritt 1.1: email_queue anlegen
CREATE TABLE IF NOT EXISTS email_queue (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
mail_from text NOT NULL,
mail_to text NOT NULL,
reply_to text,
subject text NOT NULL,
html text NOT NULL,
body_text text NOT NULL,
status text NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'sent', 'failed')),
retry_count int NOT NULL DEFAULT 0,
next_retry_at timestamptz NOT NULL DEFAULT now(),
error_last text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_email_queue_pending
ON email_queue(status, next_retry_at) WHERE status = 'pending';
ALTER TABLE email_queue ENABLE ROW LEVEL SECURITY;
CREATE POLICY "service_role_email_queue" ON email_queue
USING (true) WITH CHECK (true);
- Schritt 1.2: admin_users anlegen
CREATE TABLE IF NOT EXISTS admin_users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text UNIQUE NOT NULL,
name text,
password_hash text NOT NULL,
aktiv boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE admin_users ENABLE ROW LEVEL SECURITY;
CREATE POLICY "service_role_admin_users" ON admin_users
USING (true) WITH CHECK (true);
- Schritt 1.3: anfragen anlegen
CREATE TABLE IF NOT EXISTS anfragen (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
email text NOT NULL,
betreff text NOT NULL,
nachricht text,
status text NOT NULL DEFAULT 'offen',
admin_notizen text,
kunde_id uuid,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE anfragen ENABLE ROW LEVEL SECURITY;
CREATE POLICY "service_role_anfragen" ON anfragen
USING (true) WITH CHECK (true);
CREATE POLICY "kunden_eigene_anfragen" ON anfragen
FOR SELECT USING (auth.uid() = kunde_id);
- Schritt 1.4: Commit
git commit --allow-empty -m "chore: DB-Migrationen 1-3 im Supabase SQL-Editor ausgeführt"
Task 2: DB-Migrationen — Module 02 & 03
Auszuführen im Supabase SQL-Editor
- Schritt 2.1: Modul 02 — Audit-Logs
Inhalt von modules/02-admin-auth/migrations/MIGRATIONS_AUDIT_LOGS.sql ausführen (Tabelle admin_audit_logs).
- Schritt 2.2: Modul 02 — Token-Blacklist
Inhalt von modules/02-admin-auth/migrations/MIGRATIONS_TOKEN_BLACKLIST.sql ausführen (Tabellen admin_session_blacklist, action_token_blacklist).
- Schritt 2.3: Modul 03 — Page Views
Inhalt von modules/03-analytics/migrations/MIGRATIONS_PAGE_VIEWS.sql ausführen (Tabelle page_views).
- Schritt 2.4: Modul 03 — Phone Clicks
Inhalt von modules/03-analytics/migrations/MIGRATIONS_PHONE_CLICKS.sql ausführen (Tabelle phone_clicks).
- Schritt 2.5: Commit
git commit --allow-empty -m "chore: DB-Migrationen Modul 02+03 ausgeführt"
Task 3: DB-Migration — Modul 06-CMS
Auszuführen im Supabase SQL-Editor
- Schritt 3.1: CMS-Migration ausführen
Vollständigen Inhalt von modules/06-website-cms/migrations/MIGRATIONS_WEBSITE_CMS.sql im SQL-Editor ausführen.
Erstellt diese Tabellen:
-
hero_content,hero_badges -
ueber_uns_content,ueber_uns_stats -
galerie_bilder -
kontakt_info,kontakt_oeffnungszeiten,kontakt_social -
Schritt 3.2: Ersten Admin-User anlegen
Hinweis: Dieser Schritt erfordert
bcryptjsim node_modules. Task 4 (npm install) zuerst ausführen, dann hierher zurückkehren. Alternativ: Online-Bcrypt-Generator (z.B. https://bcrypt.online) mit Cost Factor 12 nutzen.
Bcrypt-Hash erzeugen (nach Task 4):
node -e "const b = require('bcryptjs'); b.hash('DEIN_PASSWORT', 12).then(h => console.log(h))"
Dann im SQL-Editor:
INSERT INTO admin_users (email, name, password_hash)
VALUES ('jonny@mbo-tech-it.de', 'Admin', '<HASH_AUS_OBIGEM_BEFEHL>');
- Schritt 3.3: Commit
git commit --allow-empty -m "chore: DB-Migration CMS + Admin-User angelegt"
Task 4: npm-Pakete installieren
- Schritt 4.1: Pakete installieren
npm install nodemailer bcryptjs
npm install -D @types/nodemailer @types/bcryptjs
- Schritt 4.2: Überprüfen
npx tsc --noEmit
Erwartet: Keine neuen Fehler (nur ggf. bestehende, die mit fehlenden DB-Typen zusammenhängen).
- Schritt 4.3: Commit
git add package.json package-lock.json
git commit -m "chore: add nodemailer and bcryptjs dependencies"
Task 5: lib/supabase.ts — CMS-Tabellentypen ergänzen
Datei: lib/supabase.ts
- Schritt 5.1: CMS-Typen in
Database.public.Tableseinfügen
Den bestehenden Abschluss-Block }; des Tables-Objekts vor dem Schließen um folgende Typen erweitern:
hero_content: {
Row: {
id: string;
site_name: string;
site_tagline: string;
logo_path: string | null;
favicon_path: string | null;
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;
updated_at: string;
};
Insert: {
site_name?: string;
site_tagline?: string;
logo_path?: string | null;
favicon_path?: string | null;
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;
updated_at?: string;
};
Update: {
site_name?: string;
site_tagline?: string;
logo_path?: string | null;
favicon_path?: string | null;
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;
updated_at?: string;
};
Relationships: [];
};
hero_badges: {
Row: { id: string; text: string; reihenfolge: number };
Insert: { text: string; reihenfolge?: number };
Update: { text?: string; reihenfolge?: number };
Relationships: [];
};
ueber_uns_content: {
Row: {
id: string;
eyebrow_text: string;
absatz1: string;
absatz2: string;
bild_url: string | null;
updated_at: string;
};
Insert: {
eyebrow_text?: string;
absatz1?: string;
absatz2?: string;
bild_url?: string | null;
updated_at?: string;
};
Update: {
eyebrow_text?: string;
absatz1?: string;
absatz2?: string;
bild_url?: string | null;
updated_at?: string;
};
Relationships: [];
};
ueber_uns_stats: {
Row: { id: string; wert: string; label: string; reihenfolge: number };
Insert: { wert: string; label: string; reihenfolge?: number };
Update: { wert?: string; label?: string; reihenfolge?: number };
Relationships: [];
};
galerie_bilder: {
Row: {
id: string;
storage_path: string;
alt_text: string;
reihenfolge: number;
};
Insert: { storage_path: string; alt_text?: string; reihenfolge?: number };
Update: { storage_path?: string; alt_text?: string; reihenfolge?: number };
Relationships: [];
};
kontakt_info: {
Row: {
id: string;
telefon: string;
email: string;
adresse_zeile1: string;
adresse_zeile2: string;
formular_empfaenger: string;
updated_at: string;
};
Insert: {
telefon?: string;
email?: string;
adresse_zeile1?: string;
adresse_zeile2?: string;
formular_empfaenger?: string;
updated_at?: string;
};
Update: {
telefon?: string;
email?: string;
adresse_zeile1?: string;
adresse_zeile2?: string;
formular_empfaenger?: string;
updated_at?: string;
};
Relationships: [];
};
kontakt_oeffnungszeiten: {
Row: { id: string; tag: string; von: string; bis: string; reihenfolge: number };
Insert: { tag: string; von: string; bis: string; reihenfolge?: number };
Update: { tag?: string; von?: string; bis?: string; reihenfolge?: number };
Relationships: [];
};
kontakt_social: {
Row: { id: string; platform: string; url: string; reihenfolge: number };
Insert: { platform: string; url: string; reihenfolge?: number };
Update: { platform?: string; url?: string; reihenfolge?: number };
Relationships: [];
};
- Schritt 5.2: TypeScript-Check
npx tsc --noEmit
Erwartet: Keine Fehler in lib/supabase.ts.
- Schritt 5.3: Commit
git add lib/supabase.ts
git commit -m "feat: add CMS table types to supabase.ts"
Task 6: Modul 06-CMS Dateien kopieren
- Schritt 6.1: Reguläre Dateien kopieren (PowerShell)
$src = "modules\06-website-cms\files"
$dst = "."
# API-Routen (ohne id/-Ordner)
Copy-Item "$src\app\api\admin\hero\route.ts" -Destination "app\api\admin\hero\" -Force
Copy-Item "$src\app\api\admin\hero\bild\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\hero\bild").FullName
Copy-Item "$src\app\api\admin\hero\logo\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\hero\logo").FullName
Copy-Item "$src\app\api\admin\hero\favicon\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\hero\favicon").FullName
Copy-Item "$src\app\api\admin\hero\badges\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\hero\badges").FullName
Copy-Item "$src\app\api\admin\galerie\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\galerie").FullName
Copy-Item "$src\app\api\admin\galerie\sort\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\galerie\sort").FullName
Copy-Item "$src\app\api\admin\ueber-uns\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\ueber-uns").FullName
Copy-Item "$src\app\api\admin\ueber-uns\upload\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\ueber-uns\upload").FullName
Copy-Item "$src\app\api\admin\ueber-uns\stats\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\ueber-uns\stats").FullName
Copy-Item "$src\app\api\admin\kontakt\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\kontakt").FullName
Copy-Item "$src\app\api\admin\kontakt\oeffnungszeiten\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\kontakt\oeffnungszeiten").FullName
Copy-Item "$src\app\api\admin\kontakt\social\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\kontakt\social").FullName
Copy-Item "$src\app\api\admin\passwort\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\passwort").FullName
# Dynamic-Route-Ordner [id] (Ordner heißt im Modul "id", muss "[id]" heißen)
Copy-Item "$src\app\api\admin\hero\badges\id\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\hero\badges\[id]").FullName
Copy-Item "$src\app\api\admin\ueber-uns\stats\id\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\ueber-uns\stats\[id]").FullName
Copy-Item "$src\app\api\admin\kontakt\oeffnungszeiten\id\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\kontakt\oeffnungszeiten\[id]").FullName
Copy-Item "$src\app\api\admin\kontakt\social\id\route.ts" -Destination (New-Item -ItemType Directory -Force "app\api\admin\kontakt\social\[id]").FullName
# Admin-Seiten
Copy-Item "$src\app\admin\hero\page.tsx" -Destination (New-Item -ItemType Directory -Force "app\admin\hero").FullName
Copy-Item "$src\app\admin\galerie\page.tsx" -Destination (New-Item -ItemType Directory -Force "app\admin\galerie").FullName
Copy-Item "$src\app\admin\ueber-uns\page.tsx" -Destination (New-Item -ItemType Directory -Force "app\admin\ueber-uns").FullName
Copy-Item "$src\app\admin\kontakt\page.tsx" -Destination (New-Item -ItemType Directory -Force "app\admin\kontakt").FullName
Copy-Item "$src\app\admin\passwort\page.tsx" -Destination (New-Item -ItemType Directory -Force "app\admin\passwort").FullName
# Komponenten
Copy-Item "$src\components\GalerieAnzeige.tsx" -Destination "components\" -Force
Copy-Item "$src\components\admin\HeroVerwaltung.tsx" -Destination "components\admin\" -Force
Copy-Item "$src\components\admin\GalerieVerwaltung.tsx" -Destination "components\admin\" -Force
Copy-Item "$src\components\admin\UeberUnsVerwaltung.tsx" -Destination "components\admin\" -Force
Copy-Item "$src\components\admin\KontaktVerwaltung.tsx" -Destination "components\admin\" -Force
Copy-Item "$src\components\admin\PasswortAendern.tsx" -Destination "components\admin\" -Force
- Schritt 6.2: Verify
ls app/api/admin/hero/
ls app/api/admin/hero/badges/
Erwartet: route.ts, bild/, logo/, favicon/, badges/ (mit route.ts und [id]/route.ts)
- Schritt 6.3: Commit
git add app/api/admin/ app/admin/hero/ app/admin/galerie/ app/admin/ueber-uns/ app/admin/kontakt/ app/admin/passwort/ components/GalerieAnzeige.tsx components/admin/HeroVerwaltung.tsx components/admin/GalerieVerwaltung.tsx components/admin/UeberUnsVerwaltung.tsx components/admin/KontaktVerwaltung.tsx components/admin/PasswortAendern.tsx
git commit -m "feat: add website-cms module files"
Task 7: app/api/admin/passwort/route.ts anpassen
Die kopierte Datei referenziert db.from('admins') — muss admin_users sein.
Datei: app/api/admin/passwort/route.ts
- Schritt 7.1: Tabellenname korrigieren
Beide Vorkommen von 'admins' durch 'admin_users' ersetzen:
// Zeile ~19: vorher
const { data: admin } = await db.from('admins').select('password_hash').eq('id', session.id).single()
// nachher
const { data: admin } = await db.from('admin_users').select('password_hash').eq('id', session.id).single()
// Zeile ~25: vorher
const { error } = await db.from('admins').update({ password_hash: hash }).eq('id', session.id)
// nachher
const { error } = await db.from('admin_users').update({ password_hash: hash }).eq('id', session.id)
- Schritt 7.2: TypeScript-Check
npx tsc --noEmit
- Schritt 7.3: Commit
git add app/api/admin/passwort/route.ts
git commit -m "fix: use admin_users table in passwort route"
Task 8: components/admin/HeroVerwaltung.tsx — Standardwerte anpassen
- Schritt 8.1: DEFAULTS im Code anpassen
In components/admin/HeroVerwaltung.tsx den DEFAULTS-Block suchen (ca. Zeile 33) und ersetzen:
const DEFAULTS: HeroContent = {
site_name: 'MBO-Tech-IT',
site_tagline: 'IT-Service Crailsheim',
eyebrow_text: 'Digital Denken. Lokal Handeln.',
headline1: 'Ihre IT-Infrastruktur.',
headline2: 'Professionell. Zuverlässig.',
subtext1: 'Ihr IT-Dienstleister in Crailsheim — egal ob zu Hause oder im Büro.',
subtext2: '',
cta1_text: 'Jetzt anfragen', cta1_href: '#contact',
cta2_text: 'Unsere Leistungen', cta2_href: '#services',
bg_image_path: null,
}
- Schritt 8.2: Gleiche Defaults in API-Route setzen
In app/api/admin/hero/route.ts den fields-Block im PATCH-Handler anpassen:
const fields = {
site_name: body.site_name ?? 'MBO-Tech-IT',
site_tagline: body.site_tagline ?? 'IT-Service Crailsheim',
eyebrow_text: body.eyebrow_text ?? 'Digital Denken. Lokal Handeln.',
headline1: body.headline1 ?? 'Ihre IT-Infrastruktur.',
headline2: body.headline2 ?? 'Professionell. Zuverlässig.',
subtext1: body.subtext1 ?? 'Ihr IT-Dienstleister in Crailsheim',
subtext2: body.subtext2 ?? '',
cta1_text: body.cta1_text ?? 'Jetzt anfragen',
cta1_href: body.cta1_href ?? '#contact',
cta2_text: body.cta2_text ?? 'Unsere Leistungen',
cta2_href: body.cta2_href ?? '#services',
updated_at: new Date().toISOString(),
}
- Schritt 8.3: Commit
git add components/admin/HeroVerwaltung.tsx app/api/admin/hero/route.ts
git commit -m "feat: set MBO-Tech-IT defaults in hero verwaltung"
Task 9: components/admin/AdminNav.tsx — CMS-Links ergänzen
Datei: components/admin/AdminNav.tsx
- Schritt 9.1:
navLinks-Array erweitern
Den bestehenden navLinks-Array ersetzen:
const navLinks = [
{ href: "/admin/hero", label: "Hero" },
{ href: "/admin/ueber-uns", label: "Über uns" },
{ href: "/admin/galerie", label: "Galerie" },
{ href: "/admin/kontakt", label: "Kontakt" },
{ href: "/admin/passwort", label: "Passwort" },
{ href: "/admin/analytics", label: "Analytics" },
{ href: "/admin/statistik", label: "Statistik" },
{ href: "/admin/audit-logs", label: "Audit-Logs" },
];
- Schritt 9.2: TypeScript-Check
npx tsc --noEmit
- Schritt 9.3: Commit
git add components/admin/AdminNav.tsx
git commit -m "feat: add CMS links to admin navigation"
Task 10: components/Hero.tsx — Props entgegennehmen
Die Komponente bleibt "use client" (wegen useTypewriter), nimmt aber Inhalte als Props.
Datei: components/Hero.tsx
- Schritt 10.1: Props-Interface + Defaults hinzufügen
Am Anfang der Datei nach dem Import-Block einfügen:
interface HeroProps {
eyebrowText?: string;
headline1?: string;
subtext1?: string;
subtext2?: string;
cta1Text?: string;
cta1Href?: string;
cta2Text?: string;
cta2Href?: string;
bgImagePath?: string | null;
badges?: string[];
}
- Schritt 10.2: Funktionssignatur anpassen
export default function Hero({
eyebrowText = "Digital Denken. Lokal Handeln.",
headline1 = "Ihre IT-Infrastruktur.",
subtext1 = "Ihr IT-Dienstleister in Crailsheim — egal ob zu Hause oder im Büro. Von Hard- & Software über Netzwerk & WLAN bis hin zu Webseiten & Webanwendungen: Wir lösen Ihre IT-Probleme persönlich und zum fairen Preis.",
subtext2 = "",
cta1Text = "Jetzt anfragen",
cta1Href = "#contact",
cta2Text = "Unsere Leistungen",
cta2Href = "#services",
bgImagePath = null,
badges = ["Docker", "Kubernetes", "Proxmox", "Hetzner Cloud", "Linux"],
}: HeroProps = {}) {
- Schritt 10.3: JSX anpassen — Badge, Headline, Subtext, Buttons
Im JSX-Block die hardkodierten Werte durch Props ersetzen:
{/* Tagline badge */}
<div className="inline-flex items-center gap-2 px-5 py-2 rounded-full border border-orange-500/40 bg-orange-500/10 text-orange-400 text-xs font-black tracking-[0.3em] uppercase mb-8">
<span className="w-1.5 h-1.5 bg-orange-400 rounded-full animate-pulse" />
{eyebrowText}
</div>
{/* Headline */}
<h1 className="text-5xl sm:text-6xl lg:text-7xl font-black text-slate-900 dark:text-white leading-tight mb-6 tracking-tight">
{headline1}{" "}
<span className="text-gradient">
{typed}
<span className="animate-pulse">|</span>
</span>
</h1>
{/* Subheadline */}
{subtext1 && (
<p className="text-xl sm:text-2xl text-slate-600 dark:text-slate-400 max-w-3xl mx-auto mb-4 leading-relaxed">
{subtext1}
</p>
)}
{subtext2 && (
<p className="text-xl sm:text-2xl text-slate-600 dark:text-slate-400 max-w-3xl mx-auto mb-4 leading-relaxed">
{subtext2}
</p>
)}
{/* Tech pills (aus hero_badges) */}
<div className="flex flex-wrap items-center justify-center gap-3 mb-10">
{badges.map((badge) => (
<span
key={badge}
className="px-3 py-1 rounded-md bg-slate-100 dark:bg-gray-900 border border-slate-200 dark:border-gray-700 text-slate-700 dark:text-slate-300 text-sm font-mono"
>
{badge}
</span>
))}
</div>
{/* CTAs */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<a href={cta1Href} className="btn-primary w-full sm:w-auto px-8 py-4 text-lg">
{cta1Text}
</a>
<a href={cta2Href} className="btn-secondary w-full sm:w-auto px-8 py-4 text-lg">
{cta2Text}
</a>
</div>
- Schritt 10.4: Hintergrundbild optional
Das Background-<div> mit bedingtem Bild ergänzen (falls bgImagePath gesetzt):
{bgImagePath && (
<div
className="absolute inset-0 bg-cover bg-center opacity-20"
style={{ backgroundImage: `url(${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/hero-bilder/${bgImagePath})` }}
/>
)}
- Schritt 10.5: TypeScript-Check
npx tsc --noEmit
- Schritt 10.6: Commit
git add components/Hero.tsx
git commit -m "feat: hero component accepts cms props"
Task 11: components/About.tsx — Props entgegennehmen
Datei: components/About.tsx
- Schritt 11.1: Props-Interface hinzufügen
Am Dateianfang (vor dem highlights-Array):
interface AboutProps {
eyebrowText?: string;
absatz1?: string;
absatz2?: string;
stats?: { wert: string; label: string }[];
}
- Schritt 11.2: Funktionssignatur anpassen
export default function About({
eyebrowText = "Über uns",
absatz1 = "MBO-Tech-IT steht für über 30 Jahre IT-Erfahrung — angefangen bei der einfachen Hardware-Wartung, über den Aufbau großer Client-Server-Netzwerke, bis hin zu mehr als 20 Jahren Spezialisierung in der IT-Security.",
absatz2 = "Heute liegt der Fokus auf modernen Container-Technologien, Cloud-nativer Infrastruktur und Virtualisierung. Dieses breite Fundament ermöglicht es, Lösungen zu entwickeln, die nicht nur funktionieren — sondern auch sicher und langfristig tragfähig sind.",
stats,
}: AboutProps = {}) {
- Schritt 11.3: JSX anpassen
Die hartkodierten Texte im JSX durch Props ersetzen:
<span className="text-orange-400 font-mono text-xs font-bold tracking-[0.25em] uppercase">
{eyebrowText}
</span>
<p className="text-slate-600 dark:text-slate-400 text-lg leading-relaxed mb-6">
{absatz1}
</p>
<p className="text-slate-600 dark:text-slate-400 text-lg leading-relaxed mb-8">
{absatz2}
</p>
Falls stats befüllt ist, die Statistiken rendern (als Ergänzung zu den bestehenden highlights):
{stats && stats.length > 0 && (
<div className="flex gap-8 mt-4">
{stats.map((s) => (
<div key={s.label}>
<div className="text-3xl font-black text-orange-400">{s.wert}</div>
<div className="text-sm text-slate-500 dark:text-slate-400">{s.label}</div>
</div>
))}
</div>
)}
- Schritt 11.4: Commit
git add components/About.tsx
git commit -m "feat: about component accepts cms props"
Task 12: components/Contact.tsx — Props entgegennehmen
Datei: components/Contact.tsx
- Schritt 12.1: Props-Interface hinzufügen
Am Dateianfang (nach dem "use client" Directive):
interface OeffnungsZeit { tag: string; von: string; bis: string }
interface ContactProps {
telefon?: string;
email?: string;
adresseZeile1?: string;
adresseZeile2?: string;
oeffnungszeiten?: OeffnungsZeit[];
}
- Schritt 12.2: Datei-level
contactItems-Array entfernen und Funktionssignatur anpassen
Die bestehende const contactItems = [...] auf Datei-Ebene (Zeilen 3–37) vollständig entfernen. Dann die Funktionssignatur ersetzen und contactItems in den Funktionskörper verschieben:
export default function Contact({
telefon = "+49 171 9345193",
email = "kontakt@mbo-tech-it.de",
adresseZeile1 = "Mörikestr. 2",
adresseZeile2 = "74564 Crailsheim",
oeffnungszeiten,
}: ContactProps = {}) {
const contactItems = [
{
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>,
label: "Telefon",
value: telefon,
href: `tel:${telefon.replace(/\s/g, "")}`,
},
{
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>,
label: "E-Mail",
value: email,
href: `mailto:${email}`,
},
{
icon: <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>,
label: "Standort",
value: `${adresseZeile1}, ${adresseZeile2}`,
href: `https://maps.google.com/?q=${encodeURIComponent(adresseZeile1 + " " + adresseZeile2)}`,
},
]
- Schritt 12.3: Öffnungszeiten-Block im JSX ergänzen
Unterhalb der contactItems-Liste im JSX (nach der letzten contactItems.map-Ausgabe):
{oeffnungszeiten && oeffnungszeiten.length > 0 && (
<div className="mt-6 pt-6 border-t border-slate-200 dark:border-slate-700">
<h4 className="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-3">
Öffnungszeiten
</h4>
<ul className="space-y-1">
{oeffnungszeiten.map((oz) => (
<li key={oz.tag} className="flex justify-between text-sm text-slate-600 dark:text-slate-400">
<span>{oz.tag}</span>
<span>{oz.von} – {oz.bis}</span>
</li>
))}
</ul>
</div>
)}
- Schritt 12.4: Commit
git add components/Contact.tsx
git commit -m "feat: contact component accepts cms props"
Task 13: app/page.tsx dynamisieren
Datei: app/page.tsx
- Schritt 13.1: Datei vollständig ersetzen
import { createServiceClient } from "@/lib/supabase";
import Header from "@/components/Header";
import Hero from "@/components/Hero";
import TaglineBanner from "@/components/TaglineBanner";
import StatsBar from "@/components/StatsBar";
import Services from "@/components/Services";
import Technologies from "@/components/Technologies";
import DataSovereignty from "@/components/DataSovereignty";
import PaperlessSection from "@/components/PaperlessSection";
import About from "@/components/About";
import Contact from "@/components/Contact";
import Footer from "@/components/Footer";
import { GalerieAnzeige } from "@/components/GalerieAnzeige";
export const dynamic = "force-dynamic";
export default async function Home() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
let hero = null,
badges: { text: string }[] = [],
ueberUns = null,
stats: { wert: string; label: string }[] = [],
galerie: { id: string; storage_path: string; alt_text: string }[] = [],
kontakt = null,
oeffnung: { tag: string; von: string; bis: string }[] = [];
try {
const db = createServiceClient();
const [
heroResult,
badgesResult,
ueberUnsResult,
statsResult,
galerieResult,
kontaktResult,
oeffnungResult,
] = await Promise.allSettled([
db.from("hero_content").select("*").limit(1).single(),
db.from("hero_badges").select("*").order("reihenfolge"),
db.from("ueber_uns_content").select("*").limit(1).single(),
db.from("ueber_uns_stats").select("*").order("reihenfolge"),
db.from("galerie_bilder").select("*").order("reihenfolge"),
db.from("kontakt_info").select("*").limit(1).single(),
db.from("kontakt_oeffnungszeiten").select("*").order("reihenfolge"),
]);
hero = heroResult.status === "fulfilled" ? heroResult.value.data : null;
badges = badgesResult.status === "fulfilled" ? (badgesResult.value.data ?? []) : [];
ueberUns = ueberUnsResult.status === "fulfilled" ? ueberUnsResult.value.data : null;
stats = statsResult.status === "fulfilled" ? (statsResult.value.data ?? []) : [];
galerie = galerieResult.status === "fulfilled" ? (galerieResult.value.data ?? []) : [];
kontakt = kontaktResult.status === "fulfilled" ? kontaktResult.value.data : null;
oeffnung = oeffnungResult.status === "fulfilled" ? (oeffnungResult.value.data ?? []) : [];
} catch {
// Supabase nicht konfiguriert — Fallback-Werte greifen
}
const galerieBilder = galerie.map((b) => ({
id: b.id,
src: `${supabaseUrl}/storage/v1/object/public/galerie-bilder/${b.storage_path}`,
alt: b.alt_text,
}));
return (
<main className="min-h-screen bg-[#f0f4f8] dark:bg-[#18212f]">
<Header />
<Hero
eyebrowText={hero?.eyebrow_text}
headline1={hero?.headline1}
subtext1={hero?.subtext1}
subtext2={hero?.subtext2}
cta1Text={hero?.cta1_text}
cta1Href={hero?.cta1_href}
cta2Text={hero?.cta2_text}
cta2Href={hero?.cta2_href}
bgImagePath={hero?.bg_image_path}
badges={badges.length > 0 ? badges.map((b) => b.text) : undefined}
/>
<TaglineBanner />
<StatsBar />
<Services />
<TaglineBanner />
<PaperlessSection />
<DataSovereignty />
<TaglineBanner />
<Technologies />
<About
eyebrowText={ueberUns?.eyebrow_text}
absatz1={ueberUns?.absatz1}
absatz2={ueberUns?.absatz2}
stats={stats.length > 0 ? stats : undefined}
/>
{galerieBilder.length > 0 && (
<>
<TaglineBanner />
<section className="py-16 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<GalerieAnzeige bilder={galerieBilder} />
</div>
</section>
</>
)}
<TaglineBanner />
<Contact
telefon={kontakt?.telefon}
email={kontakt?.email}
adresseZeile1={kontakt?.adresse_zeile1}
adresseZeile2={kontakt?.adresse_zeile2}
oeffnungszeiten={oeffnung.length > 0 ? oeffnung : undefined}
/>
<Footer />
</main>
);
}
- Schritt 13.2: TypeScript-Check
npx tsc --noEmit
Erwartet: Keine Fehler.
- Schritt 13.3: Dev-Server starten und Seite prüfen
npm run dev
Prüfen unter http://localhost:3000:
-
Seite lädt ohne Fehler (Fallback-Werte werden gezeigt solange DB-Tabellen leer sind)
-
Kein Absturz, kein 500-Error in der Konsole
-
Schritt 13.4: Commit
git add app/page.tsx
git commit -m "feat: load homepage sections from supabase cms"
Task 14: Build-Check & Abschluss
- Schritt 14.1: TypeScript vollständig prüfen
npx tsc --noEmit
Erwartet: 0 Fehler.
- Schritt 14.2: Produktions-Build prüfen
npm run build
Erwartet: Kein Build-Fehler. Nur Warnungen zu force-dynamic sind akzeptabel.
- Schritt 14.3: Admin-Bereich testen
Dev-Server starten und manuell testen:
http://localhost:3000/admin/login→ Login mit dem angelegten Admin-User/admin/hero→ Texte anpassen, speichern, Startseite neu laden → Änderung sichtbar/admin/galerie→ Bild hochladen (Storage Bucket muss existieren)/admin/ueber-uns→ Text + Statistik speichern/admin/kontakt→ Öffnungszeiten + Kontaktdaten speichern/admin/passwort→ Passwort ändern, neu einloggen
- Schritt 14.4: Supabase-Infrastructure (manuell im Dashboard)
Im Supabase-Dashboard https://supabase.mbo-tech-it.de:
Storage → New Bucket (alle vier anlegen, Public = ✓):
hero-bildersite-assetsueber-uns-bildergalerie-bilder
Authentication → URL Configuration:
-
Site URL:
https://mbo-tech-it.de -
Redirect URLs:
https://mbo-tech-it.de/auth/callback -
Email Confirmations: aktivieren
-
Schritt 14.5: git push
git push
Abhängigkeiten zwischen Tasks
Task 1 (email_queue, admin_users, anfragen)
└── Task 2 (audit_logs, token_blacklist, page_views, phone_clicks)
└── Task 3 (CMS + Admin-User)
└── Task 4 (npm install)
└── Task 5 (supabase.ts Typen)
└── Task 6 (CMS-Dateien kopieren)
├── Task 7 (passwort route fix)
├── Task 8 (Hero-Defaults)
└── Task 9 (AdminNav)
└── Task 10 (Hero.tsx props)
└── Task 11 (About.tsx props)
└── Task 12 (Contact.tsx props)
└── Task 13 (page.tsx dynamisieren) ← benötigt Tasks 10-12
└── Task 14 (Build-Check)