# Lead-Capture & Content-Gate Engine — Design (Phase 1)

**Datum:** 2026-07-01
**Branch:** feat/wissen-subdomain
**Status:** Entwurf zur Freigabe

## Ziel & Kontext

Mehr E-Mail-Adressen für Marketing sammeln, DSGVO-fest, seitenübergreifend auf `*.kotsch.tech`.
Kern: Besucher geben ihre E-Mail **gegen Inhalte/Downloads** her und **verifizieren per OTP-Code**
(6-stellig, inline). Wer einmal verifiziert ist, wird auf keiner Subdomain erneut gefragt.

Bestehende Bausteine, an denen wir uns orientieren: ProblemRadar `EmailContact`/`EmailConsent`
(Einwilligung + Abmelde-Token), Admin-OTP-System (`admin_otps`), Shop signierte Download-Links,
`ContactSpamGuard`, config-getriebene Seiten (webtools, cross_apps).

Dieses Dokument beschreibt **nur Phase 1: den Motor**. Phase 2 (Gate in News/Webtools) und
Phase 3 (100 SEO-Sammel-Seiten mit Inhalt) sind eigene Specs.

## Umfang Phase 1

**Drin:**
- Datenmodell `leads`, `lead_otps`, `lead_events`.
- OTP-Verifizierung inline (E-Mail → Code per Mail → Code eingeben → freigeschaltet + verifiziert).
- „Einmal eintragen"-Cookie auf `.kotsch.tech` + Server-Helper `LeadGate`.
- Wiederverwendbare Blade-Components `<x-lead-capture>` und `<x-content-gate>`.
- Lead-Magnet-Auslieferung per signiertem Download-Link (nutzt `pay/_private/files`).
- Paywall-Structured-Data (`isAccessibleForFree:false`) für gesperrte Bereiche.
- Config-getriebenes Seiten-System `config/lead_pages.php` + `LeadPageController` + Vorlage +
  Route `/gratis/{slug}` + Sitemap-Aufnahme + **1–2 Beispielseiten** als Beweis/Vorlage.
- `config/lead_magnets.php` + 1–2 echte Magnets (bestehende PDFs wiederverwenden).
- Admin `/admin/leads`: Liste/Filter (Status/Quelle), Statistik (gesamt·bestätigt·Conversion),
  CSV-Export, DSGVO-Löschen, Bestätigung/Code erneut senden. Sidebar-Eintrag.
- Spam-Schutz (Honeypot + Throttle + Wiederverwendung `ContactSpamGuard`-Logik falls passend).
- DSGVO: Consent-Checkbox (nicht vorangehakt), Consent-Snapshot + Version, Abmeldung, Datenschutz-Abschnitt.
- Feature-Tests.

**Draußen (spätere Phasen):**
- Gate in bestehende News-Artikel und Webtools (Phase 2).
- Die restlichen ~98 Sammel-Seiten samt Inhalt (Phase 3).
- Versand von Newslettern/Kampagnen an den Verteiler (separat; Export genügt vorerst).

## Datenmodell

### `leads`
- `id`
- `email` — string, **unique**, normalisiert (lowercase, trim).
- `status` — `pending` | `confirmed` | `unsubscribed` (default `pending`).
- `confirmed_at` — nullable.
- `unsubscribe_token` — string, unique.
- `source` — string nullable (z.B. `landing:seo-checkliste-2026`, `webtool:roi-rechner`, `news:<slug>`).
- `incentive` — string nullable (Magnet-Slug | `newsletter` | `discount` | `gate`).
- `locale` — string, default `de`.
- `consent_version` — string (welcher Einwilligungstext galt).
- `consent_text` — text (Snapshot des zugestimmten Textes, DSGVO-Nachweis).
- `ip_hash` — string nullable (Nachweis, keine Klartext-IP).
- `last_magnet_slug` — string nullable (zuletzt ausgeliefert).
- `timestamps`.

### `lead_otps` (spiegelt `admin_otps`)
- `id`
- `email` — index.
- `code_hash` — sha256 des 6-stelligen Codes (nie Klartext).
- `expires_at` — z.B. +15 Min.
- `attempts` — int default 0 (max. 5, dann invalidieren).
- `consumed_at` — nullable.
- `created_at`.

### `lead_events` (Audit + Funnel)
- `id`
- `lead_id` — nullable FK.
- `type` — `signup` | `otp_sent` | `confirmed` | `magnet_sent` | `unsubscribed` | `resend`.
- `meta` — json nullable (source, magnet, ip_hash).
- `created_at`.

## Verifizierungs-Ablauf (OTP inline)

1. **`POST /lead/subscribe`** `{email, incentive, source, consent, website(honeypot)}`
   - Validierung: gültige E-Mail, `consent` akzeptiert, Honeypot leer. Throttle (z.B. 5/Min/IP).
   - E-Mail normalisieren. `Lead` finden oder als `pending` anlegen (Consent-Snapshot + Version + ip_hash).
   - **Wenn Lead bereits `confirmed`:** OTP überspringen → Remember-Cookie setzen, Anreiz sofort
     ausliefern, Erfolg zurück („schon dabei / freigeschaltet"). (Erfüllt „einmal eintragen reicht".)
   - **Sonst:** OTP (6-stellig) erzeugen/erneuern, per Mail senden, `otp_sent` loggen. Antwort = „Code-Schritt".
2. **`POST /lead/verify`** `{email, code}`
   - OTP prüfen (Hash, nicht abgelaufen, attempts < max, nicht consumed).
   - Erfolg → Lead `confirmed` + `confirmed_at`; `.kotsch.tech`-Cookie setzen; `confirmed` loggen;
     **Anreiz ausliefern** (Magnet-Mail mit signiertem Link / Rabattcode / Freischaltung). Erfolg + Unlock.
   - Fehler → `attempts++`; nach Max invalidieren, „erneut senden" anbieten.
3. **`POST /lead/resend`** — neues OTP (Throttle).
4. **`GET /lead/abmelden/{unsubscribe_token}`** — Lead `unsubscribed` + `unsubscribed_at`, loggen,
   Bestätigungsseite. Abmelde-Link steckt in jeder Mail.

Antworten unterstützen sowohl klassischen POST (Redirect + Flash) als auch AJAX (JSON) für die
Inline-Nutzung in Component/Gate.

## „Einmal eintragen" — Remember-Cookie

- Cookie `kt_sub`, Wert = signierter Marker (nicht die E-Mail — Datenschutz), `HttpOnly`,
  `SameSite=Lax`, Laufzeit 1 Jahr, **Domain = `config('leads.cookie_domain')`** (Default `.kotsch.tech`,
  lokal/Preview `null`).
- Gesetzt bei Bestätigung **und** wenn eine bereits bestätigte E-Mail erneut eingegeben wird.
- Helper `App\Support\Leads\LeadGate::isUnlocked(Request): bool` (Cookie gültig?) — von Components/Gate
  genutzt, um Formular auszublenden bzw. gesperrten Inhalt freizugeben.
- Grenze bewusst akzeptiert: Cookie ist pro Browser (Standard bei Metered-Gates).

## Components

### `<x-lead-capture variant="inline|box" incentive="…" source="…" :headline :subline>`
Formular: E-Mail + Consent-Checkbox (+ Datenschutz-Link) → nach Absenden **OTP-Schritt** (Code-Eingabe +
„erneut senden"). Bei gesetztem Remember-Cookie: statt Formular dezent „✓ Du bist dabei".
Markenkonform, hell, leicht. Nutzt gemeinsames Teil-Template + kleines JS (progressive enhancement:
funktioniert auch ohne JS über normale POSTs).

### `<x-content-gate source="…" incentive="…">`
Zwei Slots: `teaser` (immer sichtbar, indexierbar) und `gated` (nur nach Unlock).
- Unlocked → `gated` rendern.
- Sonst → `teaser` + Verwischungs-/Abschluss-Verlauf + `<x-lead-capture>` + **Paywall-Schema**
  (`isAccessibleForFree:false`, `hasPart` mit `cssSelector` auf den gesperrten Bereich).

## Lead-Magnets

`config/lead_magnets.php`: `slug => [title, description, file (rel. zu pay/_private/files o. downloads),
delivery_subject, delivery_body]`.
Auslieferung nach Bestätigung: signierter, zeitlich begrenzter Download-Link (Muster wie Shop
`temporarySignedRoute`), Route `GET /lead/download/{lead}/{magnet}` (`signed`).

## Öffentliche Routen (Übersicht)

```
POST /lead/subscribe            leads.subscribe   (throttle)
POST /lead/verify               leads.verify      (throttle)
POST /lead/resend               leads.resend      (throttle)
GET  /lead/abmelden/{token}     leads.unsubscribe
GET  /lead/download/{lead}/{magnet}  leads.download (signed)
GET  /gratis/{slug}             leads.page        (SEO-Seite)
```
CSRF-Ausnahmen nicht nötig (alles First-Party mit Token). Locale-Prefix wie im restlichen Web.

## Admin

`Admin\AdminLeadController` unter `/admin/leads` (admin.auth + 2FA), Sidebar-Abschnitt „Marketing → Leads":
- Liste mit Filter (Status, Quelle, Zeitraum) + Suche (E-Mail).
- Statistik: gesamt, bestätigt, offen, abgemeldet, Conversion-Rate, Top-Quellen.
- CSV-Export (gefiltert).
- DSGVO-Löschen (Lead + Events + OTPs).
- „Code/Bestätigung erneut senden".

## DSGVO

- Double-Opt-In via OTP (verifizierte Einwilligung, Art. 6 Abs. 1 lit. a).
- Consent-Text + Version pro Lead gespeichert (Nachweis).
- Abmelde-Link in jeder Mail; Löschung im Admin.
- Neuer Abschnitt in `config/datenschutz.php` (Marketing-Newsletter/Lead-Erfassung), DE+EN.
- Keine Klartext-IP (nur `ip_hash`).

## Tests (Feature)

- Subscribe legt `pending` an + sendet OTP (Mail::fake) + `otp_sent`-Event.
- Verify mit korrektem Code → `confirmed`, Cookie gesetzt, Magnet-Mail, `confirmed`+`magnet_sent`-Events.
- Falscher Code → kein Confirm, attempts++.
- Bereits bestätigte E-Mail → überspringt OTP, liefert direkt aus.
- Dedup: gleiche E-Mail erzeugt keinen zweiten Lead.
- Abmeldung über Token → `unsubscribed`.
- Gate versteckt gesperrten Inhalt ohne Cookie, zeigt ihn mit gültigem Cookie; Teaser immer sichtbar;
  Paywall-Schema im HTML.
- `/gratis/{slug}` rendert (Beispielseite) inkl. FAQ/Paywall-Schema.
- Admin `/admin/leads` hinter Auth; CSV-Export enthält Leads; Löschen entfernt alles.

## Datei-Übersicht (geplant)

- Migration `…_create_lead_tables.php` (leads, lead_otps, lead_events).
- Models `App\Models\Lead`, `LeadOtp`, `LeadEvent`.
- Service `App\Support\Leads\LeadService` (subscribe, verify, resend, deliverMagnet, unsubscribe) +
  `LeadGate` (Cookie/Unlock) + `LeadOtp`-Logik.
- Controller `App\Http\Controllers\LeadController` (subscribe/verify/resend/unsubscribe/download) +
  `LeadPageController` (SEO-Seite).
- Admin `App\Http\Controllers\Admin\AdminLeadController` + View `admin/leads/index.blade.php` + Sidebar.
- Components `resources/views/components/lead-capture.blade.php`, `content-gate.blade.php` (+ shared Partial + JS).
- Config `config/leads.php` (cookie_domain, otp ttl/max, consent version/text), `config/lead_magnets.php`,
  `config/lead_pages.php` (+ 1–2 Beispielseiten).
- Mails: OTP-Code-Mail, Magnet-Auslieferungs-Mail (Blade).
- Datenschutz-Abschnitt in `config/datenschutz.php`.
- Tests `tests/Feature/Leads/...`.

## Offene Annahmen (Standard, änderbar)

- URL-Basis der Sammel-Seiten: `/gratis/{slug}`.
- Erste Magnets aus bestehenden PDFs (`pay/_private/files`).
- Sprache DE + EN.
- OTP: 6-stellig, 15 Min gültig, max. 5 Versuche.
- Themen der 100 Seiten (Phase 3): Business/Marketing-Vorlagen, KI/ChatGPT, IT-Hilfe/How-to,
  Selbstständigkeit/Finanzen — Mischung über alle vier.
