---
title: "Durrani Website — Tasks (Phase 3)"
type: project
tags: [durrani, website, tasks, breakdown, nextjs, payload, kadi]
date: 2026-05-24
status: draft
source: "[[plan]]"
related: "[[plan]], [[spec]], [[constitution]]"
---

# Tasks: Durrani Website (Phase 3)

> Dependency-geordnet aus [[plan]]. `[P]` = parallelisierbar (kein geteilter File-Write). `--- CHECKPOINT ---` = Halt bis Bedingung grün. Effort S/M/L. Repo: KADiCon-Org (Push-Target).

## Phase 0: Setup
- [x] T001 [Repo] `durrani-web` Next.js (App Router) + TS + pnpm scaffold — **DONE** (commit `8954dda`, lokal, nicht gepusht). *Korrektur: separates privates Repo, NICHT KADiCon-Org (Repo-Grenzen-Memory).*
- [x] T002 [P] [Deps] Tailwind (→ DESIGN-Tokens gemappt) + next-intl + ESLint — **DONE** (Prettier offen)
- [x] T003 [P] [CMS] Payload-3-Projekt scaffold — **DONE** (eigenes Repo `durrani-cms`, commit `5b72336`, runtime-verifiziert). Postgres braucht **PostGIS** (`Locations.geo` point). Separates Deployment (Netcup).
- [x] T004 [P] [CI] CI-Baseline — **DONE** (am Content-Vertrag verankert, [[ci-baseline]]). Hart-grün lokal: typecheck/lint/test(18)/build/no-secrets (web), typecheck/lint/build (cms). GH-Actions-Workflows + PostGIS-CI-Service. Playwright/axe vorbereitet (CI-Browser), Lighthouse = Preview-Gate. Anker = CMS-Contract-Test.
- [ ] T005 [Env] ENV-/Secret-Schema (Payload-URL/Token, KADi-Server-Keys, Umami) — `@t3-oss/env` o.ä., Keys server-only (S)
--- CHECKPOINT: beide Apps booten lokal, CI grün auf leerem Build ---

## Phase 1: Foundational (blockt alle Stories)
- [x] T010 [Data] Payload-Collections: Locations, MenuCategories, MenuItems, Pages, Events, Catering, FAQ, Media, Settings-Global, Users — **localized DE/EN, location-scoped** — **DONE & runtime-verifiziert** (i18n/Scoping/Drafts/Relationships live in `durrani-cms`). Reviews bleibt Roadmap.
- [x] T011 [P] [i18n] next-intl Routing `/[locale]/…`, DE primär + EN — **DONE** (routing/request/navigation/middleware + messages DE/EN; `/`→307→`/de` verifiziert). hreflang-Helper noch offen.
- [ ] T012 [P] [Design-Tokens] Design-System-Basis aus DESIGN.md (Farben/Typo/Spacing/Komponenten) — Mobile-First (M) [depends T101]
- [ ] T013 [Lib] Payload-Client (Fetch build/ISR) + Revalidate-Webhook-Route `/api/revalidate` (signed) (M) [depends T010]
- [ ] T014 [P] [Lib] JSON-LD-Schema-Builder (Restaurant/LocalBusiness/Menu/FAQ/Event/Review/Breadcrumb) (M)
- [ ] T015 [P] [Security] Security-Header (CSP/HSTS/…), Cloudflare-Pages-Config, Turnstile-Setup, **CMS-Admin nicht öffentlich + 2FA** (FR-031) (M)
--- CHECKPOINT: Collections im CMS editierbar, i18n-Routing live, Design-Tokens stehen, Schema-Builder unit-getestet ---

## KADi-Discovery & Integration (parallel zu Phase 1, blockt Reservierungs-Story)
- [ ] T020 [KADi] Repo `KADiCon-UG/kadicon` lesen → API-Endpunkte + Auth-Modell + tenant-Scoping identifizieren, dokumentieren (M)
- [ ] T021 [KADi] Contract definieren: Availability(date,party)→slots, Booking(create)→id/status, Status-Webhook, Rate-Limits/Bot-Schutz (M) [depends T020]
- [ ] T022 [KADi] **Mock-Adapter** (gleiches Interface) → Frontend baubar bevor echte API stabil (M) [depends T021]
- [ ] T023 [KADi] `kadi-client` BFF-Modul (server-only, Keys serverseitig), echte API hinter Adapter (M) [depends T022]
--- CHECKPOINT: Mock-Adapter liefert Verfügbarkeit + nimmt Buchung, Contract dokumentiert ---

## Phase 2: P1 — Gast-Kernreise (MVP) 🎯
- [x] T030 [Page] Startseite — **DONE** (commit `2fd1bb3`): Conversion-Journey statt Sammelseite — führt zu Reservieren/Bestellen/Speisekarte/Anfahrt. Hero (ein H1 `heroHeadline` + emotionale Subline, primär Reservieren / sekundär Bestellen / tertiär Speisekarte, CLS-sicherer Bildslot), `SiteStatus` (geöffnet/geschlossen + nächste Öffnungszeit aus CMS-Location, client-berechnet nach Mount → keine Hydration-Mismatch, Link Kontakt), `StickyCta` (mobile-only fixed Bar Reservieren/Bestellen/Anrufen, `sm:hidden`, fixed → 0 Layout-Shift), Brand-Story, Speisekarten-Preview (echte CMS-Gerichte), 3 Journey-Teaser, Trust **ohne Fake-Rating**. LocalBusiness/Restaurant-JSON-LD (acceptsReservations/hasMenu/OrderAction nur bei orderUrl), canonical/hreflang absolut. `nextOpening()` + Tests, home-i18n DE/EN-Parität. **Live gegen echtes CMS verifiziert**: 0 interne Felder (`kadiTenantId`/`_status`/IDs), 0 Third-Party im Initial-Render, genau 1 H1, JSON-LD valide. Gates grün: typecheck/lint/test(79)/build/budget(Home 111KB≤120)/secret-scan.
- [x] T031 [P] [Page] Speisekarte aus CMS — **DONE** (commit `088b853`): `getMenu()` über Contract-Layer (nur Public-DTOs), Kategorien/Gerichte/Preis/Diät-Flags/Allergene, Menu+Restaurant-JSON-LD, mobile-first, States (skeleton/empty/CMS-down/section-empty), canonical/hreflang/OG. 23 Tests grün. Build SSGt mit §9-Fallback (CMS beim Build nicht laufend); Real-Daten-Pfad via menu.test.ts verifiziert. Bundle-Budget-Gate aktiv. **Live-End-to-End gegen echtes CMS verifiziert** (commit `f28f758`/cms `7c62e16`): `/de`+`/en` rendern echte Daten (kein Fallback), Public-DTOs only (`kadiTenantId`/`_status`/IDs NICHT im HTML/JSON-LD), JSON-LD Menu real, canonical/hreflang absolut (metadataBase-Fix), keine Sprach-Mischung. Offen aus KAR-618: Menü-Bilder (`next/image`) + Restaurant-JSON-LD auf Location-Seiten.
- [x] T032 [Reservation] Reservierungs-UI über Mock + BFF — **DONE** (commit `4a76830`): Datum/Personen/Slot-Chips (alle States), Buchungsformular + Success/Error, gehärtete BFF (Zod/Honeypot/Rate-Limit/Idempotency/generische Fehler), Cancel-ready (Booking-Ref + v1-Storno-Hinweis), 5 Analytics-Events, server-resolved labels (Route 105KB<120). 48 Tests grün. Real-KADi via Env-Switch (switch-test grün), Live-Verifikation wenn echte API steht. **Speisekarte-Follow-ups → KAR-618** (Live-CMS-E2E + Menü-Bilder).
- [x] T033 [P] [Order] Bestellen/GloriaFood — **DONE** (commit `ef65462`): `/[locale]/bestellen` Branded-Erklärung + CTA, primärer stabiler Order-Link + optionales click-to-load Embed (Script nur bei Klick, Timeout/Error→Fallback), **0 Third-Party im Initial-Render** (live bewiesen), CSP-Domains dokumentiert (keine Wildcards), 5 Analytics-Events, OrderAction-JSON-LD nur bei konfigurierter URL. DE/EN, canonical absolut. 75 Tests grün. **Offen:** GloriaFood-Account/URL via Env konfigurieren (`NEXT_PUBLIC_GLORIAFOOD_*`).
- [x] T034 [P] [Page] Kontakt/Standort — **DONE** (commit `bbd040e`): Adresse/Tel/Mail + Öffnungszeiten aus CMS-DTO (Ruhetag + Live-Status) + click-to-load Map (kein Third-Party/Key im First-Render) + Kontaktformular (BFF: Zod/Honeypot/Rate-Limit/generische Fehler) + LocalBusiness-JSON-LD (hasMenu/acceptsReservations/sameAs, **kein Fake-Rating**). DE/EN. 71 Tests grün. **Live gegen echtes CMS verifiziert**: echte Öffnungszeiten, 0 iframes initial, 0 Leaks, canonical absolut.
- [x] T035 [P] [Test] E2E Playwright + axe — **DONE** (commit `f5f30c2`): erster echter Lauf. Flows Startseite/Speisekarte/Reservierung/Kontakt/Bestellen/Rechtsseiten in **DE+EN** auf Chromium Desktop + Mobile (Pixel 5). axe (wcag2a/2aa) für Home/Speisekarte/Reservierung/Kontakt/Bestellen/Datenschutz. **87 passed, 5 skipped, 0 failed; axe 0 Violations** (auch keine moderate/minor) über 24 Scans; keine Console-Errors/Hydration/Overflow. **Gefundene+gefixte Bugs:** (1) WCAG-AA-Kontrast serious (Brass-Accent als CTA-Fill/Text + foreground-subtle <3:1) → Accent-Rampe `#8a6415/#73510f/#5e430d` + subtle `#6f675c`; (2) Sticky Mobile-CTA verdeckte Footer → `body pb-20 sm:pb-0`; (3) `upgrade-insecure-requests` no-op in Report-Only → entfernt; (4) click-to-load Maps Report-Only-Framing-Violation → `frame-src 'self' https://www.google.com`. Test-Korrekturen: Honeypot=Silent-Accept (API-Test 200 vs 201, Discard unit-getestet), Root-Redirect folgt Accept-Language, veraltete Smoke-Assertion ersetzt. **WebKit/Safari nicht abgedeckt** (Sandbox-System-Libs fehlen). Gates: typecheck/lint/test(83)/build/budget(Home 112KB≤120)/secret-scan grün. Offen aus KAR-618: Menü-Bilder/Image-Pipeline.
- [x] T036 [P] [Legal] Rechtsseiten — **DONE** (commit `f8f976c`): `/[locale]/{impressum,datenschutz,cookies}`, DE vollständig, EN strukturiert + Nicht-bindend-Hinweis (verbindlich = DE), keine Fantasie-Formulierungen, Platzhalter bewusst in `[Klammern]`. Inhalte über **pflegbares Legal-Content-DTO** (`lib/legal/`): CMS-Location für Anbieter/Kontakt + zentrale `lib/legal/config.ts` für Hosting/Auftragsverarbeiter/Aufsichtsbehörde; Prosa in `messages/*.json`, keine harten Texte in JSX. Abgedeckt: Anbieter/Verantwortlicher/USt/Register, Hosting, Server-Logs, Kontaktformular, Reservierung, GloriaFood, Google Maps, Umami (cookieless EU), Rechtsgrundlagen, Speicherdauer, Betroffenenrechte (Art. 15-21/7/77), Aufsichtsbehörde Hessen, Social-Links, Sicherheit (Art. 32). Cookie-Strategie: keine einwilligungspflichtigen Cookies, kein Local-Storage-Tracking (im Code verifiziert: 0 Treffer), Third-Party erst nach Klick, kein Banner, Consent-Layer als Zukunftspfad. Globaler **Footer** (server-rendered, 0 Client-JS) mit Legal-Links auf jeder Route. `buildMetadata` um `robots` erweitert → Legal bewusst auf `index,follow`. **Live gegen echtes CMS verifiziert**: echte Anbieterdaten aus CMS, 0 interne Felder, 0 Third-Party-Scripts, genau 1 H1/Seite, canonical/hreflang absolut, EN-Hinweis nur in EN. Gates grün: typecheck/lint/test(83)/build/budget(Legal ≤107KB, Home 112KB ≤120)/secret-scan. **Hinweis: kein Ersatz für anwaltliche Prüfung** — final juristisch prüfen lassen (Platzhalter füllen: Verantwortlicher, USt-IdNr., ggf. Register).
- [x] T037 [Media] Image-Pipeline (KAR-618 Teil 2) — **DONE** (commit `acb3086`): produktionsnah, **ohne Fake-/Stock-Bilder zu erzwingen** — fehlt ein Foto, rendert ein ruhiger on-brand Slot (CLS-sicher), nie generische Stockware. Media-DTO um `blurDataURL`+`dominantColor` (LQIP) erweitert, Alt bleibt Pflicht; `toMedia` whitelistet + strippt interne Felder (Test). **`lib/image/image-props.ts`** = pure, node-testbare Logik (`focalToObjectPosition`, `resolveAlt` decorative→'', `buildImageProps` priority→eager/sonst lazy, blur nur bei blurDataURL, `hasRenderableImage`). **`components/ui/DurraniImage`** = zentrale Server-Komponente um next/image (responsive sizes, Hero=priority/LCP, Menu/Teaser=lazy, CLS-sichere Aspect-Ratios, focal-point-Crop, Blur-Placeholder, eleganter Fallback-Slot). Hero nutzt DurraniImage (priority, 4/3 mobil → 16/9 desktop, Slot bis echtes Foto, kein Slider). Speisekarte: Gerichtbild optional als lazy Thumbnail, ohne Bild sauberes Text-only (kein kaputtes Layout). `next.config` images.remotePatterns **ohne Wildcards** (CMS-Media-Host aus `CMS_API_URL` + `CMS_IMAGE_HOSTNAME` + Cloudflare `imagedelivery.net`), avif/webp. JSON-LD `MenuItem.image` nur wenn vorhanden. i18n `dietaryGroup` (fixt hardcoded deutsches aria-label). Tests: image-props + toMedia (**96 unit**), E2E Hero-Slot + no-broken-images (DE+EN, Desktop+Mobile), **axe weiter 0 Violations**. Gates: typecheck/lint/test(96)/E2E(87/0)/build/budget(**Home 117KB ≤120**, next/image +5KB)/secret-scan grün. **Echte Fotos = separater Content-/Shooting-Block** (DurraniImage braucht dann keine Änderung). **Watch:** Home 117KB nah an 120 — bei Wachstum beobachten.
- [x] T038 [Contact] Mailversand v1 (KAR-619) — **DONE** (commit `6947c39`): Kontaktformular sendet real, server-side, über **Provider-Abstraktion** (`lib/server/mail/`): `MailProvider`-Interface + **Resend** (fetch, kein SDK, primär) + **SMTP** (prepared/stub, braucht nodemailer) + **Dev** (in-memory Outbox, default; Misconfig → fail-soft „logged not sent"). Auswahl via `MAIL_PROVIDER` (resend|smtp|dev), API-Key strikt server-only. `compose`: Admin-Mail + opt-in Auto-Reply, DE/EN, Felder Name/E-Mail/Telefon/Anlass/Nachricht/Sprache/Zeitstempel/Seite, replyTo=Gast; Locale+Seite aus Referer (kein Form-Umbau). BFF `contact-handler`: Rate-Limit + Honeypot (silent, **keine Mail**) + Zod + Idempotency + **fail-soft Delivery** (Versandfehler blockiert User nie). Route = dünner Wrapper. **Verifiziert:** 111 unit + Live-E2E (valid→201, honeypot→200, invalid→400, genau 1 Dev-Mail mit DE-Betreff aus Referer); Client-Bundle 0 Mail-Secrets/`api.resend.com`/`nodemailer` (grep). Gates: typecheck/lint/test(111)/build/budget(Home 117KB)/secret-scan grün. **Launch:** echte `RESEND_API_KEY`/`CONTACT_*`-Env setzen (KAR-611).
--- CHECKPOINT: P1 mobil end-to-end nutzbar, Lighthouse ≥95, Schema 0 Fehler, Rechtsseiten live ---

## Phase 3: P2 — Events, Feiern & Catering
- [ ] T040 [Page] Event-/Feiern-Seite(n) aus Payload + Event-Schema (M) [depends T010,T014]
- [ ] T041 [P] [Catering] Catering-Sektion (eigene Collection, Channel-ausbaubar) (M) [depends T010]
- [ ] T042 [Form] Anfrageformular (DSGVO, Turnstile, Rate-Limit, Server-Action→Mail) (M) [depends T015]
--- CHECKPOINT: Event/Catering editierbar, Formular spam-geschützt + DSGVO-konform ---

## Phase 4: P3 — Marke, Story, Vertrauen
- [ ] T050 [P] [Page] About/Story (M) [depends T012]
- [ ] T051 [P] [Page] Galerie (optimierte Bilder, AVIF/WebP, kein CLS) (M) [depends T012]
- [ ] T052 [P] [Reviews] Bewertungen (CMS oder Google) + Review-Schema (M) [depends T014]

## Phase 5: Polish & Launch
- [ ] T060 [SEO] Sitemap + robots, OG/Twitter-Cards je Seite, alle Schema-Typen final + Rich-Results-validiert (M)
- [ ] T061 [P] [Local-SEO] Google-Business-Profile NAP-Sync, lokale Signale, Maps-Strategie (M)
- [ ] T062 [P] [A11y] axe-Pass WCAG 2.2 AA, Keyboard/Screenreader, prefers-reduced-motion (M)
- [ ] T063 [Perf] Lighthouse-Budget final (LCP<2s, INP<200ms, CLS<0.1, ≥95), Image/Font-Tuning (M)
- [ ] T064 [Analytics] Umami self-hosted (Netcup) einbinden, cookieless (S)
- [ ] T065 [Migration] 301-Redirects alte WP-URLs → neue Routen; Inhalte (Texte neu, Fotos übernommen) migriert (M)
- [ ] T066 [Launch] DNS-Cutover, alte WP sichern+abschalten, Search-Console Sitemap, Monitoring (M)
--- CHECKPOINT: alle Optimization-Targets aus spec.md erfüllt (verify-and-fix Phase 4) ---

## Design-Phase (vorgelagert, feeds T012)
- [ ] T101 [Design] DESIGN.md: Brand-Sprache-Vorschlag (Logo behalten; Farben/Typo/Bildsprache/Komponenten/Premium-Look) → **separates Review-Gate mit Kais** (L)

## Roadmap-Parkplatz (NICHT v1, Architektur offenhalten)
Kundenkonto · Loyalty · Waitlist · Eventbuchungen · WhatsApp · AI-Concierge · Empfehlungen · Seasonal-Menus · KADi-Menu-Sync.

## Build-Fortschritt (2026-05-24, Session 0956fb5e)
**Meilenstein „erste lauffähige lokale Version" erreicht** — Commit `8954dda` (lokal, NICHT gepusht).
- ✅ T001 (Scaffold), ✅ T002 (Deps), ✅ T011 (i18n DE/EN)
- 🟡 Teilweise: T012 (Design-Tokens in Tailwind gemappt, Komponenten folgen) · T015 (Security-Header + CSP-Report-Only live; Turnstile/CMS-2FA/CSP-enforce offen) · T022/T023 (BFF-Routes `availability` GET + `reservation` POST gegen Mock-Adapter; realer Adapter via Env-Flip)
- **Verifiziert:** `next build` clean (5/5 SSG, First-Load-JS ~101 kB < 120 KB-Gate), `/de`+`/en` SSG, 5 Security-Header + CSP-Report-Only aktiv, BFF-Mock liefert Slots + `{confirmed:true}`.
- **Offen als nächste Blöcke:** T003/T010 Payload-Runtime-Verifizierung (braucht Postgres) · T004 CI-Baseline (Lighthouse/axe/Playwright) · hreflang · Prettier · CSP-enforce mit Nonce.

## Linear-Sync
Nach Gate C: Option B (Parent KAR-596 + Sub-Issues je Phase/Cluster) — Solo-Work, übersichtlich.
