Skip to main content

Website with v0 and Cockpit — steps only

New here? Start with the checklist

★ Start hereWebsite with v0 (shortcut) (8 steps). This page is for copying Instructions A–J — then return here.

Read first? The A–Z getting-started guide for editorial (plain language): v0 website A–Z (for editorial).

This is not about deep tech — only: what to click when and what to copy where.

Goal Cockpit + v0: Editorial teams build high-performance, appealing, modern center websites in natural language using cockpitOS data. The dashboard provides stable public data (branding, content, hints under apiHints). In v0 you design any layouts — grids, carousels, landing pages — and bind endpoints via fetch (mostly Server Components). After step 2 (public-visitor-surface), ready URL paths are available in data.apiHints (including Hot Picks, current bundle, DOOH, …).

New in v2 (vs. older instruction copies):

  • Legal: Imprint/privacy as mandatory pages (Part D), GDPR checkbox on contact form (Part A), hardened cookie consent + next/font instead of Google CDN (Part C), go-live only when legal pages are filled (Part J)
  • Accessibility (BFSG): dedicated mandatory block in Part E, plan list alternative (Part F), kiosk idle reset + terminal a11y (Part H), mobile a11y (Part I)
  • SEO: robots/sitemap/metadata/JSON-LD from first deploy (Part E), preview noindex switch + canonical on custom domain (Part J), per domain in multi-center (Part G)
  • Design / anti-slop: design-DNA process in plain language (editorial picks from 2–3 understandable directions), variation from center traits not habit, data-driven homepage structure, exactly 2 signature details per center, boldness rule, icon rule (no emojis in UI), font blacklist + layout archetypes, favicon/OG image required (Part C), branded 404/error (Part B), Impeccable CLI (npx impeccable detect) before handoff (Part C/J)
  • Quality: go-live checklist with form test, Lighthouse targets, link preview test, and Impeccable with zero findings (Part J)
  • Shops: standalone shop vs chain/branch (Part D) — do not auto-create “chain with 1 branch” for local tenants

v0 limits (product): max. 10 Custom Instructions, each max. 5000 characters (text inside the gray box only, no Markdown). Count before saving — too long → Docusaurus/chat only, not as Instruction. Character counts are below each box.

10 slots (alphabetically in v0 — A … J):

SlotInstructionWhen
1Cockpit Aalways (routes + forms)
2Cockpit Balways (JSON/API + code structure)
3Cockpit Calways (media, chat, design-DNA)
4Cockpit Dalways (workflow + mandatory pages)
5Cockpit Ealways (performance, a11y, SEO, resilience)
6Cockpit Fcenter plan / wayfinding
7Cockpit Gmulti-center only (otherwise free)
8Cockpit Hsignage/kiosk (with I)
9Cockpit Icompanion (with H)
10Cockpit Jafter first Vercel deploy + go-live

Not as Instruction: Part F2 (3D fine-tuning) → paste into v0 chat when needed (saves a slot with full set A–J).

Core website: A + B + C + D + E (+ J after go-live) = 6 Instructions; F/G/H/I per project.

Another path? New design as ZIP into CockpitTemplates.


1. Brief preparation

  • Open v0 (v0.dev).
  • Note your center slug in Cockpit (short name).

2. Create Instructions in v0 (core + optional)

cockpitOS is the only source of truth

Shops, news, events, offers — this data belongs in cockpitOS, not as dummy code in v0. Part D keeps the AI assistant on the right workflow.

  1. In v0: open Instructions.
  2. Create A → J alphabetically (Cockpit ACockpit J). Copy each box below → paste → save.
  3. Cockpit D is named Cockpit Workflow in v0 (mandatory).
  4. Cockpit J only after first Vercel deploy (Vercel env Team Shared vs. project).
  5. Optional: F (plan), G (multi-center), H+I (signage). F2 in chat only, not an 11th Instruction.
  6. After v2 update: replace Parts A–J in existing v0 projects (do not only append) — especially A, C, D, E, J.

3. Enter center (Part A and G if needed)

Single center: In Part A, replace HIER_SLUG_EINTRAGEN with your center slug (for v0 preview). API base is https://dashboard.cockpit-os.de (staging only if IT specifies another base — adjust first line in Part A).

Multi-center (one layout): Enable Part G. In Part A, keep slug as preview example or any reference center — live uses domain (by-domain), not slug in A. Tell v0 at start: “Multi-center layout — same design for all centers with the same websiteTemplate; resolve center at runtime via host.”


4. Describe the website in v0

  • Drop in screenshots or write e.g. Bright homepage, large images, shops as cards.
  • Multi-center: One layout for all centers with the same websiteTemplate; content per domain from Cockpit; no fixed centerId.
  • Center plan 3D: Instruction F; if stuck, F2 in chat“Build 2D/3D center plan per Part F2 — mapSvg with SVGLoader, not mapImage.”
  • Signage / kiosk + NOW!: Instructions H + I; in chat: “Kiosk terminal style (Part H): plan 75%+, compact header, floating pills, slim toolbar — plus companion (Part I).”
  • If chat wanted: Chat bottom-right, consent dialog first.

5. Hand off to IT

  • Pass v0 output (ZIP or link).
  • Include two sentences:
    1. Please set NEXT_PUBLIC_DASHBOARD_URL on Vercel — same URL as Part A.
    2. Please wire deploy hook to Cockpit (A–Z guide section F3 + Instruction Part J).

Hosting: Single center → often own repo + Vercel project (F2). Multi-center (shared layout)one repo + one Vercel project with all center domains (F2b).


IDs: center vs. floor (common mistake)

The Cockpit URL …/dashboard/centerplaene/{UUID}/edit contains the floor ID (floorId), not the center ID. For the public API (…/wayfinding/floors?centerId=, …/shops, …) always use centerId from GET …/by-slug/{slug}id. Otherwise floors may be empty or without mapLocations.

If something is missing

  • No shops / empty: Slug correct? Website active for center in Cockpit?
  • No images: Ask IT.
  • No chat: Chatbot enabled in Cockpit?
  • DOOH empty / playlist null: Playlist active and in date range? Slug in URL matches DOOH? Center website must be active for …/dooh/public/….
  • SVG shop click / shop-56: Link only via GET …/wayfinding/floorsmapLocations (svgId + shop), not the shops list alone.
  • Hybrid plan (image in plan): Plan is a nice image with click areas on top. Tip: upload large, sharp image (long edge ~2000–3000 px or more). More: Hybrid center plan · AI master prompt.
  • Wayfinding: Either POST …/routing with startWaypointId (from …/entrances) and endLocationId (= mapLocations[].id) — or compute path from mapSvg/#routes; do not show #routes as visible mall lines.
  • No mapLocations / empty floors: Is centerId really the center UUID from by-slug? Not the UUID from center plan edit URL (floorId).
  • mapLocations empty but _count.mapLocations > 0: Mappings exist in DB; public API only returns active shops/locations/services/offices.
  • Only one floor / upper floor missing in v0: API often returns both — MCP chat truncates large mapSvg. First omitMapSvg=true or cockpit_center_context with floors_summary; then floorNumber=1 or server fetch.
  • 3D broken / v0 wants 2D only: Check floors[].mapSvg (string with <svg and shop-?). 3D from mapSvg with SVGLoader — not mapImage. Re-paste Part F (“typical v0 errors”).
  • mapLocations empty despite ?includeMapLocations=…: Route only uses centerId — extra params ignored.

v0 character limit: max. 5000 characters per Instruction (inner text of boxes “Part A” to “Part J”). Count before saving.

SEO/GEO without a new Instruction slot: MCP cockpit_v0_geo_standards. Mandatory: implement applicable p0Blockers in code. ≥80% GEO only with editorial parking/transit facts — otherwise ~65–78% realistic.

WhereLine to add
Part J (go-live)Before go-live: cockpit_v0_geo_standards — implement all p0Blockers, check selfVerification.
Part D (fallback)- GEO mandatory: cockpit_v0_geo_standards → p0Blockers in code (JSON-LD, sitemap, shop links)

After MCP deploy, cockpit_center_project_init is often enough for v0 — step 7 points to the same tool.

Part A — paste this box into Instruction „Cockpit A“

cockpitOS — Teil A (Routen & Ablauf)
API-Basis: https://dashboard.cockpit-os.de (HTTPS, kein Slash am Ende). Slug-Platzhalter: HIER_SLUG_EINTRAGEN

VERCEL ENV (Redaktion: nichts manuell; v0 scaffoldet, IT einmal Team Shared):
- Team Shared (IT einmal, jedes Projekt verlinken + Redeploy): COCKPIT_DASHBOARD_URL, REVALIDATION_SECRET, COCKPIT_FRONTEND_CHANNEL=website. v0 erzeugt KEIN neues Secret wenn Shared verlinkt.
- Nur dieses Projekt (v0 setzt): NEXT_PUBLIC_DASHBOARD_URL (= API-Basis oben), COCKPIT_REGISTER_TOKEN (frt_…), COCKPIT_CENTER_SLUG (= HIER_SLUG_EINTRAGEN). REVALIDATION_SECRET + Register-Route: Teil J.
- Gleicher Key auf Projekt- und Team-Ebene: Projekt-Env gewinnt.

Next.js App Router, öffentliche Site: Server Components wo möglich; alle URLs mit dieser Basis.

WARNUNG UUID: …/dashboard/centerplaene/XXXX/edit → floorId, NICHT centerId. Öffentlich immer centerId aus GET …/centers/by-slug/{slug} → Feld id.

Ablauf:
1) GET https://dashboard.cockpit-os.de/api/centers/by-slug/HIER_SLUG_EINTRAGEN → centerId (= id). Alternativ …/api/centers/by-domain?domain={hostname}
2) GET …/api/centers/{centerId}/public-visitor-surface (Branding, Öffnungszeiten, templatePublicContent, Chatbot-UI, Tracking, apiHints)
3) GET …/api/centers/by-slug/HIER_SLUG_EINTRAGEN/theme-config

4) Inhalte ({centerId} einsetzen; … = gleiche Basis):
- Shops …/api/centers/{centerId}/shops?publicWebsite=true&status=Aktiv&limit=5000&offset=0 — optional includeWayfindingLinkages=true (svgId-Hints)
- Kategorien …/website-categories?status=Aktiv&includeGlobal=true&minShopCount=1
- Inhaltskategorien …/content-categories?centerId=CENTER_ID&type=news&includeGlobal=true (News/Events/Angebote, nicht Shop-Kategorien)
- Category-Themes …/category-themes-for-website
- News …/news?published=true (Zeitfenster wie Bundle; Filter contentCategorySlug=…; Stele: forSignage=true; forToGo=true optional; ohne published nur Legacy)
- Bundle …/aktuelles-bundle (News+Events+Offers+Jobs zusammen)
- Hot Picks …/hotpicks (≠ homepage-tiles) | Homepage-Kacheln …/homepage-tiles
- Baustellen …/news?constructionDiary=true
- Einzel-Angebot …/offers?offerId={uuid|slug}
- Events …/events; Offers/Jobs …/offers | …/jobs (Publish-Fenster wie Bundle)
- Services …/services?publicWebsite=true&limit=5000
- Büros …/api/offices?centerId={centerId}&status=Aktiv
- CMS …/page-content (pageType filtern; auch impressum/datenschutz: Teil D)
- Template-Reiter (Hero, Footer, Optionen): public-visitor-surface → data.templatePublicContent.content — NICHT website-config (401)
- Umami nur über Schritt 2 → data.tracking (kein /public-config erfinden)
- Centerplan …/api/wayfinding/centerplan?centerId={centerId}
- Floors …/api/wayfinding/floors?centerId={centerId} (mapLocations dabei; optional omitMapSvg=true, floorNumber=0|1|…)
- Plan: svgId/Treffer über floors[].mapLocations; Shop.floor oft leer
- entrances …/entrances → id = startWaypointId
- Routing POST …/api/wayfinding/routing — endLocationId = mapLocations[].id (UUID), nie svgId
- mapSvg #routes = Netz für Pfadberechnung, nie Original-Linien zeigen
- Chat POST …/api/ai/visitor-chatbot (centerId im Body)
- DOOH: relative Pfade in data.apiHints (doohPublic…)

5) Kontakt- & Vermietungsformular (E-Mail/Resend nur im Dashboard):
- Layout/Formular in v0; Versand NICHT direkt ans Dashboard vom Browser (kein CORS).
- Pflicht: eigene Next.js-Routen: app/api/kontakt/route.ts und ggf. app/api/vermietung/route.ts.
- Frontend: fetch("/api/kontakt", { method:"POST", body: JSON.stringify({ centerId, name, email, message, phone?, subject?, formKey?, recipientEmail? }) }) — centerId = UUID aus Schritt 1.
- Server-Route proxyt serverseitig: POST …/api/centers/{centerId}/send-contact-inquiry bzw. …/send-rental-inquiry (zusätzlich company?, spaceInterest?).
- Pflichtfelder: name, email, message. Antwort: { success:true, recipient? } oder { success:false, error }.
- DSGVO PFLICHT: Checkbox „Ich habe die Datenschutzerklärung gelesen" mit Link auf /datenschutz, NICHT vorausgewählt; ohne Haken kein Submit. Honeypot-Feld gegen Spam. Erfolg/Fehler als Text mit aria-live melden, nie nur über Farbe.
- Anderer Empfänger als Standard: NICHT beliebig im Body; zuerst im Cockpit freigeben: templateContent.formRecipients.{formKey} (MCP update_center_website_config), dann Body formKey. recipientEmail nur wenn bereits in Cockpit-Allowlist. customRecipient = Alias.
- Kein RESEND_API_KEY in v0/Vercel. Standard-Empfänger: Center-Stammdaten email oder Website-Reiter Kontakt.

Characters: 4486 / 5000

Part B — paste this box into Instruction „Cockpit B“

cockpitOS — Teil B (Antworten, Öffnungszeiten, Code-Struktur)

JSON-Formen:
- GET by-slug: flaches Objekt OHNE success/data — id, name, slug, logo, logoUrl (oft gleich), baseColor, secondaryColor.
- GET public-visitor-surface: { success:true, data: { center, seo, chatbot, templatePublicContent, v0Integration, centerplan, features, apiHints, … } }. Branding + Center-Öffnungszeiten: data.center.openingHours, openingHoursNote, specialDays. Template (Hero/Footer): data.templatePublicContent.content (rootKey z.B. rgw). Chat-UI: data.chatbot. apiHints: pageContentGet, homepageTilesGet, aktuellesBundleGet, hotPicksGet, …
- Hot Picks: GET …/hotpicks → { success, hotPicks: [ { id, contentType, type, title, description, image, priority, position, startDate, endDate, channels, content, … } ] }. Hot Pick = Cockpit-Highlight mit Verweis auf Shop/Event/News/Angebot; Layout (Karten, Slider, Liste) = Frontend. Nicht verwechseln: …/homepage-tiles = Startseiten-Kacheln, andere Struktur/Zweck.
- DOOH public: GET …/dooh/public/playlists → { success, playlists }. GET …/playlist?slug= → { success, playlist | null } (null = inaktiv/außerhalb Gültigkeit). Items: type VIDEO|IMAGE|SLIDESHOW|IFRAME, payload URL(s), durationSeconds (0 bei Video oft volle Länge im Client).
- GET shops: { success, data: […], total, pagination }. Medien: logo, coverImage. floor/location oft null — Karte: Floors-API oder ?includeWayfindingLinkages=true → wayfindingLinkages[] (svgId, floorName, mapLocationId).
- GET wayfinding/floors?centerId=: { success, floors: […] }. centerId = Center-UUID (by-slug → id), NICHT UUID aus …/centerplaene/…/edit (floorId). Pro Etage: mapSvg (String!), mapImage (CDN), shopViewBoxes (optional JSON svgId→viewBox), mapLocations[], optional shopSvgIdsWithOffers. mapLocations: svgId (z.B. shop-56), type, shop/shopLocation/service/office mit UUID + Anzeigedaten. SVG-Klick: mapLocation per svgId → shop.id für Detail-Link. Fehlendes svgId/Shop = Cockpit-Zuordnung fehlt, nicht per Namen erraten.
- GET entrances: { success, entrances: [ { id, floorId, position, floor, … } ] } — id = startWaypointId für POST routing.
- POST …/wayfinding/routing: Route über DB-Waypoints; Ziel endLocationId = mapLocations[].id (UUID), nicht svgId. Ohne Waypoints im Cockpit leer → SVG-#routes oder „Weg nicht verfügbar".
- SVG #routes: Datenlayer, Original-Linien nie 1:1 zeigen; Besucher sehen nur berechnete Route als Overlay. Referenz: @mall-os/wayfinding (parseSvgRoutes, findPathThroughNetwork).
- Andere Reads: oft { success, data } — bei Unsicherheit Struktur prüfen.

Öffnungszeiten (Center — primäre Quelle):
- GET public-visitor-surface → data.center.openingHours (nicht by-slug, nicht page-content für Header/Widget).
- Zusätzlich: data.center.openingHoursNote, data.center.specialDays (Feiertage/Sonderzeiten).
- openingHours: null | Freitext-String | Objekt. Dashboard-Format bevorzugt:
- regularHours: { monday: { open, close }, … sunday } — ggf. dayOpen24Hours, open24Hours
- flach: weekdays, saturday, sunday (Strings)
- Legacy: monday…sunday mit { hours: "10:00 - 20:00" } oder { open: "closed", close: "closed" }
- specialDays: [ { date: "YYYY-MM-DD", label?, hours?: { open, close } | null } ] — DB-Spalte vor openingHours.specialDays.
- Shops/Services: eigenes openingHours in GET …/shops bzw. …/services.
- UI: nie Roh-JSON — Tabelle/Liste, deutsche Wochentage, „Heute geöffnet bis …" wo möglich. String: JSON.parse try/catch, sonst Fallback.
- page-content (pageType oeffnungszeiten) nur redaktionelle CMS-Seite, kein Ersatz für Center-Stammdaten.

Chatbot-Fehler: 429 = Cockpit-Rate-Limit; 502 openai_rate_limit = OpenAI-Quota (IT); 403 = Consent fehlt; 502 openai_error (400) = oft falsches Modell im Webseiten-Tab → GPT-4o mini.

Öffentliche GETs: CORS *. POST page-content ohne Auth nicht von fremden Sites. Chatbot-/Routing-POST kann 429 — Nutzer freundlich informieren.
Fehler: if (!res.ok) → leeres Array/null/Defaults — nie Error-Objekt in JSX (React #418). Kein GET website-config/agencyos ohne Auth.

Fehlerseiten (Pflicht): app/not-found.tsx + error.tsx im Center-Branding (Logo, Farben, Link zu Start/Shops/Öffnungszeiten) — keine Next/Vercel-Defaults. loading.tsx mit Skeletons statt Spinner-Vollbild.

Code-Struktur (Pflicht):
- App Router: schlanke page.tsx; Daten in Server Components; use client nur kleine UI-Blöcke.
- components/ nach Feature (shops, layout, wayfinding …), ggf. ui/. Keine 1000+ Zeilen/Datei — Logik, View, API trennen.
- lib/api/cockpit (eine Basis-URL), eine Funktion/Ressource + Typen (lib/types/…); Fehler zentral behandeln.
- lib/utils/, lib/media für CDN (Teil C); Konstanten zentral. layout + Header/Footer getrennt.
- Ziel: wartbar, auffindbare Fehler, kein Monolith.

Characters: 4731 / 5000

Part C — paste this box into Instruction „Cockpit C“

cockpitOS — Teil C (Medien, Chatbot, Design-DNA)

Bilder/Medien: relative API-Pfade (/uploads/…, centers/…, global/…) mit CDN-Basis prefixen: https://cockpitos.b-cdn.net. Env optional: NEXT_PUBLIC_BUNNY_CDN_URL.

Besucher-Chatbot:
- Zuerst public-visitor-surface → data.chatbot. enabled=false → kein Widget.
- Consent-Texte: data.visitorPrivacy (chatbotConsentTitle/-Description, privacyPolicyUrl).
- Vor erster Nachricht Opt-in-Dialog; dann POST /api/ai/visitor-chatbot: centerId (UUID), client=website, chatConsentAccepted:true, sessionId (UUID/Tab, sessionStorage), messages [{role,content}]. Keine OpenAI-Keys im Client.
- Wissensbasis (Pflicht): bei JEDEM Chat-POST pageContext mitsenden: page {path,title}; entity auf Detailseiten; localContent {sections}; siteKnowledge aus lib/center-site-knowledge.ts; Cockpit bleibt Source of Truth (Teil D).

Auth/Grenzen: kein website-config/agencyos ohne Auth (401); keine Secrets im Frontend. Template-Inhalte: public-visitor-surface → data.templatePublicContent.content + data.v0Integration. Fallback bei !res.ok (Teil B). Kontakt/Vermietung: Proxy (Teil A).

Umami/Consent + next/font: Pflicht, Teil E.

DESIGN-DNA (Pflicht, VOR dem ersten Screen — kein AI-Slop, kein Einheitslook):
1) Aus Center-Daten + Redaktion 3 Charakter-Adjektive festlegen (z.B. „urban, industriell, jung" / „premium, ruhig, hell"). JEDE Designentscheidung (Font, Radius, Bildsprache, Weißraum, Motion) folgt dieser DNA.
SPRACHE MIT DER REDAKTION: Alltagssprache, keine Fachbegriffe (Archetyp/Hairline intern). 2–3 fertige Vorschläge je ein Satz — Redaktion wählt, v0 übersetzt. Nie offene Fachfragen.
2) Tokens aus public-visitor-surface (baseColor, accentColor, secondaryColor, fontFamily, templatePublicContent) als CSS-Variablen. EIN dominanter Akzent, Rest neutral; Tints/Shades aus baseColor ableiten statt neuer Buntfarben. Radius bewusst (oft 4–8px). Hell/Dunkel nur mit Grund.
3) Typografie: Display + Body als unverwechselbares Pairing PRO CENTER via next/font. Als Default verboten: Inter, Roboto, Poppins, Montserrat, Space Grotesk, Arial, System-UI. Je nach DNA z.B. Fraunces, Instrument Serif, Bricolage Grotesque, Sora, Newsreader, Archivo. Body 16–18px, line-height ≥1.5. Mono nur für Daten (Zeiten, Etage, Shop-Nr.).
4) Layout-Archetyp intern wählen: editorial/Magazin | asymmetrisches Raster | Split-Hero | typo-dominant | bild-dominant.
5) Hero: asymmetrisch/links, kein zentrierter Gradient-Headline-Block mit zwei Buttons + drei Feature-Cards. Signature statt Stock (s.u.).
6) Sektionen: Alternativen zum 2x2-Card-Grid (nummerierte Liste mit Hairlines, Bild-Text-Wechsel) — Inhalte aus Cockpit-API.
7) Rest screen-für-screen im Chat — nie alles in einem Prompt (Drift zu Defaults).

VARIATION (Pflicht — gegen Einheitsbrei im Portfolio): Font-Paar, Archetyp und Details nie aus Gewohnheit wählen, sondern aus Center-Eigenschaften ableiten: baseColor-Ton (warm → Serif-Display prüfen, kühl/kräftig → charaktervolle Grotesk), Mieter-Mix (Mode → editorial, Nahversorgung → funktional-warm, Gastro/Freizeit → plakativ). Zwei Center nie mit gleicher Kombination (Font-Paar+Archetyp+Details).

DATEN VOR LAYOUT: Inhalte zählen (Shops, Events 30 Tage, Angebote, Gastro-Anteil), Startseiten-Sektionen danach priorisieren — keine Standard-Reihenfolge. <30 Shops: kuratierte Darstellung statt Filter-Grid. Kaum Events: Sektion weglassen statt leer. Starker Foodcourt: eigene Gastro-Sektion.

SIGNATURE-DETAILS (genau 2 pro Center): z.B. übergroße Öffnungszeiten-Typo; Shop-Namen-Laufband; Duotone-Bilder in baseColor; Trenner aus Logo-Geometrie; Hover als Farbfläche statt Schatten. 0 = generisch, >2 = unruhig. Bilder: EIN Treatment (Filter, Masken, Ratios) — nie Roh-Fotos gemischt.

MUT-REGEL: mind. eine markante Entscheidung pro Site (dunkles Theme, Serif-Display, H1 bis clamp 6–8rem, asymmetrischer Header) — der Redaktion als einfache A/B-Wahl anbieten.

VERBOTEN (Anti-Slop): Gradient-Text; Icon-im-Farbkreis; Card-Grid mit Schatten als Default; bunte Status-Pills; Stock-Illustrationen; Violett-Blau-Verlauf; Glassmorphism; Floskeln; Hover/Scroll-Reveals ohne Grund (Teil E).
STATTDESSEN: klare Hierarchie mit echten Cockpit-Daten; leere Zustände = klare CTA; Status dezent (Rahmen-Label, wenige Farben). Motion: Teil E.

ICONS: NIE Emojis im UI (auch nicht in Buttons, Listen, Badges, Kategorien) — ein Icon-Set (z.B. Lucide; nur genutzte Icons, Teil E), einheitliche Strichstärke/Größe, Farbe aus Tokens.

QUALITÄT (v0-PFLICHT, user not technical): Before "done"/deploy/go-live and when user says "check/done/live?": (1) run npx impeccable detect app/ components/ lib/ — fix until exit 0; (2) self-check: gradient? circle-icons? emojis? card-grid? pills? stock? (3) reply in plain language, no jargon. User does not know Impeccable — you run it. Project start: npx impeccable skills install -y --scope=project.

BRANDING (Pflicht): app/icon (Favicon aus Logo), app/opengraph-image (Logo + baseColor + Name), metadataBase: Teil J. Gebrandete not-found/error: Teil B.

Characters: 4920 / 5000

Quality before go-live (v0 checks internally)

Before "done", deploy, and go-live, v0 checks the code with Impeccable (Part C). Editorial does not need a terminal — one sentence in chat is enough:

SituationExample
Page looks generic"Does this still look AI-generated? Please review and improve."
Before handoff to IT"Is the website really finished? Please check everything."
Before go-live"Can we go live? Check first, then Part J."

v0 replies in plain language (e.g. "Removed gradient, unified buttons") — not with terminal output.

Tech (IT) — optional
npx impeccable skills install -y --scope=project
npx impeccable detect app/ components/ lib/

Part D — paste this box into Instruction „Cockpit Workflow“

cockpitOS — Teil D (Pflicht-Workflow)

GOLDENE REGEL: cockpitOS ist die einzige Source of Truth für Inhalte.
Shops, News, Events, Angebote, Texte, Bilder — niemals als Dummy/Placeholder im Frontend-Code hartcoden. Alles, was auf der Website erscheint, kommt aus der cockpitOS-API.

VERBOTENE MUSTER (niemals):
- getWebsiteConfig()/getHeroSlides() gegen /api/agencyos/…/website-config oder /api/centers/…/website-config ohne Auth (401)
- const shops = [ { name: "H&M", … } ] // hardcodierte Shops; ebenso News/Events/Offers
- Platzhalter-Arrays oder Mock-Objekte für echte Inhalte
- Eigene API-Endpunkte erfinden, die die Doku nicht nennt (z.B. /public-config, /analytics-config)
- Bei API-Fehlern Error-Objekte rendern — immer Fallback (leeres Array, Defaults)
- Resend/RESEND_API_KEY in v0 — email only via dashboard proxy (Part A)
- Analytics-Config selbst bauen: Daten kommen aus GET …/public-visitor-surface → data.tracking

PFLICHTSEITEN (Recht, jede Website):
- /impressum und /datenschutz IMMER anlegen; Footer-Link auf jeder Seite.
- Content from page-content (pageType impressum/datenschutz). If empty: ask editorial, offer MCP write. No invented legal text; go-live only when filled (Part J).
- Datenschutzerklärung muss abdecken: Umami (nach Opt-in), Chatbot (KI-Verarbeitung), Kontaktformular, Hosting. Cookie-Banner + Formular-Checkbox: Teil C/A.

PROJEKT-START (immer so beginnen):
1. cockpit_center_project_init (center_slug, api_base_url) → centerId, Checkliste, Quick-Reference
2. cockpit_update_center_website_config wenn Website-Config (Theme, Analytics, Domain) gesetzt/geändert wird
3. Inhalte lesen: cockpit_center_context (centerId)
4. Fehlende Inhalte direkt ins cockpitOS schreiben: cockpit_content_push
5. Nie direkt in der DB ändern — immer offizielle MCP-Tools oder API

MCP-KERNTOOLS:
- Start: cockpit_center_project_init(centerSlug | centerId)
- Lesen: cockpit_center_context(centerId, include="shops,news,events,offers,…")
- Schreiben: cockpit_content_push({ centerId, shops?, news?, events?, offers?, … })
- Config/Theme/Analytics: cockpit_get_center_website_config + update_center_website_config
- Deploy melden: cockpit_register_frontend_deployment (Teil J)
- Cache: cockpit_revalidate_website nach größeren Pushes
- Tool unbekannt: cockpit_mcp_discover_tools — nie raten, nie DB direkt
- GEO mandatory: cockpit_v0_geo_standards → p0Blockers (JSON-LD, sitemap, shop links; baseline Part E)

SHOPS — STANDALONE VS CHAIN:
- Local/this center only: content_push shops:[{name,category,…}] WITHOUT chainName/kette/marke/chainSlug (optional standaloneShop:true). No create_chain+location.
- Real chain (branches): list_chains → create_chain → create_shop_location.
- locationCount=1 ≠ every shop is a chain. “Local shop” = standalone.

CONTENT-WORKFLOW — PROAKTIV ANBIETEN:
- Beschreibt die Redaktion einen Inhalt (Shop, News, Event, Angebot), sofort anbieten ihn direkt ins cockpitOS zu schreiben. Nicht warten: „Soll ich das direkt ins cockpitOS eintragen?"
- cockpit_content_push direkt verwenden (Live-Push aktiv). cockpit_content_push_preview nur auf Wunsch oder bei komplexen Mehrfach-Aktionen.
- Nach dem Push: cockpit_center_context aufrufen und kurz bestätigen, dass der Inhalt angekommen ist.

VERÖFFENTLICHUNGSZEITFENSTER (wie Cockpit „Sichtbar ab/bis" — beim MCP-Schreiben mitliefern):
- News: publishDate (+ optional publishEndDate)
- Events: startDate/endDate = Termin; zusätzlich publishStartDate/publishEndDate wenn Website-Sichtbarkeit abweicht
- Angebote: startDate/endDate = Aktionszeitraum; publishStartDate/publishEndDate für Website-Laufzeit. cockpit_update_offer hat keine Publish-Felder — dafür content_push oder API-PUT nutzen.
- Public GETs filter server-side (news/events/jobs/bundle). Still set windows when editorial gives dates.

ANALYTICS (Umami):
- Über cockpit_update_center_website_config: analyticsConfig mit umamiWebsiteId + umamiUrl. Tracking-Code nie selbst hartcoden — das Dashboard steuert Analytics über die Config.
- Verifikation: GET /api/agencyos/v1/centers/{centerId}/website-config

LIVE-BETRIEB & DEPLOY:
- Inhalte parallel im v0-Chat oder Cockpit — gleiche DB (content_push).
- Nach Vercel-Deploy: Teil J (register_frontend_deployment oder IT /api/cockpit-register). Status: Website-Management → Frontend-Kanäle (v0).

WENN INHALTE FEHLEN: nicht erfinden, nicht mocken. Sofort anbieten, den Inhalt über cockpit_content_push anzulegen. Beispiel: „Ich sehe, dass noch kein Angebot vorhanden ist. Soll ich direkt eines im cockpitOS anlegen?"

ZIEL: Die Redaktion greift so wenig wie möglich manuell ein. Der Assistent schreibt aktiv und proaktiv ins cockpitOS.

Characters: ~4980 / 5000

Standalone shop vs chain (editorial summary)

SituationCorrectWrong
Local tenant, this center onlyStandalone via content_pushCreate chain + branch
Known brand with branches (Deichmann, DM, …)Check/create chain + branch
User says “no chain” / “local shop”standaloneShop: true, no chain fieldscreate_chain because shop is new

Why: Fields like chainName or kette in a push auto-create a ShopChain — even for single-location tenants. That pollutes the chain list and mixes branch logic with local shops.

Part E — paste this box into Instruction „Cockpit E“

cockpitOS — Teil E (Performance, Barrierefreiheit, SEO-Basis, Resilienz)

Ziel: 60fps ohne Ruckeln, kleiner JS-Bundle, schnelle Interaktion (INP), für alle bedienbar, ab Tag 1 indexierbar richtig konfiguriert.

ARCHITEKTUR:
- Standard: Server Components; "use client" nur für echte Interaktion (Slider, Overlay, Formular-State). Keine ganze page.tsx als Client wegen einem Button.
- Schwere Client-Bausteine: dynamic(() => import(…), { ssr:false }) + loading-Fallback.
- Keine komplette shadcn/ui- oder Icon-Library — nur genutzte Komponenten/Icons importieren.

REACT (bei Listen/Slidern mit Ruckeln): keine Inline-Objekte/Funktionen pro Item (style={{…}}, onClick={() => …}); useMemo/useCallback nur bei nachgewiesenem Re-Render-Problem.

ANIMATIONEN (Pflicht):
- Nur transform und opacity. Nicht animieren: top, left, width, height, margin, padding, filter, blur, box-shadow, backdrop-filter.
- Kein scale-hover auf jedem Karten-Grid; max. eine Einstiegs-Sequenz, Rest statisch oder leichtes CSS :hover.
- prefers-reduced-motion: reduce → Animationen aus/stark verkürzt, Slider-Autoplay aus.
- Slider: kein Layout-Shift beim Bildwechsel; Intervall ≥5s; bei document.hidden pausieren.
- Bei Konflikt mit Teil C „Motion": diese Performance-Regeln haben Vorrang.

SCROLL & EVENTS: passive Listener; throttle/debounce ≥100ms; kein setState pro Scroll-Pixel; kein Maus-Parallax, kein scroll-gebundenes blur/filter.

BILDER: next/image mit width/height oder fill+sizes; Hero priority, darunter lazy; keine Multi-MB-Rohbilder (CDN: Teil C); Autoplay-Video nur muted loop, kurz, komprimiert.

BUNDLE: eine Animations-Lib (CSS oft genug; Framer Motion nur wenn nötig); keine ungenutzten Chart-/Map-/UI-Pakete.

BARRIEREFREIHEIT (BFSG — Pflicht ab Start, nicht nachrüsten):
- Semantik: header/nav/main/footer, genau eine h1 pro Seite, html lang="de".
- Alt-Texte aus API-Daten (Shop-Name, Event-Titel); dekorative Bilder alt="".
- Kontrast ≥4.5:1 — Akzentfarbe auf Weiß prüfen, sonst abgedunkelte Textvariante ableiten.
- Volle Tastaturbedienung (Menü, Slider, Akkordeon, Chat); sichtbarer Fokus-Ring (nie outline:none ohne Ersatz); Skip-Link zum Inhalt.
- Formulare: label je Feld; Fehler als Text via aria-describedby + aria-live, nicht nur rot.
- Touch-Targets ≥44px. Centerplan: Shops zusätzlich als durchsuchbare Liste erreichbar (Teil F) — Canvas allein reicht nicht.

SEO-GRUNDGERÜST (ab erstem Deploy):
- app/robots.ts: Preview (*.vercel.app bzw. VERCEL_ENV!==production) → alles disallow + noindex; Live-Domain → allow + Sitemap-Verweis. Umschalten beim Go-Live: Teil J.
- app/sitemap.ts dynamisch aus API (Start, Shops, News, Events, statische Seiten).
- generateMetadata je Seite: unique Title „{Seite} | {Center-Name}", echte Description; canonical über metadataBase (Live-Domain, Teil J).
- JSON-LD: ShoppingCenter + openingHoursSpecification (Home), Event/Offer auf Detailseiten. Vollstandard: cockpit_v0_geo_standards (Teil D/J).

UMAMI & COOKIE-CONSENT (Pflicht bei Tracking):
- public-visitor-surface → data.tracking + data.visitorPrivacy. NICHT /public-config.
- <V0AnalyticsSnippet centerId={centerUuid} /> — Umami erst NACH Cookie-Opt-in (cc_cookie, analytics); Banner-Texte aus visitorPrivacy; „Ablehnen" gleichrangig neben „Akzeptieren" (kein Dark Pattern). Ohne Opt-in: null Requests an Analytics.
- Optional Footer „Analytics deaktivieren" wenn tracking.trackingOptOut → localStorage cockpit-tracking-opt-out=1.
- Webfonts IMMER über next/font (self-hosted, kein Google-CDN-Request zur Laufzeit — DSGVO). Externe Maps/Embeds nur nach Cookie-Kategorie external (Platzhalter mit Klick-Freigabe).

RESILIENZ (Fallback wenn API kurz down — Pflicht): ISR + statisches JSON-Fallback + Try-Catch. Nie leere Dummy-Shops/News bei API-Fehler.
1) ISR: export const revalidate = 300 auf Seiten/Segments mit Cockpit-fetch.
2) Statisches JSON-Fallback (Build): Skript vor next build (scripts/generate-fallback-json.mjs) holt Shops/News/Events/Bundle von NEXT_PUBLIC_DASHBOARD_URL + Slug (Teil A) → public/fallback/*.json. package.json: "prebuild": "node scripts/generate-fallback-json.mjs".
3) Laufzeit: try { fetch…; if(!res.ok) throw } catch { JSON.parse(await readFile('public/fallback/shops.json','utf8')) } — gleiches Muster für news/events/bundle. Optional dezentes Banner „Offline-Modus".

ISR/CACHE (Publish & Live): on-demand POST /api/revalidate + REVALIDATION_SECRET (Vorlage). Nach MCP content_push invalidiert das Dashboard; cockpit_revalidate_website nach großen Pushes. Zeitfenster News/Events/Angebote: serverseitige Filter + Revalidate — nicht stale-forever.

ABNAHME (IT): DevTools Performance (Long Tasks >50ms reduzieren), React Profiler (kein Voll-Rerender pro State), optional ANALYZE=true npm run build. Ziel: Lighthouse mobil Performance ≥90, Accessibility ≥95 (Gate: Teil J).

Characters: 4803 / 5000

Part F — paste this box into Instruction „Cockpit F“

cockpitOS — Teil F (Centerplan: SVG, Hybrid, 3D)

DATEN (öffentlich, kein website-config)
1) GET …/public-visitor-surface → data.centerplan: initialViewMode (2d|3d), initialZoom3D, hoverColor3d, hoverColorSvg, noExtrudeLayerIds[], showLogosOnPlan; data.v0Integration.centerplan
2) GET …/api/wayfinding/floors?centerId= → floors[]: mapSvg (String!), mapImage, shopViewBoxes, mapLocations[], enable3D, …
MCP/v0: zuerst omitMapSvg=true (alle Etagen zählen) — volle Antwort oft zu groß für Chat (nur EG sichtbar). Dann floorNumber=0|1|… oder Server-Fetch; oder cockpit_center_context include=floors_summary.
3) Optional GET …/wayfinding/centerplan?centerId= (Metadaten/Bounds)
4) Routing: GET …/entrances; POST …/wayfinding/routing — endLocationId = mapLocations[].id (UUID), startWaypointId = entrances[].id

HYBRID (PNG in SVG)
- mapSvg = <image> Hintergrund + path/polygon id="shop-…"
- 2D: <image> pointer-events:none; Klicks nur auf Vektor-IDs
- mapImage + relative Pfade → CDN https://cockpitos.b-cdn.net
- shopViewBoxes: JSON svgId→viewBox für Detail-Zoom

SVG-ID-KONVENTION (nicht raten)
shop-… → Shop (extrudieren/klickbar) | service-… → Service (dünn)
entrance-… | mall-floor-… Boden | foodcourt-… | display-… nur Signage
decorative/decorative-… → Deko, NICHT klickbar, NICHT extrudieren
<g id="logos">/nopointer-logos → flach | <g id="hover-layer"> → Hybrid-Klickflächen, transparent
<g id="routes"> oder Routes → Wegenetz UNSICHTBAR (display:none); Route nur als berechnetes Overlay

KLICK & NAVIGATION
mapLocations: svgId === DOM-id → shop.id/service.id. Niemals svgId als Shop-UUID. Kein Mapping ≠ per Name erraten.
2D (v0, @mall-os/wayfinding privat): buildSvgIdToLocationMap → linkableSvgIds; click delegation path/g/polygon; onPoiClick
HOVER 2D (Pflicht): NICHT mouseover/mouseout auf path/g — feuert pro Kind, Hover klebt. Stattdessen: svg mousemove → elementFromPoint/closest POI → eine hoveredSvgId; svg mouseleave → null. hoverColorSvg aus surface. CSS :hover nur optional.
V0-NAVIGATION (Pflicht): Plan nur IDs — Ziele in v0: /shops/{slug|id}, /services/{slug|id}; Modal-CTA intern, NIE Cockpit-Website. modal-then-detail|direct. ?shop=uuid gleiche Logik.
OPTION EMBED: /embed/centerplan. cockpitOrigin=customDomain||{slug}.cockpit-os.de. chrome=plan|minimal|standard|full (default minimal). showFloors|showViewMode|showLegend|showSidebar=0|1. detailMode=parent|cockpit
A11y (Pflicht): Plan nie einziger Zugang — parallele Shop-Liste mit Suche/Etagen-Filter (Tastatur + Screenreader); Info-Card mit Fokus-Management, per Esc schließbar.

3D-DARSTELLUNG (wenn gewünscht)
- Client-only: dynamic(() => import("./centerplan-3d"), { ssr:false })
- Stack: @react-three/fiber + three + SVGLoader (oder Port @mall-os/wayfinding)
- Ablauf: mapSvg parsen → Shapes; Typ via id + parent <g id>
- ExtrudeGeometry: Shops höher, Services flacher, floor als Platte
- Globale Gruppe: SCALE ~0.01, Y-Flip SVG→3D
- initialViewMode respektieren; noExtrudeLayerIds + data-no-extrude="true" → flach
- #routes in 3D unsichtbar; Etagen-Tabs über floors[].floorNumber; enable3D prüfen
- model3D (GLB) nur Fallback wenn mapSvg leer — sonst SVG-3D

V0-TYPISCHE FEHLER — 3D NICHT VORZEITIG AUF 2D UMSCHALTEN
- API liefert 3D-taugliche Daten: floors[].mapSvg = SVG-String (beginnt oft mit "<svg")
- NICHT „kein verwertbares Format" behaupten ohne mapSvg geprüft zu haben
- NICHT mapImage extrudieren — mapImage = nur 2D-Hintergrund; 3D aus mapSvg-Vektoren
- NICHT mapSvg als JSON/XML missverstehen; NICHT <img>/dangerouslySetInnerHTML statt SVGLoader
- centerId = Center-UUID aus by-slug → id; NICHT UUID aus Centerplan-Edit-URL (floorId)
- Vor Aufgeben (Debug): mapSvg vorhanden? „shop-" im String? mapLocations.length > 0?
- Hybrid: <image> nicht extrudieren — nur path/polygon mit id shop-…/service-…
- Einzelne Paths können ExtrudeGeometry werfen → try/catch: Path überspringen, Rest rendern
- 2D-only NUR wenn mapSvg leer ODER keine shop-Polygone ODER enable3D=false
- Nicht pauschal „3D entfernen" — erst SVGLoader-Pipeline + classify + Teil-F-Regeln umsetzen

ROUTEN
Primär POST …/wayfinding/routing. Fallback: #routes aus mapSvg parsen (Graph aus Segment-Endpunkten). Anker-IDs: p-shop-…, p-service-…, pf-stairs…, pfb-elevator…

CODE-STRUKTUR
Server: floors + surface laden. Client: nur Canvas/Interaktion. components/wayfinding/ (2d-plan, 3d-scene, floor-tabs getrennt); lib/wayfinding/ (classify, route). Bei Konflikt Performance: Teil E.

VERBOTEN
Demo-Shops auf Plan | website-config für Plan | Error in JSX (#418) | #routes sichtbar | decorative/hover-layer als Shops behandeln | Cockpit-URL als v0 Shop-Detail

Center-UUID aus by-slug — nicht floorId aus Centerplan-Edit-URL.
Doku: centerplan/svg-struktur.md, hybrid-png-mit-polygonen.md, hybrid-ai-masterprompt.md (Cockpit-Docs). 2D-Klick-Nachbau: Chat-Prompt „Centerplan“ unten auf dieser Seite.

Characters: 4398 / 5000

Centerplan — chat prompt (after re-pasting Part F)

If the plan shows SVG but clicks are missing or shops link to the wrong site: paste this box once into v0 chat (not as an Instruction).

Centerplan 2D fertigstellen (Palais Vest / Basis-Template).

KONTEXT: Teil F Instruction ist aktiv. PlanView2D rendert mapSvg — fehlt: Klick-Delegation + v0-Navigation.

DATEN (bereits in Teil A/B/F):
by-slug → centerId; public-visitor-surface; GET …/wayfinding/floors?centerId=; Client GET shops.

AUFGABE
1) lib/wayfinding/build-svg-id-map.ts (client, useMemo):
buildSvgIdToLocationMap(mapSvg, mapLocations) — svgId direkt, dann x/y in BBox
2) plan-view-2d.tsx erweitern:
- Nach SVG-Mount: decorative, #routes, hover-layer, <image> → pointer-events:none
- Click delegation: closest path/g/polygon/rect; id ∈ linkableSvgIds oder shop-/service-*
- Hover: svg.onmousemove → elementFromPoint + closest POI → setHoveredSvgId (eine ID); svg.onmouseleave → null. NICHT mouseover/mouseout auf Kind-Pfade.
- Pan nur wenn Bewegung > 8px (sonst Klick)
- onPoiClick({ type, id, mapLocationId, svgId })
3) lib/navigation/resolve-href.ts:
resolveShopDetailHref → /shops/{slug||id}
resolveServiceDetailHref → /services/{slug||id}
KEIN Link auf dashboard oder Cockpit-Center-Website
4) lib/center-config.ts: navigationMode = 'modal-then-detail' (Default)
Klick → ShopDetailSheet mit Link href={resolveShopDetailHref(shop)}
mode 'direct' → router.push statt Sheet
5) app/centerplan/page.tsx: Etagen-Tabs, PlanView2D, optional Shop-Liste (A11y)
6) Deep-Link ?shop=uuid beim Mount

DATEIEN (Vorschlag)
lib/wayfinding/build-svg-id-map.ts
components/wayfinding/plan-view-2d.tsx
components/wayfinding/shop-detail-sheet.tsx
lib/navigation/resolve-href.ts
lib/center-config.ts

DEBUG: console.log({ linked: Object.keys(map).length, mapSvgLen, mapLocs: floor.mapLocations?.length })
3D erst wenn 2D-Klicks stehen (Teil F2 Chat).

Part F2 — 3D rebuild (chat prompt or Instruction „Cockpit F2“)

When: v0 builds the plan wrong, wants to drop 3D, or uses mapImage instead of mapSvg. Part F stays active; F2 additionally as Instruction or once in chat.

Full workflow

The box below is the complete build prompt for v0. If character limit is tight: paste into chat only, not a second Instruction.

Part F2 — paste this box into Instruction „Cockpit F2“ or into v0 chat

cockpitOS — Teil F2 (Centerplan 2D+3D nachbauen wie cockpitOS)

Ziel: 2D/3D-Umschalter wie Center-Website. NICHT auf mapImage-only fallback wenn mapSvg existiert. NICHT 3D entfernen ohne SVGLoader-Pipeline.

DATEN (Server lädt, Client rendert)
centerId = UUID aus GET …/centers/by-slug/{slug} → id (NICHT floorId aus Centerplan-Edit-URL!)
1) GET …/public-visitor-surface → data.centerplan (initialViewMode, initialZoom3D, hoverColor3d, hoverColorSvg, noExtrudeLayerIds)
2) GET …/wayfinding/floors?centerId= → floors[].mapSvg (STRING!), mapImage, mapLocations[], enable3D
3) GET …/shops?publicWebsite=true&status=Aktiv&limit=5000
Pro Etage: mapSvg leer → 2D mit mapImage (CDN https://cockpitos.b-cdn.net). Sonst 3D aus mapSvg.

Mapping aus mapLocations + shops: svgId→Location, shopLogos, shopNames, linkableSvgIds.

DATEIEN
lib/centerplan/load-data.ts (Server) | centerplan-view.tsx (client)
centerplan-3d-scene.tsx (dynamic ssr:false) | parse-svg-for-3d.ts | svg-id-classify.ts
Etagen-Tabs, Toggle 2D|3D, min-h-[60vh].

3D-PIPELINE (Pflicht)
Deps: three, @react-three/fiber, @react-three/drei
1) strip fill/stroke url(#…) — SVGLoader crasht sonst
2) SVGLoader.parse(mapSvg) → paths; id = node.id ODER parent.id (<g id="shop-56"><path/>)
3) classify: shop-* shop | service-* service | mall-floor floor | decorative skip
#routes/route → unsichtbar | logos/nopointer-logos → flach
noExtrudeLayerIds + data-no-extrude → flach
4) SVGLoader.createShapes → ExtrudeGeometry; fehlerhafte Paths try/catch skip
depth shop=60 foodcourt=50 floor=20 service=4; rotateX(-PI/2); scale(1,-1,1); shop GAP 0.95
5) Gruppe scale={[0.01,-0.01,-0.01]} + BBox zentrieren
6) Kamera isometrisch: fov=50, dist=max(w,d)/initialZoom3D, pos center+(d*0.707,d*0.707,d*0.707)
7) Hybrid: <image> NICHT extrudieren — nur path/polygon ids
8) Klick onPointerUp → svgId → mapLocation → shop.id
9) Kein Demo-Plan, kein website-config

2D: mapSvg inline; Hybrid pointer-events:none auf <image>; hoverColorSvg

DEBUG: console.log({ mapSvgLen, hasShop: mapSvg?.includes('shop-'), mapLoc: mapLocations?.length })

FIX wenn kaputt: mapSvg.slice(0,300) zeigen; mapImage aus Three entfernen; centerId prüfen.

VERBOTEN: mapImage extrudieren | SVG als <img> für 3D | „kein Format" ohne Debug | Error in JSX
Ergänzt Teil F — bei Konflikt Performance: Teil E.
Zusätzlich: Plan-A11y aus Teil F umsetzen (parallele Shop-Liste, Fokus-Management der Info-Card).

Part G — paste this box into Instruction „Cockpit G“

cockpitOS — Teil G (Multi-Center: ein Layout, viele Domains)

PRINZIP
Mehrere Shopping Center teilen EIN v0-Layout (gleiches websiteTemplate im Cockpit). Unterschiede (Name, Shops, Hero, Plan, Farben, Fonts) kommen pro Center aus der API — nicht aus dupliziertem Code. Gilt für jede Template-Familie (ilg, mec-shopbox, rgw, …).

ZIEL: Ein v0-Projekt pro Layout/Template-Familie — KEIN Duplizieren des Layouts pro Center-ID.

WANN: Pflicht wenn mehrere Center dasselbe Layout nutzen. Einzel-Center: Teil G optional, fester Slug in Teil A reicht.

VERBOTEN
- centerId oder Slug in Komponenten hardcoden (kein const centerId = "uuid…")
- HIER_SLUG_EINTRAGEN in page.tsx, Hero, Footer, Shops (nur .env.example/Preview)
- Pro Center eigene Kopie von Homepage/Shops-Seiten im Repo
- Links immer mit /{slug}/-Prefix — auf Custom Domain falsch (/shops, nicht /slug/shops)

PFLICHT: lib/cockpit/resolve-center.ts (einzige Quelle)
export async function resolveCenter(): Promise<{ centerId, slug, websiteTemplate } | null>
Auflösungs-Reihenfolge (Server/Middleware):
1) host = Request-Host ohne Port; www. entfernen
2) Production (Host nicht *.vercel.app): GET {NEXT_PUBLIC_DASHBOARD_URL}/api/centers/by-domain?domain={host} → { success, center: { id, slug, websiteTemplate, … } } — id = centerId
3) Preview/Dev (vercel.app/localhost): a) Query ?center={slug} ODER b) DEFAULT_CENTER_SLUG ODER c) optional Subdomain {slug}.vercel.app in Middleware
4) slug → GET …/api/centers/by-slug/{slug} → centerId (= id)
5) GET …/public-visitor-surface(centerId) → data.center.websiteTemplate
6) Optional Env EXPECTED_WEBSITE_TEMPLATE: Abweichung → 404/Fehlerseite, nie falsches Layout rendern

ARCHITEKTUR
- middleware.ts: Host/Query auswerten; centerId+slug an Layout (Header x-center-id/x-center-slug oder React Context)
- app/layout.tsx: einmal resolveCenter → CenterProvider; alle Pages lesen Context
- Daten-fetch immer mit aufgelöstem centerId (Server Components)
- Neues Center = Domain in Vercel + Cockpit pflegen — kein neues Projekt

LINKS & ROUTEN
- Custom Domain: Pfade OHNE Slug (/shops, /aktuelles)
- Preview/Slug-Modus: /{slug}/shops — lib/cockpit/paths.ts generatePath(path, { slug, isCustomDomain })

SEO & RECHT PRO DOMAIN (Pflicht)
- generateMetadata, canonical, OG-Image, Favicon, JSON-LD: host-basiert aus resolveCenter — pro Center Name/Logo/baseColor, nie geteilt hardcoden.
- app/sitemap.ts + robots.ts pro Host dynamisch (nur Inhalte des aufgelösten Centers; Preview: noindex, Teil E/J).
- /impressum und /datenschutz je Center aus page-content (Teil D) — Betreiber unterscheiden sich, niemals einen gemeinsamen Text für alle Domains ausliefern.
- Design-DNA (Teil C) pro Center anwenden: gleiche Struktur ja, aber Farben/Signature-Element aus API, damit Domains nicht klonhaft wirken.

VERCEL & ENV
Modell A (empfohlen): EIN Projekt, ALLE Domains dieser Template-Familie unter Vercel → Domains
Modell B: Ein Repo, mehrere Vercel-Projekte — nur DEFAULT_CENTER_SLUG unterscheidet sich
Pflicht: NEXT_PUBLIC_DASHBOARD_URL=https://dashboard.cockpit-os.de (ohne Slash)
Optional: EXPECTED_WEBSITE_TEMPLATE=<Wert aus Cockpit>, DEFAULT_CENTER_SLUG=beispiel-center-slug

DATEN & UNTERSCHIEDE
Nach resolveCenter: gleiche APIs wie Teil A (surface, shops, page-content, floors, …). Branding, Hero, Shops, Plan — alles pro Center aus API; Layout/JSX identisch.

CHATBOT & FORMULARE
POST visitor-chatbot / Proxy kontakt: centerId aus resolveCenter — nie fest verdrahtet. Chatbot: pageContext bei jedem POST (Teil C).

ABNAHME
- Domain A → Logo/Shops/Impressum von Center A; Domain B → Center B, gleiches Layout
- v0-Preview: ?center=slug funktioniert
- grep: keine hardcodierte Center-UUID in components/ oder app/
- Sitemap von Domain A enthält keine URLs von Center B

Characters: 3757 / 5000

Part H — Signage, steles & kiosk (info terminal, not phone)

When: v0 builds kiosk or wayfinder stele (e.g. 55″, often 1080×1920). This is an info terminalnot a scaled-up phone. Companion/NOW!Part I (mobile layout).

Reference: Premium AI / Wayfinding, Kiosk + Companion NOW!, System info for v0.

Terminal layout (target for v0)

┌─────────────────────────────┐ ← 0% Bezel tot
│ HEADER kompakt (~40px) │ ← Logo · Uhr · minimal — kein Hero-Banner
├─────────────────────────────┤
│ │
│ CENTERPLAN HERO (75%+) │ 2D/3D — Hauptelement, volle Breite/Höhe
│ │
│ ┌─ [EG][1.OG][2.OG] ─┐ │ Floating Floor-Pills (Overlay im Plan)
│ │
├─────────────────────────────┤
│ TOOLBAR schmal (Icons) │ Suche · Shops · Route · Handoff-QR
├─────────────────────────────┤ ← 100% Sockel tot
└─────────────────────────────┘

Shop-/Route-Info = kompakte Card über dem Plan — kein Mobile-Vollbild-Panel

Design-Prinzipien: hohe Informationsdichte · kleinere Typografie als Companion · Plan dominiert · UI am Rand schmal · professioneller Terminal-Look.

Physische Stele: oben/unten Bezel oft tot (~400–480 px oben, ~290–380 px unten) — Haupt-Taps in unterer Toolbar und Floating Pills (greifbare Zone). 3D: bottomUIFraction ~0.2–0.35 damit Shops nicht hinter Toolbar verschwinden.

Part H — paste this box into Instruction „Cockpit H“

cockpitOS — Teil H (Kiosk/Stele — Info-Terminal)

NUR KIOSK/STELE — Companion → Teil I (Handy-UI). Signage: H + I zusammen.

KEIN GROSSES HANDY
55"-Stele (oft 1080×1920) = Info-Terminal, NICHT Companion vergrößert. Mehr Dichte, Plan dominiert.

TERMINAL-LAYOUT (Pflicht)
1) PLAN = HERO: min. 75% der nutzbaren Fläche — 2D/3D-Centerplan (Teil F/F2)
2) HEADER kompakt: max ~40–48px — Logo, Uhr, Center-Name klein — kein Hero-Banner
3) FLOOR-TABS: floating Pills als Overlay unten IM Plan — keine volle Breiten-Leiste
4) TOOLBAR unten: schmale Icon-Leiste (Suche, Shops, Route, Handoff-QR) — kompakt
5) SHOP/ROUTE-INFO: kompakte Cards über dem Plan — keine Mobile-Vollbild-Sheets
6) Typo kleiner/dichter als Companion; professioneller Terminal-Look; Design-Tokens/DNA aus Teil C (Center-Branding, kein Generic-Kiosk-Grau)

TOUCH & BEZEL (physische Stele)
- Oben/unten Bezel oft tot (~400–480px oben, ~290–380px unten) — Haupt-Taps in unterer Toolbar + Floor-Pills (greifbare Zone)
- Touch-Targets min. 48px in Toolbar/Pills; KEINE Hover-only-Interaktion (kein Cursor)
- Hoher Kontrast für Tageslicht/Reflexionen; Schrift größer als Desktop-Body (Lesedistanz ~1m)
- 3D: bottomUIFraction ~0.25, damit Ziel-Shop über Toolbar sichtbar bleibt
- Handoff-QR in Toolbar → Companion (Teil I)

KIOSK-BETRIEB (Pflicht)
- Idle-Reset: nach ~90s ohne Touch zurück zur Startansicht; Suche, Route, Chat-Verlauf und Eingaben zurücksetzen — der nächste Besucher darf NICHTS vom Vorgänger sehen (Datenschutz).
- Kein Cookie-Banner am Terminal: kein personenbezogenes Tracking; Nutzung nur aggregiert über Cockpit-Analytics-Config (Tracking: Teil I).
- Keine externen Login-/Zahlungs-Flows; Tastatur nur als On-Screen bei Suche.
- Fehler-Resilienz: API down → Fallback-Daten (Teil E) + Hinweis; nie weißer Screen im Dauerbetrieb. Auto-Reload bei fatalem Fehler (error.tsx mit Timer).
- Uhr/Öffnungszeiten live aus public-visitor-surface; „Heute geöffnet bis …" prominent.

DATEN
Teil A: floors/mapSvg, shops, surface — kein Dummy.

VERBOTEN
- Mobile-App-Layout auf Stele (große Cards, Handy-Bottom-Sheets, riesige Header)
- Plan als kleine Karte statt Hero
- Nur Kiosk ohne Companion (Teil I)
- Branding-Zone 30%, die den Plan verdrängt
- Session-Daten, die den Idle-Reset überleben

v0-Prompt: „Kiosk Terminal-Style: Plan 75%+, kompakter Header, Floating Floor-Pills, schmale Toolbar unten."

HANDOFF
- Toolbar: QR/Button „Auf Handy weiter" → POST {DASHBOARD}/api/public/handoff-sessions { centerId, touchscreenId?, location? } → sessionKey
- Gleiche Origin für Kiosk + Companion (ein v0-Projekt/eine Vercel-URL)
- Details Tracking/PATCH: Teil I (Companion-Mount, Usage-Events)

REFERENZ-DOKU (Cockpit-Docs): premium-ai-template.md | kiosk-companion-now-app.md | kiosk-companion-system-info-for-v0.md

Characters: 2670 / 5000

Part I — Companion / NOW! app (kiosk pair)

When: Every signage/kiosk app needs a companion (NOW! — phone app). User scans QR at kiosk and continues on smartphone (same route/shop).

Always a pair

Rule of thumb: v0 signage project = kiosk (Part H) + companion (Part I) in the same Next.js repo — shared API/plan components, separate shells/layouts.

Full feature list: System info kiosk & companion for v0

Part I — paste this box into Instruction „Cockpit I“

cockpitOS — Teil I (Companion/NOW! — Paar zur Signage-App)

PRINZIP
Jede Signage/Kiosk-App hat eine Companion (NOW!) — Handy per QR vom Kiosk.
EIN v0-Projekt, ZWEI Oberflächen:
- /kiosk oder /signage → Stele (Teil H — Touch-Zonen)
- /companion → Mobil (eigenes UI — NICHT Stele-Zonen)

ARCHITEKTUR
Shared: lib/cockpit/, lib/centerplan/ (Teil F/F2), Shop-Detail, Branding/Design-Tokens (Teil C)
Getrennt: kiosk-shell vs companion-shell (Layout, Navigation, Typo)
Gleiche centerId, gleiche floors/shops/APIs (Teil A)

HANDOFF KIOSK → COMPANION (Pflicht)
1) Kiosk: Button/QR „Auf Handy weiter"/Route mitnehmen (Toolbar, Teil H)
2) Session: POST {DASHBOARD}/api/public/handoff-sessions { centerId, touchscreenId?, location? } → sessionKey
3) QR/Link: /companion?centerId=UUID&sessionKey=…&shop=…&screen=companion-home|wayfinding (centerId always in URL — Umami/reporting)
4) Companion mount: PATCH {DASHBOARD}/api/public/handoff-sessions { sessionKey, centerId, companionOpened:true }
5) Companion: read URL params → target shop/route, optional handoff banner
6) Journey steps: POST {DASHBOARD}/api/analytics/usage-events { centerId, source:kiosk|companion, eventType, payload:{ sessionKey } }

COMPANION UI (MOBIL)
- mobile-first Portrait; env(safe-area-inset-*)
- Fix oben: Logo + „NOW!" | Fix unten: Concierge/Chat (Teil C visitor-chatbot inkl. Consent-Dialog vor erster Nachricht)
- Scroll nur zwischen Header und Chatbar — kein mitscrollender fixed-Bug
- Home: Vollbild-Karte (gleiche mapSvg/3D wie Kiosk) + Highlights + Chat
- pointer-events: Karte tappbar — kein unsichtbares Overlay über Canvas (Wrapper pointer-events:none, Karte + interaktive UI auto)
- Shop-Sheet z-index über Chatbar; Route inline auf Home-Karte möglich
- A11y mobil (Pflicht, Teil E): Touch-Targets ≥44px in Daumenzone; Bottom-Sheets mit Fokus-Trap, per Esc/Swipe schließbar, Fokus zurück zum Auslöser; Kontrast ≥4.5:1 auch auf Kartenoverlays; prefers-reduced-motion respektieren

ROUTEN COMPANION
/companion — Home (Karte + Highlights)
/companion/shops, /companion/shops/[id]
/companion/wayfinding (optional wenn Home schon Plan hat)
/companion/qr/[qrCodeId] — location QR resolver (required for printed service/shop/parking QRs)
Menü: News, Events, Angebote → Cockpit-GETs (Teil A)

LOCATION QR (not handoff — separate session API)
1) Printed QR = stable entry URL on active companion domain (v0 or Cockpit signage), e.g. {companionOrigin}/companion/qr/{qrCodeId}
2) GET {DASHBOARD}/api/public/qr-codes/{qrCodeId}/resolve → targetType, redirectMode, finalRedirectPath|finalRedirectUrl
OR (preferred): POST {DASHBOARD}/api/public/qr-codes/{qrCodeId}/scan { centerId, sessionKey? } — track + resolve
3) Alternatively separate: POST {DASHBOARD}/api/qr-codes/{qrCodeId}/track { centerId, sessionKey? }
4) Redirect: companion → router.push(finalRedirectPath); external → window.location.href = finalRedirectUrl (not router.push with absolute URL)
5) Website/NOW! QRs: entry {websiteOrigin}/qr/{qrCodeId} — same resolve API, redirectMode website|external
Handoff QRs (kiosk→phone) stay POST/PATCH /api/public/handoff-sessions — do not mix with location QRs.

DATEN
Wie Center-Website: public-visitor-surface, floors, shops, aktuelles-bundle, hotpicks. Kein website-config ohne Auth. centerId aus URL/Handoff — nie hardcoden.

RECHT (mobil öffentlich erreichbar)
Footer/Menü: Links auf /impressum + /datenschutz des Centers (Teil D). Umami erst nach Opt-in (Teil C) — Handoff-Session ist KEIN Consent.

TRACKING (optional — sonst unsichtbar in Cockpit-Reportings)
Umami: GET {DASHBOARD}/api/umami-app-config?app=companion + Script (center_id in Events)
Nutzung: POST {DASHBOARD}/api/analytics/usage-events — source kiosk|companion, centerId, eventType, payload.sessionKey
Handoff: POST {DASHBOARD}/api/public/handoff-sessions (QR) + PATCH (companionOpened) — sichtbar unter Companion → QR-Sessions
QR-Sessions-Tab: alle Handoffs mit POST/PATCH public API (auch v0)

DESIGN
Companion ≠ verkleinerter Kiosk — eigene Handy-Optik (Cards, Bottom-Sheets, Daumenzone unten), aber gleiche Design-DNA/Farben wie Website/Kiosk (Teil C).

v0-START-PROMPT
„Baue Kiosk (Teil H) UND Companion NOW! (Teil I) mit Handoff-QR und gemeinsamem Centerplan."

VERBOTEN
- Signage nur Kiosk ohne /companion
- Companion mit Stele-Zonen-Layout
- Hardcodierte Session ohne sessionKey in URL
- Dummy-Shops | getWebsiteConfig ohne Auth

Siehe auch: kiosk-companion-system-info-for-v0.md (Cockpit-Docs)

Characters: 3144 / 5000

Part J — Instruction „Cockpit J“ (deploy & go-live)

Optional but recommended after first Vercel deploy. Editorial stays in v0 — Cockpit learns the live URL automatically.

Part J — paste this box into Instruction „Cockpit J“

cockpitOS — Teil J (Deploy, Revalidate, Go-Live)

WANN: Nach erstem Vercel-Deploy oder wenn sich die Deploy-URL ändert.
ZIEL: Cockpit kennt die Live-Adresse; Publish erscheint schnell auf der Website; Redaktion trägt keine URLs/Secrets ein.

REVALIDATION_SECRET (v0 automatisch — Redaktion nie anfassen):
- Pflicht: app/api/revalidate/route.ts (Cockpit-Vorlage: POST JSON { secret, centerSlug, paths?, fullRevalidation?, pathMode? }).
- v0 beim Erst-Setup/wenn Route fehlt:
1) Existiert Vercel-Env REVALIDATION_SECRET (Team Shared verlinkt)? → nutzen, NICHT neu generieren.
2) Sonst: crypto.randomBytes(32).toString('hex') → als REVALIDATION_SECRET in Vercel Project Env; .env.example nur mit Platzhalter.
3) Redaktion: kein Schritt. Interner IT-Hinweis (Secret nie in Chat/Git/Client): Cockpit Render braucht denselben Wert — SMG-Standard: Team Shared mit globalem Cockpit-Secret verlinken statt pro Center neu.
- Nach MCP content_push/Cockpit-Publish ruft das Dashboard POST /api/revalidate auf websitePublicUrl — nur bei übereinstimmenden Secrets. Sonst ISR-Fallback revalidate=300 (Teil E). Manuell: cockpit_revalidate_website.

AUTOMATISCH (bevorzugt, IT richtet Team Shared einmal ein):
- Route app/api/cockpit-register + optional postbuild scripts/cockpit-register-after-deploy.mjs
- Vercel Env:
A) Team Shared (jedes Projekt verlinken + Redeploy): COCKPIT_DASHBOARD_URL, REVALIDATION_SECRET, COCKPIT_FRONTEND_CHANNEL (website|signage|companion)
B) Nur Projekt: COCKPIT_REGISTER_TOKEN (frt_…), COCKPIT_CENTER_SLUG, NEXT_PUBLIC_DASHBOARD_URL (= Teil A)
Optional: VERCEL_PROJECT_ID=prj_…, COCKPIT_VERCEL_PROJECT_MODE=dedicated|shared

MANUELL IM CHAT (wenn kein Register-Hook):
0. Optional: cockpit_frontend_channels — isV0WebsiteConnected?
1. Deploy-Origin (https://…vercel.app ohne Pfad)
2. cockpit_register_frontend_deployment: centerId, channel website|signage|companion, origin, optional vercelProjectId/Mode
3. Bestätigen: „Mit Cockpit verbunden — nach Publish automatische Aktualisierung (wenn Revalidate aktiv)."

GO-LIVE-CHECKLISTE (vor Custom Domain — Pflicht, jeden Punkt bestätigen):
1) cockpit_v0_geo_standards: alle p0Blockers implementiert, selfVerification abgehakt
2) robots/index: Live-Domain allow + Sitemap-Verweis; Preview-noindex-Sperre greift NUR noch auf vercel.app; canonical + metadataBase = Custom Domain (nicht vercel.app, nicht cockpit-os.de)
3) /impressum + /datenschutz befüllt (aus page-content, Teil D) und im Footer verlinkt — ohne diese KEIN Go-Live
4) Cookie-Banner blockt Umami/Externes bis Opt-in; „Ablehnen" gleichrangig (Teil C)
5) Kontakt-/Vermietungsformular real getestet: Testanfrage kommt beim richtigen Empfänger an (formRecipients prüfen)
6) 404/error gebrandet (Teil B); Favicon + OG-Image gesetzt — Link-Vorschau testen
7) Lighthouse mobil: Performance ≥90 anstreben, Accessibility ≥95 (Teil E); Centerplan auf Touch bedienbar + Listen-Alternative (Teil F)
8) Öffnungszeiten, specialDays, Anfahrt stimmen mit Cockpit überein; Sitemap erreichbar
9) Impeccable: npx impeccable detect app/ components/ lib/ → Exit 0 (no open findings); on Exit 2 fix, do not silently ignore (Part C)

GO-LIVE CUSTOM DOMAIN (nur IT/mit Freigabe):
- cockpit_dns_status → cockpit_go_live_domain (dryRun true, dann false)
- Dashboard-Env: VERCEL_API_TOKEN, UD_RESELLING_* (IT)

STATUS: Website-Management → Frontend-Kanäle (v0)
LIVE-BETRIEB: Inhalte über Teil D (content_push) — v0-Chat oder Cockpit. Revalidate automatisch wenn Secret + Route stimmen.
NIEMALS: Register-Token (frt_…), REVALIDATION_SECRET oder AgencyOS-Keys in Client/öffentliche Repos.
TOKEN (IT/MCP): Dashboard → Frontend-Kanäle → Register-Token; oder cockpit_center_project_init + register_frontend_deployment
KANÄLE: website = Center-Website | signage = Kiosk (Teil H) | companion = NOW! (Teil I)

Characters: 3674 / 5000

Vercel hosting & dashboard stats (all frontends)

v0 layouts are additional channels — Cockpit website, signage, and preview remain. API always via dashboard.

Details: Parallel frontends (website, signage, companion)

TopicSummary
Data/APIAlways dashboard — CORS allows Vercel
Connect v0Website management → Frontend channels (v0) — one place; often automatic after deploy (Part J)
Handoff QRKiosk + companion same origin (one v0 project); POST /api/public/handoff-sessions
Companion → QR sessionsHandoff sessions from v0 visible when POST /api/public/handoff-sessions is used
Make stats visiblev0: Umami (/api/umami-app-config?app=companion) + optional POST /api/analytics/usage-events with centerId

For IT (JSON examples, field lists): Examples & media

Nutzungsstatistik: Seitenaufrufe werden anonymisiert erfasst. Im Umami-Dashboard nach diesem Pfad filtern: /en/content-creator-handbuch/center-website-v0-api-ready-to-go