# Design-Spec: Awin-Affiliate im Artikel-System (kotsch.tech)

**Datum:** 2026-07-21 · **Status:** vom User freigegeben (Ansatz A + B) · **Branch:** feat/wissen-subdomain

## Ziel

Mit Awin-Partnerprogrammen Geld verdienen, ohne das bewährte Artikel-System umzubauen:

1. **A — Redaktionell:** Kaufberatungs-/Vergleichs-Artikel im News-System enthalten Produktboxen, deren Links über eine zentrale, im Admin gepflegte Weiterleitung (`/empfehlung/{key}`) auf Awin-Deeplinks führen.
2. **B — KotschAds:** Awin-Anzeigen laufen zusätzlich als `affiliate`-Anzeigen über die vorhandene House-Ad-Engine auf allen Sites.

Nischen-neutral: Es ist unbekannt, welche Advertiser den Publisher annehmen. Der Haupt-Workflow ist daher, **pro angenommenem Programm neue Artikel anzulegen** (bestehende werden nachgerüstet, wo es passt).

## Ist-Zustand (relevant)

- Artikel = statische Ordner `news/{slug}/index.html` (1.422 Stück). `AdditionalWebsiteController` parst Titel/Tags/Kategorie/Body (`.article__body`) und rendert über `resources/views/news/show.blade.php`. Neue Artikel erscheinen automatisch in Übersicht, Sitemap, Kategorien, „Ähnliche Artikel“.
- `show.blade.php` leitet externe Links im Body per JS über das `/go`-Interstitial (manueller Bestätigungs-Klick) — kotsch.tech-Hosts sind ausgenommen. Externe Links landen außerdem in der „Weiterführende Links“-Karte (`outboundLinks()`, kotsch.tech-Hosts ausgenommen).
- KotschAds: `HouseAd` (Typen enthalten bereits `affiliate`), Auswahl via `HouseAd::pickFor(site, zone, n)`, Klick-Zählung via `/api/ads/go/{id}`. Kotsch-Partials: `news/_ad-sidebar.blade.php` (Zone sidebar), `partials/direct-link-ads.blade.php` (Zone inline), `tools/_ads_banner.blade.php`. Block-Label heißt derzeit „Empfehlungen“.
- Datenschutz ist config-getrieben (`config/datenschutz.php` + DatenschutzController).

## Datenfluss

```
Artikel-Produktbox
  → https://kotsch.tech/empfehlung/{key}?ref={artikel-slug}   (interner Link → kein /go-Interstitial)
  → 302 auf Awin-Deeplink, clickref={ref} angehängt            (Klick-Zähler in DB)
  → Händler-Shop (Awin-Tracking greift erst ab awin1.com)
```

Auswertung: Klicks je Link im Admin; Conversions je Artikel im Awin-Dashboard über `clickref`.

## Komponenten

### 1. DB + Model: `affiliate_links`

Migration (Muster: `app_links`):

| Spalte | Typ | Bemerkung |
|---|---|---|
| id | bigint PK | |
| key | string, unique | Slug `[a-z0-9-]`, z. B. `bosch-gsr-12v` |
| label | string | Produkt-/Linkname (Admin-Anzeige) |
| merchant | string | Händler-/Advertiser-Name |
| url | text | Awin-Deeplink (aus dem Awin-Linkbuilder kopiert) |
| fallback_url | text, nullable | Ziel bei deaktiviertem Link |
| notes | text, nullable | z. B. Programm-Status |
| active | boolean, default true | |
| clicks | unsignedBigInteger, default 0 | |
| timestamps | | |

Model `App\Models\AffiliateLink` mit `scopeActive`; keine weitere Logik.

### 2. Route + Controller: `/empfehlung/{key}`

- `Route::get('/empfehlung/{key}', [AffiliateLinkController::class, 'go'])->where('key', '[a-z0-9\-]+')->name('empfehlung.go');` in `routes/web.php` (Nähe `/go`).
- `AffiliateLinkController@go`:
  - Key unbekannt → **404**.
  - `active = false` → 302 auf `fallback_url`, sonst auf die Startseite (kein Klick-Zählen).
  - Sonst: `clicks` inkrementieren; `clickref` anhängen, **falls** `?ref=` vorhanden ist, `^[a-z0-9\-]{1,80}$` matcht **und** der Deeplink noch keinen `clickref`-Parameter enthält (`?`/`&` korrekt wählen); `redirect()->away($ziel, 302)` mit Header `X-Robots-Tag: noindex, nofollow`.
- In Artikeln wird **absolut** verlinkt (`https://kotsch.tech/empfehlung/…`), wie im Artikel-HTML üblich — funktioniert damit host-unabhängig auf news.kotsch.tech und wird vom `/go`-JS-Interceptor sowie von `outboundLinks()` übersprungen (kotsch.tech-Host).
- robots: `Disallow: /empfehlung/` an **beiden** bestehenden Stellen ergänzen — in `public/robots.txt` (gewinnt bei Apache) **und** in der `/robots.txt`-Route in `routes/web.php` (Zeile ~108), damit beide Quellen identisch bleiben.

### 3. Admin: `/admin/affiliate`

Eigene Seite im bestehenden Admin-Panel (Layout, Zugangsschutz und Routen-Muster **1:1 wie /admin/werbung**, Nav-Gruppe „Marketing“): Liste (key, label, merchant, active, clicks, Datum) + Anlegen/Bearbeiten/Aktiv-Toggle/Löschen. Controller `App\Http\Controllers\Admin\AffiliateAdminController`.

### 4. Produktbox + automatische Kennzeichnung

**Snippet-Vorlage** (handgeschriebenes HTML im Artikel-Body; keine Parser-Änderung nötig):

```html
<div class="product-box">
  <span class="product-box__badge">Anzeige</span>
  <h3 class="product-box__title">Bosch GSR 12V-15 Akku-Bohrschrauber</h3>
  <ul>
    <li>Kompakter 12-Volt-Allrounder für Möbelbau und Reparaturen</li>
    <li>2 Akkus + Koffer im Set</li>
  </ul>
  <p class="product-box__price">Preisbereich: ca. 80–110 €</p>
  <a class="product-box__cta"
     href="https://kotsch.tech/empfehlung/bosch-gsr-12v?ref=akkuschrauber-kaufberatung-heimwerker"
     rel="sponsored nofollow noopener" target="_blank">Zum Angebot bei OBI *</a>
</div>
```

Konventionen: `ref` = Artikel-Slug; `key` = sprechender Produkt-Slug; CTA nennt den Händler; `*` am CTA.

**Kennzeichnung (automatisch):**

- `AdditionalWebsiteController::page()` setzt `'hasAffiliate' => str_contains($body, '/empfehlung/')`. (Kein Cache-Problem: `show()` ruft `page()` direkt auf; der Listing-Cache bleibt unverändert.)
- `show.blade.php` rendert bei `hasAffiliate`:
  - **Banner unter dem Hero:** „**Transparenz:** Dieser Artikel enthält Affiliate-Links (Werbung), erkennbar am `*`. Kaufst du darüber ein, erhalten wir eine Provision — am Preis ändert sich für dich nichts.“
  - **Fußnote vor dem Artikel-Abschluss:** „`*` Affiliate-Link (Werbung): Bei einem Kauf über diesen Link erhält kotsch.tech eine Provision vom Händler.“
- CSS für `.product-box` (inkl. Badge, CTA-Button) und das Banner kommt in den `<style>`-Block von `show.blade.php`, im Stil der bestehenden tip-/info-Boxen (hell, Light-only, Markenfarben).

### 5. KotschAds-Anpassung

- Awin-Anzeigen werden im Admin als Typ `affiliate` angelegt; `target_url` = Awin-Deeplink direkt, mit `clickref=kotschads` (Klick-Zählung macht `/api/ads/go/{id}` bereits).
- Alle kotsch-Partials, die `HouseAd::pickFor` nutzen (`news/_ad-sidebar`, `partials/direct-link-ads`, `tools/_ads_banner`): enthält die Auswahl mindestens eine `affiliate`-Anzeige → Block-Label „**Anzeige**“ statt „Empfehlungen“; affiliate-Karten bekommen `rel="sponsored nofollow noopener"`.
- Folgeschritt außerhalb dieses Repos: toolaro-Banner-Komponente wertet das bereits mitgelieferte `type`-Feld der Serve-API aus und zeigt bei `affiliate` ebenfalls „Anzeige“.

### 6. Datenschutz

Neuer Abschnitt „Affiliate-Programme (Awin)“ im Datenschutz-Config für den Website-Bereich:

> kotsch.tech nimmt am Partnerprogramm der AWIN AG (Eichhornstraße 3, 10785 Berlin) teil. Als Affiliate-Links gekennzeichnete Verweise führen über eine Weiterleitung von AWIN zum Shop des Anbieters; erst dort bzw. bei AWIN wird der Klick zu Abrechnungszwecken erfasst (u. a. per Cookie beim Anbieter). Auf kotsch.tech selbst wird dafür kein Tracking-Script eingebunden und kein Cookie gesetzt.

Consent-Banner bleibt unberührt (kein neues Script).

## Content-Workflow (kein Code)

Sobald ein Awin-Programm den Publisher annimmt: 1–3 neue Artikel über die bestehende Pipeline (Ordner `news/{slug}/index.html`) — typischerweise Kaufberatung („X kaufen: worauf achten“), Vergleich („Die besten X 2026“) oder Einzel-Review — mit 1–3 Produktboxen. Zusätzlich passende Bestandsartikel nachrüsten. Neue Themen-Kategorien ggf. in `CATEGORY_GROUPS` ergänzen (bekannter Wartungspunkt).

## Tests (composer test)

1. `/empfehlung/{key}` aktiv → 302 auf Deeplink, `clickref` aus `ref` angehängt; ohne/mit ungültigem `ref` → ohne clickref; vorhandener `clickref` im Deeplink wird nicht dupliziert.
2. Unbekannter Key → 404; deaktiviert → 302 auf `fallback_url` bzw. Startseite, ohne Zählung.
3. Klick-Zähler inkrementiert.
4. Admin-Routen ohne Login geschützt; CRUD funktioniert.
5. Fixture-Artikel mit `/empfehlung/`-Link → Kennzeichnungs-Banner + Fußnote erscheinen; Artikel ohne → nicht.
6. Kotsch-Partial zeigt „Anzeige“, sobald eine `affiliate`-Anzeige ausgespielt wird (und „Empfehlungen“ sonst).

## Nicht-Ziele (YAGNI)

Keine Awin-API/Produktfeeds, keine automatischen Preise/Verfügbarkeiten, kein eigener Deals-Bereich/Subdomain, keine EN-Sonderbehandlung (EN-Artikel sind noindex-Übersetzungen), kein Bot-Filter am Klick-Zähler (Awin rechnet ohnehin serverseitig ab).
