Messe-Landingpage /her-career/ zur herCAREER Expo 2026

Ziel des QR-Codes auf der Rückwandgrafik. Der gedruckte Code zeigt auf
https://her-career.anelia-wolf.com – die Adresse ist damit unveränderlich.
Die Seite liegt als /her-career/ im normalen Export, die Subdomain liefert
dieselbe Datei aus demselben Docroot aus; DNS, Zertifikat, Vhosts für nginx
und Apache und die Prüfbefehle stehen in docs/SUBDOMAIN-HERCAREER.md.

Aufbau in beiden Sprachen: Hero (ink) → 01 Messe-Special → 02 Themen →
03 Auf der Messe → 04 Kontakt mit dem Formular. Die vier Themenblöcke der
Rückwand liegen auf den drei Leistungsseiten, wie in der Navigation; jede
Zeile verlinkt absolut auf die Hauptdomain.

Der Wortlaut des Messe-Specials fehlt noch und steht sichtbar als „[…]“ auf
der Seite. Der Build-Guard hält den Launch-Flip an, bis er da ist
(CONTENT_TODO in src/lib/legal.ts, docs/OFFENE-DATEN.md Punkte 2.7–2.9).
Deadline ist nicht der Livegang, sondern der 22.10.2026.

- Canonical auf anelia-wolf.com/her-career/, damit die Subdomain-Kopie nicht
  ein zweites Mal im Index steht
- flags.showHerCareer trägt Termine-Ledger, Sitemap-Eintrag und die
  Meta-Beschreibung mit Stand und Uhrzeit. Ab dem 24.10.2026 auf false
- ServiceHero: facts und cta sind optional – ohne beides und ohne Foto
  entfällt die rechte Spalte
- ProcessContact: optionaler Zähler, die Messeseite nummeriert ihre Abschnitte
- Kopf-CTA auf dieser Seite ausgeblendet. Das macht eine Regel in globals.css,
  weil der Kopf außerhalb von <main> liegt und auf der Subdomain der Pfad
  ohnehin „/“ ist

Enthält außerdem die bis dahin nicht committeten Änderungen an Header,
LangSwitch, OverlayMenu, Wordmark und globals.css.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 22:39:26 +02:00
parent b9e1d4d15c
commit 33ec0df7e3
21 changed files with 941 additions and 50 deletions

View File

@@ -1,8 +1,9 @@
# Anelia Wolf Website # Anelia Wolf Website
Statische Website für Anelia Wolf (Keynote Speakerin, Trainerin, Coach). Statische Website für Anelia Wolf (Keynote Speakerin, Trainerin, Coach).
Zwölf Routen in zwei Sprachen: Startseite, drei Leistungsseiten, Impressum und Vierzehn Routen in zwei Sprachen: Startseite, drei Leistungsseiten, die Messeseite
Datenschutz deutsch auf `/`, englisch unter `/en/` plus die 404. zur herCAREER Expo, Impressum und Datenschutz deutsch auf `/`, englisch unter
`/en/` plus die 404.
Next.js 16 (App Router) im **Static Export**, Tailwind v4, GSAP + Lenis, TypeScript. Next.js 16 (App Router) im **Static Export**, Tailwind v4, GSAP + Lenis, TypeScript.
Kein Backend, keine Datenbank, keine Formulare. Kein Backend, keine Datenbank, keine Formulare.
@@ -56,6 +57,7 @@ mit ändern.
| `src/components/` | `layout/`, `motion/` (GSAP-Primitiven), `ui/`, `brand/`, `home/`, `services/` | | `src/components/` | `layout/`, `motion/` (GSAP-Primitiven), `ui/`, `brand/`, `home/`, `services/` |
| `public/brand/`, `public/photos/` | Drop-in-Slots für Logo und Fotos (je eigene README) | | `public/brand/`, `public/photos/` | Drop-in-Slots für Logo und Fotos (je eigene README) |
| `docs/superpowers/` | Spec und Build-Plan | | `docs/superpowers/` | Spec und Build-Plan |
| `docs/SUBDOMAIN-HERCAREER.md` | `her-career.anelia-wolf.com` Ziel des QR-Codes auf der Rückwandgrafik: DNS, Zertifikat, Vhost |
## Drop-in-Assets ## Drop-in-Assets
@@ -80,6 +82,7 @@ Das gelieferte Logo-SVG wird inline eingebettet. Enthält es ein `<script>`,
| `showPlaceholders` | `true` bis zum Launch: offene Angaben stehen sichtbar als „[…]“ in Gold | | `showPlaceholders` | `true` bis zum Launch: offene Angaben stehen sichtbar als „[…]“ in Gold |
| `hostOutsideEu` | nur `true`, wenn Hoster/Office-Anbieter außerhalb EU/EWR sitzen (Drittland-Absatz) | | `hostOutsideEu` | nur `true`, wenn Hoster/Office-Anbieter außerhalb EU/EWR sitzen (Drittland-Absatz) |
| `showFaq`, `showStimmen`, `showSpeakerPdf`, `showSecondTalk`, `show24hPromise`, `showIndustry`, `showTwins` | schalten einzelne Blöcke frei, sobald die Inhalte freigegeben sind | | `showFaq`, `showStimmen`, `showSpeakerPdf`, `showSecondTalk`, `show24hPromise`, `showIndustry`, `showTwins` | schalten einzelne Blöcke frei, sobald die Inhalte freigegeben sind |
| `showHerCareer` | `true`, solange die herCAREER Expo bevorsteht: nur dann trägt `/her-career/` die Termin-Angaben und steht in der Sitemap. Ab 24.10.2026 auf `false`. |
## Vor dem Livegang ## Vor dem Livegang

View File

@@ -26,6 +26,7 @@ Adressen, Englisch liegt vollstaendig unter `/en/`:
| `/leistungen/keynotes/` | Keynotes & Speaking | | `/leistungen/keynotes/` | Keynotes & Speaking |
| `/impressum/` | Impressum | | `/impressum/` | Impressum |
| `/datenschutz/` | Datenschutzerklärung | | `/datenschutz/` | Datenschutzerklärung |
| `/her-career/` | Messe-Landingpage zur herCAREER Expo 2026 Ziel des QR-Codes auf der Rückwandgrafik, ausgeliefert auch unter `her-career.anelia-wolf.com` (`docs/SUBDOMAIN-HERCAREER.md`) |
| 404 | „Diese Fährte endet hier.“ deutsch, mit englischem Zweizeiler darunter | | 404 | „Diese Fährte endet hier.“ deutsch, mit englischem Zweizeiler darunter |
| `/en/` | English start page | | `/en/` | English start page |
| `/en/services/coaching/` | Coaching & Assessments | | `/en/services/coaching/` | Coaching & Assessments |
@@ -33,6 +34,7 @@ Adressen, Englisch liegt vollstaendig unter `/en/`:
| `/en/services/keynotes/` | Keynotes & Speaking | | `/en/services/keynotes/` | Keynotes & Speaking |
| `/en/imprint/` | Imprint (Übersetzung, verbindlich bleibt `/impressum/`) | | `/en/imprint/` | Imprint (Übersetzung, verbindlich bleibt `/impressum/`) |
| `/en/privacy/` | Privacy (Übersetzung, verbindlich bleibt `/datenschutz/`) | | `/en/privacy/` | Privacy (Übersetzung, verbindlich bleibt `/datenschutz/`) |
| `/en/her-career/` | herCAREER Expo 2026 (englische Fassung der Messeseite) |
Dazu erzeugt der Build `sitemap.xml`, `robots.txt`, `icon.svg` und `apple-icon.png`. Dazu erzeugt der Build `sitemap.xml`, `robots.txt`, `icon.svg` und `apple-icon.png`.
@@ -154,12 +156,22 @@ Beides steht an genau einer Stelle: `routesByLang` in `src/lib/i18n.ts`.
Wer eine Adresse aendert, aendert sie dort Navigation, Fuss, Sitemap, Wer eine Adresse aendert, aendert sie dort Navigation, Fuss, Sitemap,
hreflang und der Sprachumschalter lesen alle aus dieser Tabelle. hreflang und der Sprachumschalter lesen alle aus dieser Tabelle.
**Umschalter.** Die beiden Flaggen im Kopf (`LangSwitch.tsx`) fuehren nicht **Umschalter.** Der Umschalter (`LangSwitch.tsx`) steht auf jeder Breite im Kopf
pauschal auf die Startseite, sondern auf das Gegenstueck derselben Seite und fuehrt nicht pauschal auf die Startseite, sondern auf das Gegenstueck
(`counterpart()`). Unter 1024 px ist im Kopf kein Platz; dort steht der derselben Seite (`counterpart()`). Zwei Formen:
Umschalter im Overlay-Menue. Eine Flagge steht streng genommen fuer ein Land
und nicht fuer eine Sprache deshalb steht das Kuerzel DE/EN daneben und der - ab 1024 px beide Sprachen nebeneinander, die aktive still in Gold,
zugaengliche Name nennt die Sprache ausgeschrieben. - darunter nur die Zielsprache als ein Knopf ein Tipp, ein Wechsel. Neben
Wortmarke und „Menue“ hat auf einem Telefon kein Paar Platz.
Der Kopf liegt ueber dem Overlay-Menue, der Knopf bleibt also auch bei offenem
Menue erreichbar. Im Menue selbst steht er nicht mehr: Dort fiel er auf kurzen
Geraeten (iPhone SE, 667 px) unter die Kante, ohne dass man dem Menue ansah,
dass darunter noch etwas kommt.
Eine Flagge steht streng genommen fuer ein Land und nicht fuer eine Sprache
deshalb steht das Kuerzel DE/EN daneben und der zugaengliche Name nennt die
Sprache ausgeschrieben.
**Zwei Wurzel-Layouts.** `app/(de)/layout.tsx` schreibt `<html lang="de">`, **Zwei Wurzel-Layouts.** `app/(de)/layout.tsx` schreibt `<html lang="de">`,
`app/en/layout.tsx` schreibt `<html lang="en">`. Im App Router schreibt nur das `app/en/layout.tsx` schreibt `<html lang="en">`. Im App Router schreibt nur das
@@ -240,6 +252,12 @@ Ressourcen lädt, ist eine sehr strenge Content-Security-Policy möglich.
speist Canonical-URLs, OpenGraph und `sitemap.xml`. Bei anderer Domain hier speist Canonical-URLs, OpenGraph und `sitemap.xml`. Bei anderer Domain hier
ändern und neu bauen. ändern und neu bauen.
**Subdomain `her-career.anelia-wolf.com`**: Der QR-Code auf der Rückwandgrafik
zeigt auf diese Adresse gedruckt und damit unveränderlich. Sie braucht keinen
zweiten Build: derselbe Docroot, `/` liefert `her-career/index.html`, alles andere
geht per 301 auf die Hauptdomain. Vhost-Beispiele für nginx und Apache, DNS,
Zertifikat und Prüfbefehle stehen in **`docs/SUBDOMAIN-HERCAREER.md`**.
--- ---
## 3. Bilder und Logo einspielen (Drop-in) ## 3. Bilder und Logo einspielen (Drop-in)
@@ -330,6 +348,7 @@ Freigaben vorliegen. Kein Flag verlangt eine Codeänderung an Komponenten.
| `show24hPromise` | `false` | Zusage „Antwort innerhalb von 24 Stunden“ | | `show24hPromise` | `false` | Zusage „Antwort innerhalb von 24 Stunden“ |
| `showIndustry` | `false` | Branchennennung im Nebentätigkeits-Kontext | | `showIndustry` | `false` | Branchennennung im Nebentätigkeits-Kontext |
| `showTwins` | `true` | Zwillinge-Satz in der Biografie | | `showTwins` | `true` | Zwillinge-Satz in der Biografie |
| `showHerCareer` | `true` | Messeseite `/her-career/` trägt den Termine-Ledger („Auf der Messe“) und steht in der Sitemap; die Meta-Beschreibung nennt Stand und Uhrzeit. Ab dem 24.10.2026 auf `false` siehe Abschnitt 6. |
| `showLangSwitch` | `true` | Sprachumschalter im Kopf und im Overlay-Menü. Auf `false` verschwinden beide Flaggen, die englischen Seiten bleiben über ihre Adresse erreichbar. | | `showLangSwitch` | `true` | Sprachumschalter im Kopf und im Overlay-Menü. Auf `false` verschwinden beide Flaggen, die englischen Seiten bleiben über ihre Adresse erreichbar. |
Nicht bestätigte Aussagen sind bewusst ausgeschaltet: eine Zusage, die nicht Nicht bestätigte Aussagen sind bewusst ausgeschaltet: eine Zusage, die nicht
@@ -414,7 +433,14 @@ vergangenes Event). Zu tun, mit Termin im Kalender:
3. Auch anpassen: `src/content/keynotes.ts` (Meta-Zeile 3. Auch anpassen: `src/content/keynotes.ts` (Meta-Zeile
„Keynote · herCAREER Expo 2026 · 23.10. · 10:30“) und „Keynote · herCAREER Expo 2026 · 23.10. · 10:30“) und
`src/content/home.ts` (Marginalie „Bühne“, Fact-Zeile). `src/content/home.ts` (Marginalie „Bühne“, Fact-Zeile).
4. `npm run build`, `out/` hochladen. 4. **Messeseite `/her-career/`**: `flags.showHerCareer` auf `false` setzen. Damit
fällt der Termine-Ledger weg, die Meta-Beschreibung wechselt auf die Fassung ohne
Stand und Uhrzeit, und die Seite verschwindet aus der Sitemap; Hero, Messe-Special,
Themen und Kontakt bleiben stehen. Die Seite selbst bleibt erreichbar gedruckte
QR-Codes laufen weiter.
Ist das Messe-Special nach der Messe hinfällig, muss sein Text mit weg
(`src/content/her-career.ts`, `src/content/en/her-career.ts`).
5. `npm run build`, `out/` hochladen.
**Offen: Wer pflegt das, und wann?** Das ist Spec §12 Punkt 17 und muss mit **Offen: Wer pflegt das, und wann?** Das ist Spec §12 Punkt 17 und muss mit
der Kundin geklärt werden am besten als feste Zuständigkeit plus der Kundin geklärt werden am besten als feste Zuständigkeit plus
@@ -481,7 +507,7 @@ Produktions-Build.
| `npx tsc --noEmit -p tsconfig.json` | fehlerfrei | | `npx tsc --noEmit -p tsconfig.json` | fehlerfrei |
| `npm run lint` (ESLint) | fehlerfrei | | `npm run lint` (ESLint) | fehlerfrei |
| `npm run build` | erfolgreich, 12 statische Seiten | | `npm run build` | erfolgreich, 12 statische Seiten |
| Routen in `out/` | `/`, `/leistungen/coaching/`, `/leistungen/trainings/`, `/leistungen/keynotes/`, `/impressum/`, `/datenschutz/`, `404.html` (+ `_not-found/`) alle vorhanden | | Routen in `out/` | `/`, `/leistungen/coaching/`, `/leistungen/trainings/`, `/leistungen/keynotes/`, `/her-career/`, `/impressum/`, `/datenschutz/`, `404.html` (+ `_not-found/`) alle vorhanden |
| `localStorage` / `sessionStorage` / `document.cookie` in `out/` | **kein Treffer** (HTML, JS, CSS geprüft) | | `localStorage` / `sessionStorage` / `document.cookie` in `out/` | **kein Treffer** (HTML, JS, CSS geprüft) |
| Externe Origins in den HTML-Dateien | nur `anelia-wolf.com`, `linkedin.com`, `her-career.com`, `bfdi.bund.de`, `schema.org`, `w3.org` (SVG-Namespace) | | Externe Origins in den HTML-Dateien | nur `anelia-wolf.com`, `linkedin.com`, `her-career.com`, `bfdi.bund.de`, `schema.org`, `w3.org` (SVG-Namespace) |
| Externe Ressourcen | keine: 0 iframes, alle `<script src>` unter `/_next/`, alle 18 `@font-face` zeigen auf lokale `.woff2` in `_next/static/media/`. Die Treffer `nextjs.org`, `react.dev`, `gsap.com`, `github.com` liegen ausschließlich als Text in Fehlermeldungen und Lizenzkommentaren in Vendor-Bundles **keine Requests**. | | Externe Ressourcen | keine: 0 iframes, alle `<script src>` unter `/_next/`, alle 18 `@font-face` zeigen auf lokale `.woff2` in `_next/static/media/`. Die Treffer `nextjs.org`, `react.dev`, `gsap.com`, `github.com` liegen ausschließlich als Text in Fehlermeldungen und Lizenzkommentaren in Vendor-Bundles **keine Requests**. |
@@ -530,7 +556,8 @@ Produktions-Build aus `out/` ist er nicht vorhanden.
- [ ] Freigegebene Blöcke per Flag eingeschaltet (FAQ, Stimmen, Speaker-PDF …) - [ ] Freigegebene Blöcke per Flag eingeschaltet (FAQ, Stimmen, Speaker-PDF …)
- [ ] `hostOutsideEu` korrekt gesetzt, AV-Verträge mit Hoster und E-Mail-Anbieter - [ ] `hostOutsideEu` korrekt gesetzt, AV-Verträge mit Hoster und E-Mail-Anbieter
- [ ] Domain in `src/lib/site.ts` stimmt, HTTPS aktiv, 404-Seite konfiguriert - [ ] Domain in `src/lib/site.ts` stimmt, HTTPS aktiv, 404-Seite konfiguriert
- [ ] `out/` hochgeladen, alle zwölf Routen im Browser geprüft (sechs deutsch, sechs englisch) - [ ] `out/` hochgeladen, alle vierzehn Routen im Browser geprüft (sieben deutsch, sieben englisch)
- [ ] Subdomain `her-career.anelia-wolf.com` eingerichtet und mit dem **gedruckten** QR-Code gescannt (`docs/SUBDOMAIN-HERCAREER.md`)
- [ ] Sprachumschalter auf jeder Seite geprüft: er führt auf dieselbe Seite in der anderen Sprache, nicht auf die Startseite - [ ] Sprachumschalter auf jeder Seite geprüft: er führt auf dieselbe Seite in der anderen Sprache, nicht auf die Startseite
- [ ] Kalendereintrag für den 24.10.2026 (Termine-Block, Abschnitt 6) - [ ] Kalendereintrag für den 24.10.2026 (Termine-Block, Abschnitt 6)

239
docs/OFFENE-DATEN.md Normal file
View File

@@ -0,0 +1,239 @@
# Offene Daten Website Anelia Wolf
Stand: 07.09.2026 · Pflegen: bei jeder Lieferung die Statusspalte ändern, nicht die Zeile löschen.
Die Website ist fertig gebaut und läuft in beiden Sprachen. Was hier steht, ist alles,
was noch von außen kommen muss, bevor sie live gehen kann. Drei Quellen im Code halten
dieselbe Liste maschinenlesbar wer hier etwas einträgt, trägt es dort mit ein:
| Quelle | Inhalt |
|---|---|
| `LEGAL_TODO` in `src/lib/legal.ts` | Pflichtangaben für Impressum und Datenschutz |
| `CONTENT_TODO` in `src/lib/legal.ts` | sichtbare Platzhalter auf den Leistungsseiten |
| `flags` in `src/lib/site.ts` | Blöcke, die auf eine Freigabe warten |
**Der Build passt auf.** Solange irgendwo ein „[…]“ steht, bricht `npm run build` ab,
sobald `flags.showPlaceholders` auf `false` gesetzt wird. Diesen Schalter umzulegen ist
der letzte Schritt vor dem Livegang niemand kann ihn versehentlich überspringen.
**Status-Spalte:** `offen` · `liegt vor` · `entfällt` (mit Begründung).
---
## 1. Rechtlich blockiert den Livegang
Ohne diese Angaben darf die Seite nicht online. Ein Impressum ohne ladungsfähige
Anschrift ist abmahnfähig, eine Datenschutzerklärung ohne Hoster-Angabe unvollständig.
| # | Angabe | Wofür | Wer liefert | Status |
|---|---|---|---|---|
| 1.1 | Ladungsfähige Anschrift: Straße, Hausnummer, PLZ, Ort **kein Postfach** | Impressum (§ 5 DDG), Datenschutz Ziffer 1 | Anelia | offen |
| 1.2 | USt-IdNr. (§ 27a UStG) **oder** W-IdNr. (§ 139c AO) oder: Abschnitt streichen | Impressum | Anelia / Steuerberatung | offen |
| 1.3 | Kleinunternehmerregelung nach § 19 UStG: ja / nein | Impressum | Anelia / Steuerberatung | offen |
| 1.4 | Berufshaftpflicht: Versicherer, Anschrift, räumlicher Geltungsbereich oder: Abschnitt streichen | Impressum | Anelia | offen |
| 1.5 | Geschäftsbezeichnung nur, wenn sie tatsächlich so geführt wird | Impressum (optional) | Anelia | offen |
| 1.6 | Bundesland des Firmensitzes → daraus die zuständige Datenschutz-Aufsichtsbehörde (Name, Anschrift, Website) | Datenschutz, Art. 77 DSGVO | Anelia | offen |
| 1.7 | Hosting-Anbieter: Firma, Anschrift, Land | Datenschutz Ziffer 3 | webklar | offen |
| 1.8 | Löschfrist der Server-Logfiles in Tagen (üblich 714) beim Hoster erfragen | Datenschutz Ziffer 3 | webklar | offen |
| 1.9 | **AV-Vertrag mit dem Hoster** (Art. 28 DSGVO), unterschrieben | Datenschutz Ziffer 3 | Anelia + Hoster | offen |
| 1.10 | E-Mail-/Office-Anbieter: Firma, Anschrift, Land | Datenschutz Ziffer 4 | Anelia | offen |
| 1.11 | **AV-Vertrag mit dem E-Mail-Anbieter** (Art. 28 DSGVO) | Datenschutz Ziffer 4 | Anelia | offen |
| 1.12 | Sitzt Hoster **oder** E-Mail-Anbieter außerhalb EU/EWR? Wenn ja: Land + Rechtsgrundlage (Angemessenheitsbeschluss oder Standardvertragsklauseln) | schaltet `flags.hostOutsideEu` | webklar prüft | offen |
| 1.13 | Aufbewahrungsfrist für Kontaktanfragen (z. B. 12 Monate) | Datenschutz Ziffer 4 | Anelia | offen |
| 1.14 | Bildnachweis: Name der Fotografin, ggf. Website | Impressum + Bildunterschriften (§ 13 UrhG) | Anelia | offen |
| 1.15 | Bildnachweis: Logo-Designer:in bzw. Agentur | Impressum (optional) | Anelia | offen |
| 1.16 | „Stand“-Datum für beide Rechtstexte setzen | Impressum, Datenschutz | webklar beim Flip | offen |
> **Hinweis zur englischen Fassung:** `/en/imprint/` und `/en/privacy/` sind Übersetzungen,
> verbindlich bleibt die deutsche Fassung das steht dort auch im ersten Absatz. Jede
> Angabe aus dieser Tabelle muss trotzdem in **beiden** Dateien landen; der Build-Guard
> prüft beide.
---
## 2. Inhaltlich sichtbare Platzhalter auf den Leistungsseiten
Diese „[…]“ stehen aktuell in Gold auf der Seite und sind für jeden Besucher lesbar.
| # | Angabe | Wo auf der Seite | Status |
|---|---|---|---|
| 2.1 | Name des Analyse-Instruments (DISG / INSIGHTS / persolog / …) + Zertifizierung. Aktive Lizenz? Darf Name oder Badge erscheinen? | Coaching → Bausteine | offen |
| 2.2 | Dauer des Auswertungsgesprächs | Coaching → Bausteine (zweimal) | offen |
| 2.3 | Sitzungsdauer im Coaching | Coaching → Rahmen | offen |
| 2.4 | Paket bzw. übliche Anzahl Sitzungen | Coaching → Rahmen | offen |
| 2.5 | Gruppengröße bei Trainings | Trainings → Hero-Fakten **und** Rahmen | offen |
| 2.6 | Umfang der Trainingsreihe „Neukundengewinnung“ | Trainings → Module | offen |
| 2.7 | **Messe-Special: Was bekommt, wer den QR-Code auf der Rückwand scannt?** | Messeseite `/her-career/` → Das Special | offen |
| 2.8 | Frist des Specials (z. B. „Anfragen bis 30.11.2026“) | Messeseite → Das Special | offen |
| 2.9 | Bedingung zum Einlösen (Stichwort, Nachweis, nichts?) | Messeseite → Das Special | offen |
> **Die Zeilen 2.72.9 hängen an einem Druckerzeugnis.** Der QR-Code auf der
> Rückwandgrafik ist mit „Messe-Special · Code scannen“ beschriftet und führt auf
> `her-career.anelia-wolf.com`. Wer dort ankommt, erwartet genau dieses Angebot;
> bis zum Wortlaut steht an der Stelle sichtbar „[…]“. Deadline ist damit nicht der
> Livegang, sondern der **22.10.2026**.
---
## 3. Freigaben schalten fertige Blöcke frei
Diese Blöcke sind gebaut und ausgeschaltet. Kein Codeaufwand, nur ein Schalter in
`src/lib/site.ts`. Ausgeschaltet bleiben sie bewusst: Eine Zusage, die nicht gehalten
wird, und ein Testimonial ohne Einwilligung sind riskanter als eine Leerstelle.
| # | Flag | Was fehlt | Status |
|---|---|---|---|
| 3.1 | `showStimmen` | Zitate mit Name, Rolle, Unternehmen und **schriftlicher** Einwilligung. Kundenlogos nur mit Erlaubnis. | offen |
| 3.2 | `showSpeakerPdf` | Speaker-Profil als PDF (Kurzvita, Themen, Fotos, Technik) → `public/speaker-profil.pdf` | offen |
| 3.3 | `showSecondTalk` | Darf „Unlock your Sales-DNA“ gelistet werden? Mit welchem Inhalt? Weitere Vorträge? | offen |
| 3.4 | `show24hPromise` | Zusage „Antwort innerhalb von 24 Stunden“ ja oder nein? | offen |
| 3.5 | `showIndustry` | Darf die Branche der Nebentätigkeit genannt werden (z. B. „Medizinprodukte“)? Der Arbeitgeber wird nie genannt. | offen |
| 3.6 | `showTwins` | Steht auf `true`. Freigabe für den Satz über die Zwillinge bitte bestätigen. | zu bestätigen |
### Einzelfreigaben Biografie
Jede Angabe einzeln bestätigen, sie steht namentlich auf der Startseite:
Vater als Händler · Ausbildung zur Zahntechnikerin · Abteilungsleitung ·
Verkauf und Store Management im Premium-Segment · B2B-Außendienst heute ·
Zwillinge. Dazu: Dürfen später Jahreszahlen ergänzt werden?
### Weitere offene Punkte
| # | Frage | Status |
|---|---|---|
| 3.7 | Preise: Spanne oder „auf Anfrage“ für Coaching, Trainingstag, Keynote | offen |
| 3.8 | Reiseradius und Bühnentechnik bei Keynotes | offen |
| 3.9 | herCAREER-Logo: Erlaubnis vorhanden? (Ohne bleibt es beim reinen Textlink so ist es aktuell gebaut.) | offen |
| 3.10 | Markenrecherche „WOLF-Prinzip“ Dritte verwenden die Bezeichnung bereits. Bis zu einer Eintragung sagt die Seite „Bezeichnung“, nie „Wortmarke“. | empfohlen |
| 3.11 | Wer pflegt den Termine-Block nach dem 23.10.2026? | offen |
| 3.12 | FAQ-Antworten auf den drei Leistungsseiten: von uns formuliert, von Anelia noch nicht gegengelesen. Sie stehen **sichtbar** auf der Seite (`showFaq` ist an). | zu bestätigen |
| 3.13 | Zusage im Hero-Knopf: „30 Min · kostenfrei“ bestätigen oder ändern. | zu bestätigen |
| 3.14 | Zielgruppen-Reihenfolge: Unternehmen zuerst oder Einzelpersonen zuerst? Reiner Texttausch in „Leistungen“ und Hero. | offen |
| 3.15 | Messeseite `/her-career/`: Hero-Text, Themen-Zeilen und Kontakt-Überschrift sind von uns formuliert (Ton der Website, Inhalte der Rückwand) von Anelia gegenlesen. | zu bestätigen |
| 3.16 | Subdomain `her-career.anelia-wolf.com`: DNS, Zertifikat und Vhost einrichten. Muss **vor dem 22.10.2026** stehen, die Adresse ist gedruckt (`docs/SUBDOMAIN-HERCAREER.md`). Wer: webklar. | offen |
---
## 4. Bild- und Design-Dateien
**Das ist die Tabelle für den Designer bzw. die Fotografin.** Fehlt eine Datei, blendet
die Seite den Bildplatz sauber aus oder weicht auf eine typografische Lösung aus
es bricht nichts, aber die Seite verliert ihr Gesicht.
| Datei | Ablage | Format | Mindestmaß | Verhältnis | Wo sie erscheint | Pflicht | Status |
|---|---|---|---|---|---|---|---|
| `anelia-hero.jpg` | `public/photos/` | JPG, sRGB | 1600 px breit | 4:5 hoch | Startseite, Hero (großflächig) | ja | offen |
| `anelia-portrait.jpg` | `public/photos/` | JPG, sRGB | 1400 px breit | 3:4 hoch | „Über Anelia“, engerer Schnitt | nein fällt sonst auf das Hero-Foto zurück | offen |
| `anelia-stage.jpg` | `public/photos/` | JPG, sRGB | 1600 px breit | 3:2 quer | Keynotes-Seite, Bühnenfoto | nein | offen |
| `logo-mark.svg` | `public/brand/` | SVG, transparent, ohne `<script>` | | | Kopf, Hero, WOLF-Prinzip, Fuß, Seitenwechsel, 404 | ja | **liegt vor** |
| `logo-full.png` | `public/brand/` | PNG auf Tinte | 2400 × 1260 px | 1,91:1 | Social-Vorschau beim Teilen (LinkedIn) | nein | **liegt vor** |
| `speaker-profil.pdf` | `public/` | PDF | | | Keynotes → „Für Veranstalter“ (nur mit `showSpeakerPdf`) | nein | offen |
Dazu zwei Punkte, die keine Datei sind, aber am Logo hängen:
| # | Angabe | Wer liefert | Status |
|---|---|---|---|
| 4.1 | **Schriftliche Nutzungsrechte am Logo** vom Designer die Website veröffentlicht es, das Impressum nennt ihn | Anelia | offen |
| 4.2 | Bestätigung, dass das Monogramm **ohne Wortmarke** und der Wolfskopf **einzeln** gezeigt werden dürfen (Favicon, Seitenwechsel, 404) | Anelia / Designer | offen |
### Regeln für die Bilddateien
- **Ruhiger Hintergrund** beim Hero-Foto: Darüber liegt das Monogramm, und die Headline
läuft bis in die linke Bildhälfte.
- **Kein Text im Bild.** Alles Geschriebene steht auf der Seite als echter Text das
bleibt scharf, ist vorlesbar und lässt sich übersetzen.
- **sRGB**, nicht Adobe RGB. Sonst wirken die Bilder im Browser flau.
- **Originale ≥ 2000 px** mitliefern, nicht nur die zugeschnittene Fassung. Der Zuschnitt
kann sich noch ändern.
- **Name der Fotografin** gehört dazu (Punkt 1.14). „Foto: [Name]“ ist eine gesetzliche
Pflicht, kein Höflichkeitsvermerk.
### Regeln für ein neues Logo
Falls das Monogramm ersetzt wird: SVG, transparent, **ohne `<script>`** (ein Skript
bricht den Build absichtlich ab die Datei wird inline in jede Seite eingebettet, und
die Seite sagt „keine externen Skripte“ zu). Die schwarzen Flächen auf
`fill="currentColor"`, damit das W auf dunklem Grund elfenbein und auf hellem tinte
erscheint. IDs `v-left`, `v-right`, `wolf` vergeben, sonst läuft die Zeichen-Animation in
DOM-Reihenfolge. Details in `public/brand/README.md`.
---
## 5. Prompt Liefertabelle für den Designer erzeugen
Der Block unten ist zum Kopieren gedacht. Er steht für sich allein: Wer ihn in einen
Chat einfügt, bekommt die Tabelle, ohne dieses Dokument mitschicken zu müssen. Die
Platzhalter in eckigen Klammern vorher ersetzen.
```text
Du bist Projektleiter einer Webagentur. Erstelle eine Liefertabelle für die
Fotografin und den Grafiker der Website von Anelia Wolf (Keynote Speakerin,
Trainerin und Coach). Ton: knapp, sachlich, keine Werbesprache.
Die Website ist fertig gebaut: statischer Export, Next.js, zwei Sprachen
(Deutsch auf /, Englisch auf /en/). Gestaltung: warmes Schwarz (#0F0E0C),
Elfenbein (#F6F1E8), Gold (#C9A24D), keine Schatten, keine runden Ecken,
Playfair Display für Überschriften. Fehlt eine Bilddatei, blendet die Seite den
Platz sauber aus nichts bricht, aber die Seite verliert ihr Gesicht.
Diese Dateien fehlen noch:
1. anelia-hero.jpg Startseite, großflächig hinter Headline und Monogramm.
JPG, sRGB, hochkant 4:5, mindestens 1600 px breit, ruhiger Hintergrund.
2. anelia-portrait.jpg Abschnitt „Über Anelia“, engerer Schnitt.
JPG, sRGB, hochkant 3:4, mindestens 1400 px breit. Optional.
3. anelia-stage.jpg Keynotes-Seite, Anelia auf der Bühne.
JPG, sRGB, quer 3:2, mindestens 1600 px breit. Optional.
4. speaker-profil.pdf Kurzvita, Themen, Fotos, Technik. Optional.
Feste Regeln: kein Text im Bild (alle Schrift liegt als echter Text auf der
Seite), sRGB statt Adobe RGB, Originale ab 2000 px mitliefern, Name der
Fotografin für den Bildnachweis nach § 13 UrhG.
Gib eine Markdown-Tabelle aus mit den Spalten:
Datei | Motiv/Bildidee | Format | Mindestmaß | Seitenverhältnis | Wo es erscheint |
Pflicht | Wer liefert | Termin | Status
Fülle „Motiv/Bildidee“ mit einem konkreten Vorschlag je Zeile, der zur
Gestaltung passt. „Termin“ mit [DATUM] vorbelegen, „Status“ mit „offen“.
Setze unter die Tabelle drei Sätze, was passiert, wenn eine Datei fehlt.
Antworte auf Deutsch.
```
### Variante für die Rechtsangaben
Dieselbe Mechanik für Abschnitt 1, falls Anelia eine Liste zum Ausfüllen möchte:
```text
Erstelle eine Ausfüll-Liste für eine Selbstständige, die einer Webagentur die
Pflichtangaben für Impressum und Datenschutzerklärung ihrer neuen Website
liefern muss. Deutsches Recht, DDG und DSGVO.
Benötigt werden: ladungsfähige Anschrift (kein Postfach), USt-IdNr. oder
Wirtschafts-Identifikationsnummer oder die Entscheidung, den Abschnitt zu
streichen, Kleinunternehmerregelung nach § 19 UStG ja/nein,
Berufshaftpflichtversicherung (Versicherer, Anschrift, Geltungsbereich) oder
die Entscheidung, den Abschnitt zu streichen, Bundesland des Firmensitzes
(daraus folgt die zuständige Aufsichtsbehörde), E-Mail- und Office-Anbieter
mit Anschrift und Land, Aufbewahrungsfrist für Kontaktanfragen in Monaten,
Name der Fotografin und der Logo-Designerin für den Bildnachweis.
Gib eine Markdown-Tabelle aus: Angabe | Warum sie gebraucht wird (ein
Halbsatz) | Beispiel | Ihre Antwort. Die Spalte „Ihre Antwort“ bleibt leer.
Schreibe die Kundin mit „Sie“ an, keine Paragraphenketten im Fließtext.
Antworte auf Deutsch.
```
---
## 6. Wenn alles da ist der Livegang
1. Angaben in `src/content/legal.ts` **und** `src/content/en/legal.ts` eintragen,
Platzhalter in `src/content/coaching.ts` und `trainings.ts` ersetzen (deutsch und
englisch).
2. Bilder nach `public/photos/` legen, Namen exakt wie in der Tabelle oben.
3. Freigegebene Blöcke per Flag einschalten.
4. `flags.hostOutsideEu` korrekt setzen.
5. `flags.showPlaceholders` auf `false` setzen und `npm run build` laufen lassen. Bricht
der Build ab, nennt er die Fundstelle dann ist noch etwas offen.
6. Restliche Checkliste in `docs/HANDOVER.md`, Abschnitt 9.

128
docs/SUBDOMAIN-HERCAREER.md Normal file
View File

@@ -0,0 +1,128 @@
# Subdomain `her-career.anelia-wolf.com`
Stand: 07.09.2026 · Zuständig: webklar (Hoster-Konfiguration)
Auf der Rückwandgrafik am Messestand steht unten links ein QR-Code, beschriftet
„Messe-Special · Code scannen“. Er zeigt auf:
```
https://her-career.anelia-wolf.com
```
**Diese Adresse ist gedruckt.** Sie lässt sich nicht mehr ändern, und sie muss am
22.10.2026 funktionieren inklusive gültigem Zertifikat. Ein Scan, der auf einer
Zertifikatswarnung landet, ist schlimmer als kein Code.
Die Datei dahinter ist `out/her-career/index.html` aus dem normalen Build
(`npm run build`). Es gibt **keinen zweiten Build und kein zweites Deployment**
die Subdomain zeigt in denselben Docroot wie die Hauptdomain.
## Was einzurichten ist
1. **DNS:** `her-career` als A- (und AAAA-)Record auf dieselbe IP wie
`anelia-wolf.com`, oder als CNAME auf den Hauptdomain-Namen.
2. **Zertifikat:** Die Subdomain muss im Zertifikat stehen als weiterer
SAN-Eintrag (`certbot ... -d anelia-wolf.com -d www.anelia-wolf.com -d
her-career.anelia-wolf.com`) oder über ein Wildcard-Zertifikat.
3. **Vhost:** Derselbe Docroot, drei Regeln (unten). Kurz gesagt: `/` liefert die
Messeseite, ihre Dateien kommen aus demselben Verzeichnis, jede andere Adresse
wandert per 301 auf die Hauptdomain.
Der dritte Punkt ist der wichtige. Ohne ihn läge die **komplette Website** unter
zwei Hostnamen jede Seite doppelt, mit doppelten Adressen in der Suche. Die
Messeseite trägt zwar einen Canonical auf `https://anelia-wolf.com/her-career/`
(gesetzt in `src/app/(de)/her-career/page.tsx`), der die Kopie zusammenfasst; die
übrigen Seiten tun das für ihre Subdomain-Zwillinge nicht.
## nginx
```nginx
server {
listen 443 ssl;
http2 on;
server_name her-career.anelia-wolf.com;
ssl_certificate /etc/letsencrypt/live/anelia-wolf.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/anelia-wolf.com/privkey.pem;
# Derselbe Docroot wie die Hauptdomain: der hochgeladene Inhalt von out/.
root /var/www/anelia-wolf.com;
# „/“ ist die Messeseite.
location = / {
try_files /her-career/index.html =404;
}
# Was die Seite selbst lädt, kommt aus demselben Verzeichnis.
# (Leere Blöcke erben root und liefern die Datei aus kein Redirect.)
location /_next/ { }
location /brand/ { }
location /photos/ { }
location = /favicon.ico { }
location = /icon.svg { }
location = /apple-icon.png { }
# Alles andere gehört auf die Hauptdomain.
location / {
return 301 https://anelia-wolf.com$request_uri;
}
}
# Port 80 nur zum Hochreichen auf HTTPS.
server {
listen 80;
server_name her-career.anelia-wolf.com;
return 301 https://her-career.anelia-wolf.com$request_uri;
}
```
## Apache
```apache
<VirtualHost *:443>
ServerName her-career.anelia-wolf.com
DocumentRoot /var/www/anelia-wolf.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/anelia-wolf.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/anelia-wolf.com/privkey.pem
RewriteEngine On
# „/“ ist die Messeseite (intern, ohne Redirect die Adresse bleibt stehen).
RewriteRule ^/?$ /her-career/index.html [L]
# Dateien der Seite bleiben hier.
RewriteRule ^/(_next|brand|photos)/ - [L]
RewriteRule ^/(favicon\.ico|icon\.svg|apple-icon\.png)$ - [L]
# Alles andere auf die Hauptdomain.
RewriteRule ^/(.*)$ https://anelia-wolf.com/$1 [R=301,L]
</VirtualHost>
```
## Wenn der Hoster keine Rewrites kann
Dann die ganze Subdomain per 301 auf `https://anelia-wolf.com/her-career/`
umleiten. Der QR-Code funktioniert weiterhin der Browser folgt der Umleitung,
ohne dass jemand etwas merkt; in der Adresszeile steht danach die Hauptdomain.
Das ist die schlechtere, aber jederzeit funktionierende Variante.
## Prüfen (nach dem Ausrollen, vor der Messe)
```bash
curl -sI https://her-career.anelia-wolf.com/ | head -1
curl -s https://her-career.anelia-wolf.com/ | grep -o '<link rel="canonical"[^>]*>'
curl -sI https://her-career.anelia-wolf.com/impressum/ | head -2
```
Erwartet: `200`, ein Canonical auf `https://anelia-wolf.com/her-career/`, und für
`/impressum/` ein `301` auf `https://anelia-wolf.com/impressum/`.
Dazu einmal mit dem Handy den echten, gedruckten Code scannen nicht den aus der
PDF-Vorschau, sondern den auf dem gelieferten Druck.
## Nach der Messe
Ab dem 24.10.2026 steht in `src/lib/site.ts` `flags.showHerCareer` auf `false`
(siehe `docs/HANDOVER.md`, Abschnitt 6). Die Seite bleibt erreichbar gedruckte
Codes laufen weiter, und Anelias Rückwand wird nicht am Tag danach eingestampft ,
verliert aber die Termin-Angaben und den Eintrag in der Sitemap. Der Vhost bleibt,
wie er ist.

View File

@@ -0,0 +1,33 @@
import type { Metadata } from "next";
import { HerCareerBody } from "@/components/pages/HerCareerBody";
import { herCareer } from "@/content/her-career";
import { alternatesFor, OG_LOCALE } from "@/lib/i18n";
import { flags, site } from "@/lib/site";
/* Solange die Messe bevorsteht, nennt die Beschreibung Stand und Uhrzeit; danach
(`flags.showHerCareer: false`) die Fassung ohne Termine eine Suchergebnis-Zeile,
die einen vergangenen Termin ankündigt, ist schlechter als gar keine Zeile. */
const description = flags.showHerCareer ? herCareer.meta.descriptionMesse : herCareer.meta.description;
/**
* Ziel des QR-Codes auf der Rückwandgrafik. Der Canonical zeigt bewusst auf
* anelia-wolf.com/her-career/: Dieselbe Datei wird auch unter
* her-career.anelia-wolf.com ausgeliefert, und ohne diese Angabe stünde die Seite
* unter zwei Adressen im Index (docs/SUBDOMAIN-HERCAREER.md).
*/
export const metadata: Metadata = {
title: herCareer.meta.title,
description,
alternates: alternatesFor("de", "herCareer"),
openGraph: {
title: `${herCareer.meta.title} ${site.name}`,
description,
url: alternatesFor("de", "herCareer").canonical,
locale: OG_LOCALE.de,
},
};
/** /her-career/ Aufbau in <HerCareerBody/>, Texte in src/content/her-career.ts. */
export default function HerCareerPage() {
return <HerCareerBody content={herCareer} />;
}

View File

@@ -0,0 +1,27 @@
import type { Metadata } from "next";
import { HerCareerBody } from "@/components/pages/HerCareerBody";
import { herCareerEn } from "@/content/en/her-career";
import { alternatesFor, OG_LOCALE } from "@/lib/i18n";
import { flags, site } from "@/lib/site";
/* Solange die Messe bevorsteht, nennt die Beschreibung Stand und Uhrzeit; danach
(`flags.showHerCareer: false`) die Fassung ohne Termine eine Suchergebnis-Zeile,
die einen vergangenen Termin ankündigt, ist schlechter als gar keine Zeile. */
const description = flags.showHerCareer ? herCareerEn.meta.descriptionMesse : herCareerEn.meta.description;
export const metadata: Metadata = {
title: herCareerEn.meta.title,
description,
alternates: alternatesFor("en", "herCareer"),
openGraph: {
title: `${herCareerEn.meta.title} ${site.name}`,
description,
url: alternatesFor("en", "herCareer").canonical,
locale: OG_LOCALE.en,
},
};
/** /en/her-career/ layout in <HerCareerBody/>, copy in src/content/en/her-career.ts. */
export default function EnHerCareerPage() {
return <HerCareerBody content={herCareerEn} />;
}

View File

@@ -82,6 +82,12 @@
--breakpoint-wide: 90rem; --breakpoint-wide: 90rem;
} }
/* „Kurze“ Ansicht: Telefon im Hochformat mit sichtbarer Adressleiste (iPhone SE 667 px,
viele Androids 640 px) und jedes Telefon im Querformat. Das Overlay-Menü rückt dort
enger zusammen, sonst fällt sein Fuß (Telefonnummer, E-Mail, Rechtslinks) unter die
Kante. Höhe statt Breite, weil das Problem die Höhe ist. */
@custom-variant short (@media (max-height: 47.5rem));
@theme inline { @theme inline {
--color-ink: var(--ink); --color-ink: var(--ink);
--color-ink-2: var(--ink-2); --color-ink-2: var(--ink-2);
@@ -182,6 +188,17 @@ body:has(main > [data-theme="light"]:first-child) header:not([data-scrolled]) {
--line: var(--line-light); --line: var(--line-light);
} }
/* Messeseite (/her-career/): Dort trägt der Kopf keinen CTA die eine Aufforderung
der Seite ist das Formular im Kontaktblock, und der Knopf im Kopf zeigte als
einziges Element von der Messeseite weg (auf den Kontaktblock der Startseite).
Der Kopf liegt außerhalb von <main> und kennt die Route nicht; auf der Subdomain
her-career.anelia-wolf.com ist der Pfad ohnehin „/“. Deshalb erkennt die Regel die
Seite an ihrem eigenen Abschnitt „Messe-Special“ dessen id ist zugleich das Ziel
des Folio-Links im Hero (src/content/her-career.ts). */
body:has(main #messe-special) header [data-header-cta] {
display: none;
}
/* Legal pages (Spec A/D6): „Datenschutzerklärung“ is one 20-character word and /* Legal pages (Spec A/D6): „Datenschutzerklärung“ is one 20-character word and
at 44 px it is wider than the 375 px column it widened the whole document. at 44 px it is wider than the 375 px column it widened the whole document.
Scoped here because the rule belongs to src/components/legal/LegalPage.tsx, Scoped here because the rule belongs to src/components/legal/LegalPage.tsx,

View File

@@ -1,17 +1,27 @@
import type { MetadataRoute } from "next"; import type { MetadataRoute } from "next";
import { LANGS, routesByLang, type Lang, type PageKey } from "@/lib/i18n"; import { LANGS, routesByLang, type Lang, type PageKey } from "@/lib/i18n";
import { site } from "@/lib/site"; import { flags, site } from "@/lib/site";
/** Static export: metadata routes are prerendered; mark explicitly (Next static-exports guide). */ /** Static export: metadata routes are prerendered; mark explicitly (Next static-exports guide). */
export const dynamic = "force-static"; export const dynamic = "force-static";
type ChangeFrequency = NonNullable<MetadataRoute.Sitemap[number]["changeFrequency"]>; type ChangeFrequency = NonNullable<MetadataRoute.Sitemap[number]["changeFrequency"]>;
const pages: ReadonlyArray<{ page: PageKey; changeFrequency: ChangeFrequency; priority: number }> = [ type PageEntry = { page: PageKey; changeFrequency: ChangeFrequency; priority: number };
/* Die Messe-Landingpage steht nur in der Sitemap, solange die Messe bevorsteht.
Gefunden wird sie ohnehin über den gedruckten QR-Code, nicht über die Suche
nach dem Event wäre sie ein Eintrag, der auf ein vergangenes Datum zeigt. */
const herCareerPages: ReadonlyArray<PageEntry> = flags.showHerCareer
? [{ page: "herCareer", changeFrequency: "monthly", priority: 0.5 }]
: [];
const pages: ReadonlyArray<PageEntry> = [
{ page: "home", changeFrequency: "monthly", priority: 1 }, { page: "home", changeFrequency: "monthly", priority: 1 },
{ page: "coaching", changeFrequency: "monthly", priority: 0.8 }, { page: "coaching", changeFrequency: "monthly", priority: 0.8 },
{ page: "trainings", changeFrequency: "monthly", priority: 0.8 }, { page: "trainings", changeFrequency: "monthly", priority: 0.8 },
{ page: "keynotes", changeFrequency: "monthly", priority: 0.8 }, { page: "keynotes", changeFrequency: "monthly", priority: 0.8 },
...herCareerPages,
{ page: "impressum", changeFrequency: "yearly", priority: 0.2 }, { page: "impressum", changeFrequency: "yearly", priority: 0.2 },
{ page: "datenschutz", changeFrequency: "yearly", priority: 0.2 }, { page: "datenschutz", changeFrequency: "yearly", priority: 0.2 },
]; ];

View File

@@ -27,7 +27,11 @@ export function Wordmark({ size = "nav", href = "/", className, mark, chrome = c
<TransitionLink <TransitionLink
href={href} href={href}
aria-label={chrome.wordmark.ariaLabel} aria-label={chrome.wordmark.ariaLabel}
className={cn("wordmark group inline-flex max-w-full items-center", footer ? "gap-4 sm:gap-6" : "gap-4", className)} className={cn(
"wordmark group inline-flex max-w-full items-center",
footer ? "gap-4 sm:gap-6" : "gap-3 sm:gap-4",
className,
)}
> >
{mark && ( {mark && (
<span <span
@@ -40,12 +44,19 @@ export function Wordmark({ size = "nav", href = "/", className, mark, chrome = c
<span <span
className={cn( className={cn(
"wordmark__text flex flex-col", "wordmark__text flex flex-col",
footer ? "min-w-0 text-[clamp(1.25rem,0.75rem+1.8vw,2.75rem)]" : "text-[1.0625rem]", /* Unter 640 px eine Nummer kleiner: „ANELIA WOLF“ ist mit 17 px und 0.32em
Laufweite 158 px breit und lief damit schon immer über seine sechs Spalten
hinaus sichtbar wurde das erst, als rechts daneben der Sprachknopf dazukam
(vorher lief es in leeren Raum). Der Monogramm-Kopf bleibt 28 px. */
footer ? "min-w-0 text-[clamp(1.25rem,0.75rem+1.8vw,2.75rem)]" : "text-[0.9375rem] sm:text-[1.0625rem]",
)} )}
> >
<span <span
className={cn( className={cn(
"wordmark__name whitespace-nowrap pl-[0.32em] font-sans text-[1em] font-light uppercase leading-none tracking-[0.32em]", "wordmark__name whitespace-nowrap pl-[0.32em] font-sans text-[1em] font-light uppercase leading-none",
/* Die Laufweite trägt den größten Teil der Breite (10 Lücken × 0.32em). Im Kopf
wird sie unter 640 px enger; im Fuß bleibt sie, dort ist die volle Breite da. */
footer ? "tracking-[0.32em]" : "tracking-[0.22em] sm:tracking-[0.32em]",
"text-fg", "text-fg",
)} )}
> >

View File

@@ -25,9 +25,11 @@ export interface ProcessContactProps {
content?: HomeContent["contact"]; content?: HomeContent["contact"];
/** Ziel des Datenschutz-Links im Formular. */ /** Ziel des Datenschutz-Links im Formular. */
privacyHref?: string; privacyHref?: string;
/** Nenner des Abschnittszählers („04 / 04“). Die Startseite zählt ohne Nenner. */
total?: string;
} }
export function ProcessContact({ content: contact = contactDe, privacyHref }: ProcessContactProps = {}) { export function ProcessContact({ content: contact = contactDe, privacyHref, total }: ProcessContactProps = {}) {
return ( return (
<div data-theme="dark" className="relative bg-transparent text-fg"> <div data-theme="dark" className="relative bg-transparent text-fg">
<Rule variant="gold" /> <Rule variant="gold" />
@@ -35,6 +37,7 @@ export function ProcessContact({ content: contact = contactDe, privacyHref }: Pr
id={contact.id} id={contact.id}
theme="dark" theme="dark"
numeral={contact.numeral} numeral={contact.numeral}
total={total}
label={contact.eyebrow} label={contact.eyebrow}
className="pt-[clamp(5rem,11vh,9rem)] pb-[clamp(3.5rem,7vh,6rem)] md:pt-[clamp(8rem,18vh,15rem)] md:pb-[clamp(5rem,10vh,8rem)]" className="pt-[clamp(5rem,11vh,9rem)] pb-[clamp(3.5rem,7vh,6rem)] md:pt-[clamp(8rem,18vh,15rem)] md:pb-[clamp(5rem,10vh,8rem)]"
> >

View File

@@ -129,10 +129,17 @@ export function Header({ mark, chrome = chromeDe, nav = navDe, legal = legalDe,
</nav> </nav>
<div className="col-span-6 flex items-center justify-end gap-6 xl:col-span-4 2xl:gap-8"> <div className="col-span-6 flex items-center justify-end gap-6 xl:col-span-4 2xl:gap-8">
{/* Steht an der Stelle, an der bis zum 06.09.2026 „Kontakt“ stand. */} {/* Steht an der Stelle, an der bis zum 06.09.2026 „Kontakt“ stand.
Zwei Formen, weil neben Wortmarke und „Menü“ auf einem Telefon kein Paar
Platz hat: unter 1024 px nur die Zielsprache als ein Knopf, darüber beide.
Im Overlay-Menü steht er nicht mehr dort fiel er auf kurzen Geräten
unter die Kante, ohne dass man ihm ansah, dass da noch etwas kommt. */}
{flags.showLangSwitch && <LangSwitch lang={lang} variant="compact" className="lg:hidden" />}
{flags.showLangSwitch && <LangSwitch lang={lang} className="hidden lg:flex" />} {flags.showLangSwitch && <LangSwitch lang={lang} className="hidden lg:flex" />}
<a <a
data-menu-inert="" data-menu-inert=""
/* Haken für globals.css: Auf der Messeseite entfällt dieser Knopf. */
data-header-cta=""
href={chrome.header.cta.href} href={chrome.header.cta.href}
className="hidden whitespace-nowrap border border-gold bg-[image:var(--gold-sheen)] bg-[length:250%_100%] bg-[position:0%_50%] px-[1.4em] py-[1.05em] font-sans text-[0.75rem] font-medium uppercase leading-none tracking-[0.16em] text-ink transition-[background-position] duration-[600ms] ease-[var(--ease-out-quart)] hover:bg-[position:100%_50%] focus-visible:bg-[position:100%_50%] xl:inline-block 2xl:px-[1.6em] 2xl:tracking-[0.2em]" className="hidden whitespace-nowrap border border-gold bg-[image:var(--gold-sheen)] bg-[length:250%_100%] bg-[position:0%_50%] px-[1.4em] py-[1.05em] font-sans text-[0.75rem] font-medium uppercase leading-none tracking-[0.16em] text-ink transition-[background-position] duration-[600ms] ease-[var(--ease-out-quart)] hover:bg-[position:100%_50%] focus-visible:bg-[position:100%_50%] xl:inline-block 2xl:px-[1.6em] 2xl:tracking-[0.2em]"
> >
@@ -161,7 +168,7 @@ export function Header({ mark, chrome = chromeDe, nav = navDe, legal = legalDe,
</div> </div>
</Container> </Container>
</header> </header>
<OverlayMenu open={open} onClose={() => setOpen(false)} chrome={chrome} legal={legal} lang={lang} /> <OverlayMenu open={open} onClose={() => setOpen(false)} chrome={chrome} legal={legal} />
</> </>
); );
} }

View File

@@ -5,27 +5,57 @@ import { cn } from "@/lib/utils";
import { counterpart, langSwitch, otherLang, type Lang } from "@/lib/i18n"; import { counterpart, langSwitch, otherLang, type Lang } from "@/lib/i18n";
import { TransitionLink } from "@/components/motion/TransitionLink"; import { TransitionLink } from "@/components/motion/TransitionLink";
export interface LangSwitchProps {
lang: Lang;
className?: string;
/**
* `pair` zeigt beide Sprachen nebeneinander (Platz ab 1024 px).
* `compact` zeigt nur die andere Sprache als einen Knopf ein Tipp, ein Wechsel.
* Auf dem Telefon ist das die einzige Form, die neben Wortmarke und „Menü“ passt.
*/
variant?: "pair" | "compact";
}
const BASE =
"inline-flex items-center gap-1.5 font-mono text-[11px] uppercase tracking-[0.14em] transition-colors duration-300";
/** /**
* Sprachumschalter im Kopf. Steht an der Stelle, an der vorher „Kontakt“ stand. * Sprachumschalter im Kopf. Steht an der Stelle, an der vorher „Kontakt“ stand.
* *
* Zeigt beide Sprachen: die aktive als stiller Zustand, die andere als Link auf das * Zielt auf das Gegenstück derselben Seite (`counterpart()`), nicht pauschal auf die
* Gegenstück derselben Seite (`counterpart()`), nicht pauschal auf die Startseite. * Startseite.
* *
* Zur Flagge: Eine Flagge steht für ein Land, nicht für eine Sprache deshalb steht * Zur Flagge: Eine Flagge steht für ein Land, nicht für eine Sprache deshalb steht
* neben ihr immer das Sprachkürzel, und der zugängliche Name nennt die Sprache * neben ihr immer das Sprachkürzel, und der zugängliche Name nennt die Sprache
* ausgeschrieben. Wer die Flaggen weglassen will, entfernt hier zwei Zeilen. * ausgeschrieben. Wer die Flaggen weglassen will, entfernt hier zwei Zeilen.
*/ */
export function LangSwitch({ lang, className }: { lang: Lang; className?: string }) { export function LangSwitch({ lang, className, variant = "pair" }: LangSwitchProps) {
const pathname = usePathname() ?? "/"; const pathname = usePathname() ?? "/";
const other = otherLang(lang); const other = otherLang(lang);
const href = counterpart(pathname, other); const href = counterpart(pathname, other);
const base = /* Kompakt: Die aktive Sprache steht nicht da sie steht ja im Dokument. Gezeigt wird,
"inline-flex items-center gap-1.5 font-mono text-[11px] uppercase tracking-[0.14em] transition-colors duration-300"; wohin es geht. Die negativen Ränder heben das Tippziel auf 54 × 42 px, ohne die
Grundlinie oder den Abstand zum Nachbarn zu verschieben (WCAG 2.5.8: 24 × 24 px
Minimum, 44 × 44 px ist die Empfehlung die Breite von 38 px lag darunter). */
if (variant === "compact") {
return (
<TransitionLink
href={href}
label={langSwitch[other].label}
hrefLang={other}
aria-label={langSwitch[other].title}
className={cn(BASE, "-mx-2 -my-3 shrink-0 px-2 py-3 text-fg", className)}
>
<Flag lang={other} />
{langSwitch[other].short}
</TransitionLink>
);
}
return ( return (
<div className={cn("flex items-center gap-2", className)}> <div className={cn("flex items-center gap-2", className)}>
<span className={cn(base, "text-accent")} aria-current="true" title={langSwitch[lang].title}> <span className={cn(BASE, "text-accent")} aria-current="true" title={langSwitch[lang].title}>
<Flag lang={lang} /> <Flag lang={lang} />
{langSwitch[lang].short} {langSwitch[lang].short}
</span> </span>
@@ -35,7 +65,7 @@ export function LangSwitch({ lang, className }: { lang: Lang; className?: string
label={langSwitch[other].label} label={langSwitch[other].label}
hrefLang={other} hrefLang={other}
aria-label={langSwitch[other].title} aria-label={langSwitch[other].title}
className={cn(base, "-my-3 py-3 text-fg-muted hover:text-accent focus-visible:text-accent")} className={cn(BASE, "-my-3 py-3 text-fg-muted hover:text-accent focus-visible:text-accent")}
> >
<Flag lang={other} /> <Flag lang={other} />
{langSwitch[other].short} {langSwitch[other].short}

View File

@@ -2,9 +2,7 @@
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { flags, legal as legalDe, site } from "@/lib/site"; import { legal as legalDe, site } from "@/lib/site";
import type { Lang } from "@/lib/i18n";
import { LangSwitch } from "./LangSwitch";
import { chrome as chromeDe, type Chrome } from "@/content/chrome"; import { chrome as chromeDe, type Chrome } from "@/content/chrome";
import { gsap, EASE, MM, getLenis, refreshSoon } from "@/lib/gsap"; import { gsap, EASE, MM, getLenis, refreshSoon } from "@/lib/gsap";
import { TransitionLink } from "@/components/motion/TransitionLink"; import { TransitionLink } from "@/components/motion/TransitionLink";
@@ -17,8 +15,6 @@ export interface OverlayMenuProps {
chrome?: Chrome; chrome?: Chrome;
/** Impressum/Datenschutz der jeweiligen Sprache. */ /** Impressum/Datenschutz der jeweiligen Sprache. */
legal?: readonly { href: string; label: string }[]; legal?: readonly { href: string; label: string }[];
/** Aktuelle Sprache für den Umschalter im Fuß des Menüs. */
lang?: Lang;
} }
/** The header's „Menü“ button part of the dialog's operable set (see the trap below). */ /** The header's „Menü“ button part of the dialog's operable set (see the trap below). */
@@ -34,7 +30,7 @@ export const MENU_TOGGLE_ID = "menue-toggle";
* as well as by mouse. Every other header control is `inert` while the sheet is * as well as by mouse. Every other header control is `inert` while the sheet is
* open, so the mouse-operable and the keyboard-operable set are the same one. * open, so the mouse-operable and the keyboard-operable set are the same one.
*/ */
export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe, lang = "de" }: OverlayMenuProps) { export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe }: OverlayMenuProps) {
const panel = useRef<HTMLDivElement>(null); const panel = useRef<HTMLDivElement>(null);
const pathname = usePathname(); const pathname = usePathname();
const onCloseRef = useRef(onClose); const onCloseRef = useRef(onClose);
@@ -168,7 +164,9 @@ export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe,
data-lenis-prevent="" data-lenis-prevent=""
className="fixed inset-0 z-40 flex flex-col overflow-y-auto overscroll-contain bg-ivory text-ink" className="fixed inset-0 z-40 flex flex-col overflow-y-auto overscroll-contain bg-ivory text-ink"
> >
<Container className="flex min-h-full flex-1 flex-col pb-10 pt-[calc(var(--header-h,60px)+24px)]"> {/* short: = kurze Ansicht (≤ 760 px hoch). Ohne das Engersetzen fällt der Fuß
(Telefonnummer, E-Mail, Rechtslinks) auf einem iPhone SE unter die Kante. */}
<Container className="flex min-h-full flex-1 flex-col pb-10 pt-[calc(var(--header-h,60px)+24px)] short:pb-6 short:pt-[calc(var(--header-h,60px)+12px)]">
<ol className="border-t border-line-light"> <ol className="border-t border-line-light">
{chrome.menu.items.map((item) => ( {chrome.menu.items.map((item) => (
<li key={item.href} className="overflow-hidden border-b border-line-light"> <li key={item.href} className="overflow-hidden border-b border-line-light">
@@ -177,7 +175,7 @@ export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe,
href={item.href} href={item.href}
label={item.label} label={item.label}
onClick={() => onCloseRef.current()} onClick={() => onCloseRef.current()}
className="group flex items-baseline gap-5 py-5 lg:gap-8 lg:py-7" className="group flex items-baseline gap-5 py-5 short:py-3 lg:gap-8 lg:py-7"
> >
<span className="font-mono text-xs tracking-[0.06em] tabular-nums text-gold-deep">{item.numeral}</span> <span className="font-mono text-xs tracking-[0.06em] tabular-nums text-gold-deep">{item.numeral}</span>
<span className="font-display text-[clamp(2rem,1.4rem+3vw,4rem)] leading-none text-ink transition-[font-style] group-hover:italic"> <span className="font-display text-[clamp(2rem,1.4rem+3vw,4rem)] leading-none text-ink transition-[font-style] group-hover:italic">
@@ -197,7 +195,7 @@ export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe,
<div <div
data-menu-foot="" data-menu-foot=""
className="mt-auto flex flex-col gap-4 pt-12 lg:flex-row lg:items-end lg:justify-between" className="mt-auto flex flex-col gap-4 pt-12 short:gap-3 short:pt-6 lg:flex-row lg:items-end lg:justify-between"
> >
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<a href={site.phoneHref} className="font-display text-[1.75rem] leading-none text-ink"> <a href={site.phoneHref} className="font-display text-[1.75rem] leading-none text-ink">
@@ -208,10 +206,6 @@ export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe,
</a> </a>
</div> </div>
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 font-mono text-xs tracking-[0.06em] text-grey-deep"> <div className="flex flex-wrap items-center gap-x-6 gap-y-2 font-mono text-xs tracking-[0.06em] text-grey-deep">
{/* Unter 1024 px versteckt der Kopf den Umschalter (dort ist kein Platz neben
der Wortmarke). Ohne diese Zeile gäbe es auf dem Telefon gar keinen Weg in
die andere Sprache er steht deshalb genau dort, wo er im Kopf fehlt. */}
{flags.showLangSwitch && <LangSwitch lang={lang} className="lg:hidden" />}
<a href={site.linkedin} target="_blank" rel="noopener noreferrer" className="hover:text-gold-deep"> <a href={site.linkedin} target="_blank" rel="noopener noreferrer" className="hover:text-gold-deep">
{chrome.menu.linkedin} <span aria-hidden="true"></span> {chrome.menu.linkedin} <span aria-hidden="true"></span>
</a> </a>

View File

@@ -0,0 +1,78 @@
import { Section } from "@/components/layout/Section";
import { ServiceHero } from "@/components/services/ServiceHero";
import { ServiceLayout } from "@/components/services/ServiceLayout";
import { SectionHeading } from "@/components/services/SectionHeading";
import { ProcessContact } from "@/components/home/ProcessContact";
import { Facts } from "@/components/ui/Facts";
import { Ledger } from "@/components/ui/Ledger";
import { Reveal } from "@/components/motion/Reveal";
import type { HerCareerContent } from "@/content/her-career";
import { flags } from "@/lib/site";
/** Content column of every light section: cols 312 on desktop as on the service pages. */
const BODY = "col-span-12 lg:col-span-10 lg:col-start-3";
/** Heading column: cols 38. */
const HEAD = "col-span-12 lg:col-span-6 lg:col-start-3";
export interface HerCareerBodyProps {
content: HerCareerContent;
}
/**
* Der Rumpf von /her-career/ bzw. /en/her-career/ die Messe-Landingpage hinter dem
* QR-Code der Rückwandgrafik (Adresse: her-career.anelia-wolf.com, Auslieferung:
* docs/SUBDOMAIN-HERCAREER.md).
*
* Kurz gehalten, weil hier jemand mit dem Handy am Stand steht: Hero (ink) →
* 01 Messe-Special → 02 Themen → 03 Auf der Messe (nur `flags.showHerCareer`) →
* Kontakt-Band (ink). Alle weiterführenden Links liegen als absolute Adressen in
* der Inhaltsdatei und führen auf die Hauptdomain. Kein <main> das rendert
* template.tsx.
*/
export function HerCareerBody({ content }: HerCareerBodyProps) {
const { special, themen, messe, contact } = content;
const showFair = flags.showHerCareer;
/* Der Kontaktblock zählt mit er ist der letzte nummerierte Abschnitt der Seite. */
const total = showFair ? "04" : "03";
return (
<>
{/* Ohne Fakten-Stapel: der Hero trägt Headline, Subline und den einen Knopf. */}
<ServiceHero content={content} />
<ServiceLayout>
{/* 01 Messe-Special der Grund, warum jemand den Code gescannt hat. */}
<Section id={special.id} theme="light" numeral="01" total={total} label={special.eyebrow}>
<SectionHeading eyebrow={special.eyebrow} h2={special.h2} className={HEAD} />
<Reveal className={`${BODY} mt-12 lg:mt-16`}>
<Facts variant="table" title={special.title} items={special.rows} />
</Reveal>
</Section>
{/* 02 Themen die vier Blöcke der Rückwand, verlinkt auf die Hauptdomain. */}
<Section id={themen.id} theme="light" numeral="02" total={total} label={themen.eyebrow}>
<SectionHeading eyebrow={themen.eyebrow} h2={themen.h2} className={HEAD} />
<div className={`${BODY} mt-12 lg:mt-16`}>
<Ledger variant="toc" rows={themen.rows} />
</div>
</Section>
{/* 03 Auf der Messe derselbe Termine-Ledger wie auf der Startseite, eine Quelle. */}
{showFair && (
<Section id={messe.id} theme="light" numeral="03" total={total} label={messe.eyebrow}>
<SectionHeading eyebrow={messe.eyebrow} className={HEAD} />
<div className={`${BODY} mt-12 lg:mt-16`}>
<Ledger variant="termine" rows={messe.termine.rows} heading={messe.termine.heading} />
</div>
</Section>
)}
</ServiceLayout>
{/* Kontakt: derselbe Block wie Sektion 06 der Startseite, mit dem Formular. */}
<ProcessContact content={contact} privacyHref={content.privacyHref} total={total} />
</>
);
}
export default HerCareerBody;

View File

@@ -19,8 +19,10 @@ export interface ServiceHeroContent {
/** The italic word may also carry `.gold-text` (keynotes the page's one gradient word). */ /** The italic word may also carry `.gold-text` (keynotes the page's one gradient word). */
h1GoldEm?: boolean; h1GoldEm?: boolean;
subline: string; subline: string;
cta: Cta; /** Haupt-Knopf. Ohne Angabe trägt der Hero nur Headline und Subline. */
facts: readonly FactItem[]; cta?: Cta;
/** Fakten-Stapel oben rechts. Ohne Angabe (und ohne Foto) entfällt die Spalte. */
facts?: readonly FactItem[];
/** No fraction here the page's own section counter is the only counter (see <Folio/>). */ /** No fraction here the page's own section counter is the only counter (see <Folio/>). */
folio: { next?: { label: string; href: string } }; folio: { next?: { label: string; href: string } };
/** Photo alt / caption (keynote stage photo). */ /** Photo alt / caption (keynote stage photo). */
@@ -46,6 +48,9 @@ export function ServiceHero({ content, photo, className }: ServiceHeroProps) {
Angaben doppelt, und über der H1 stand faktisch die Überschrift ein zweites Mal. */ Angaben doppelt, und über der H1 stand faktisch die Überschrift ein zweites Mal. */
const { numeral, masthead, h1, h1GoldEm, subline, cta, facts, folio, alt, caption } = content; const { numeral, masthead, h1, h1GoldEm, subline, cta, facts, folio, alt, caption } = content;
const hasPhoto = Boolean(photo); const hasPhoto = Boolean(photo);
/* Die Messeseite trägt weder Fakten noch Foto dann gibt es die rechte Spalte
nicht, statt eine leere Reveal-Hülle in die Spalten 1012 zu stellen. */
const hasAside = hasPhoto || Boolean(facts?.length);
return ( return (
<section <section
@@ -80,20 +85,25 @@ export function ServiceHero({ content, photo, className }: ServiceHeroProps) {
<Reveal play="load" delay={1.0} as="p" className="mt-10 max-w-[40ch] font-sans text-xl font-light leading-[1.45] text-fg-muted"> <Reveal play="load" delay={1.0} as="p" className="mt-10 max-w-[40ch] font-sans text-xl font-light leading-[1.45] text-fg-muted">
{subline} {subline}
</Reveal> </Reveal>
<Reveal play="load" delay={1.2} className="mt-8"> {cta && (
<Button {...cta} /> <Reveal play="load" delay={1.2} className="mt-8">
</Reveal> <Button {...cta} />
</Reveal>
)}
</div> </div>
{hasAside && (
<div <div
className={cn( className={cn(
"col-span-12 flex flex-col gap-10", "col-span-12 flex flex-col gap-10",
hasPhoto ? "lg:col-span-5 lg:col-start-8" : "lg:col-span-3 lg:col-start-10", hasPhoto ? "lg:col-span-5 lg:col-start-8" : "lg:col-span-3 lg:col-start-10",
)} )}
> >
<Reveal play="load" delay={0.6} className={cn(hasPhoto && "lg:self-end")}> {facts && facts.length > 0 && (
<Facts items={facts} /> <Reveal play="load" delay={0.6} className={cn(hasPhoto && "lg:self-end")}>
</Reveal> <Facts items={facts} />
</Reveal>
)}
{photo && ( {photo && (
<Portrait <Portrait
src={photo} src={photo}
@@ -106,6 +116,7 @@ export function ServiceHero({ content, photo, className }: ServiceHeroProps) {
/> />
)} )}
</div> </div>
)}
</div> </div>
<Folio next={folio.next} play="load" delay={1.3} /> <Folio next={folio.next} play="load" delay={1.3} />

View File

@@ -4,7 +4,7 @@ import { GRID } from "@/components/layout/Section";
import { Lines } from "@/components/motion/Lines"; import { Lines } from "@/components/motion/Lines";
import { Reveal } from "@/components/motion/Reveal"; import { Reveal } from "@/components/motion/Reveal";
import { Rule } from "@/components/motion/Rule"; import { Rule } from "@/components/motion/Rule";
import type { Headline } from "@/content/home"; import type { Headline, HomeContent } from "@/content/home";
import { Button, type ButtonProps } from "./Button"; import { Button, type ButtonProps } from "./Button";
import { ContactLinks } from "./ContactLinks"; import { ContactLinks } from "./ContactLinks";
@@ -16,6 +16,8 @@ export interface CtaBandProps {
cta: ButtonProps; cta: ButtonProps;
/** Mono micro line under the button (falls back to `cta.micro`). */ /** Mono micro line under the button (falls back to `cta.micro`). */
micro?: string; micro?: string;
/** Kontaktzeilen in der Sprache der Seite. Ohne Angabe die deutsche Fassung. */
contact?: HomeContent["contact"]["links"];
id?: string; id?: string;
className?: string; className?: string;
} }
@@ -37,7 +39,7 @@ export function HeadlineText({ before, em, after }: Headline) {
* primary CTA. Paints its own ink ground; the seam to the ivory above is the * primary CTA. Paints its own ink ground; the seam to the ivory above is the
* gold hairline alone (spec §2.3) no fade, no gradient. * gold hairline alone (spec §2.3) no fade, no gradient.
*/ */
export function CtaBand({ h2, cta, micro, id = "kontakt", className }: CtaBandProps) { export function CtaBand({ h2, cta, micro, contact, id = "kontakt", className }: CtaBandProps) {
return ( return (
<section id={id} data-theme="dark" className={cn("relative text-fg", className)}> <section id={id} data-theme="dark" className={cn("relative text-fg", className)}>
<Rule variant="gold" className="relative z-[1]" /> <Rule variant="gold" className="relative z-[1]" />
@@ -55,7 +57,7 @@ export function CtaBand({ h2, cta, micro, id = "kontakt", className }: CtaBandPr
</Reveal> </Reveal>
</div> </div>
<div className="col-span-12 lg:col-start-9 lg:col-span-4"> <div className="col-span-12 lg:col-start-9 lg:col-span-4">
<ContactLinks /> <ContactLinks content={contact} />
</div> </div>
</div> </div>
</Container> </Container>

View File

@@ -0,0 +1,103 @@
/**
* English copy of /en/her-career/. Same shape as src/content/her-career.ts.
*
* The trade fair is German, the audience is not: herCAREER draws an international
* crowd, and Anelia speaks at the booth in both languages. The QR code on the
* stand wall points at the German page; the language switch in the header leads
* here. Wording of the brand terms follows Anelia's own English herCAREER
* profile: „WOLF Principle“, „Courage to Succeed!“.
*
* Placeholders keep their German wording on purpose they address Anelia, not
* the visitor, exactly as in the other English content files.
*/
import { site } from "@/lib/site";
import { routesByLang } from "@/lib/i18n";
import { assertNoPlaceholders, collectStrings, ph } from "@/lib/legal";
import type { HerCareerContent } from "@/content/her-career";
import { homeEn } from "./home";
const r = routesByLang.en;
/** Absolute address on the main domain see the German file for the why. */
const main = (path: string): string => `${site.url}${path}`;
export const herCareerEn: HerCareerContent = {
meta: {
title: "herCAREER Expo 2026",
description:
"Anelia Wolf at herCAREER Expo 2026: the fair special, the topics from the booth and the direct line to the keynote speaker, trainer and coach.",
descriptionMesse:
"Anelia Wolf at herCAREER Expo 2026 in Munich: stand P.10.2 in hall 3, keynote on 23 October at 10:30 in Auditorium 2. Fair special, topics and the direct line.",
},
numeral: "2026",
masthead: {
role: "Keynote Speaker · Trainer · Coach",
center: "herCAREER Expo 2026",
lang: "German & English",
},
eyebrow: "herCAREER Expo 2026 · MOC Munich",
h1: { before: "Good to have you ", em: "here", after: "." },
subline:
"You scanned the code on the stand wall. Here is what we talk about at the booth the fair special, my three topics and the shortest way to a conversation.",
folio: { next: { label: "Fair special", href: "#messe-special" } },
special: {
id: "messe-special",
eyebrow: "Fair special",
h2: { before: "Your advantage.", em: "", after: "" },
title: "The special",
rows: [
{ label: "The offer", value: ph("Messe-Special: Was bekommt, wer den Code scannt?") },
{ label: "Valid until", value: ph("Frist, z. B. Anfragen bis 30.11.2026") },
{ label: "How to claim", value: ph("Bedingung, z. B. Stichwort in der Anfrage") },
],
},
themen: {
id: "themen",
eyebrow: "Topics",
h2: { before: "What we talk about.", em: "", after: "" },
rows: [
{
numeral: "01",
title: "Coaching & Assessments",
oneLiner:
"Who am I, where do I stand, where am I going? A personality and behaviour assessment first, then the work in a pair.",
keywords: ["Potential assessment", "One-to-one coaching", "Career change", "Leadership"],
link: { label: "See coaching", href: main(r.coaching) },
},
{
numeral: "02",
title: "Training & Workshops",
oneLiner:
"B2B sales training: revenue growth through new business and existing customers, communication that fits the type across the team.",
keywords: ["New business", "Existing customers", "Team communication"],
link: { label: "See training", href: main(r.trainings) },
},
{
numeral: "03",
title: "Keynotes & Speaking",
oneLiner: "Talks for company events and conferences an impulse that stays with the room.",
keywords: ["WOLF Principle", "Courage to Succeed", "German & English"],
link: { label: "See keynotes", href: main(r.keynotes) },
},
],
},
messe: {
id: "auf-der-messe",
eyebrow: "At the fair",
termine: homeEn.termine,
},
privacyHref: main(r.datenschutz),
contact: {
...homeEn.contact,
numeral: "04",
h2: { before: "Let us keep ", em: "talking", after: "." },
form: { ...homeEn.contact.form, subject: "herCAREER Expo 2026", note: "", privacy: "" },
},
};
assertNoPlaceholders("src/content/en/her-career.ts", collectStrings(herCareerEn));

147
src/content/her-career.ts Normal file
View File

@@ -0,0 +1,147 @@
/**
* /her-career/ Messe-Landingpage zur herCAREER Expo 2026.
*
* Diese Seite ist das Ziel des QR-Codes auf der Rückwandgrafik am Stand
* (`AneliaWolf_Rueckwandgrafik_1980x1240`). Der Code zeigt auf
* `https://her-career.anelia-wolf.com` die Adresse ist gedruckt und damit
* unveränderlich; wie die Subdomain diese Datei ausliefert, steht in
* docs/SUBDOMAIN-HERCAREER.md.
*
* Wer hier landet, steht mit dem Handy am Stand. Deshalb ist die Seite kurz und
* beantwortet in dieser Reihenfolge: Wo bin ich, was war das Special, worüber
* reden wir, wie erreiche ich dich. Alle weiterführenden Links gehen ABSOLUT auf
* die Hauptdomain ein Klick, und der Besucher ist auf anelia-wolf.com.
*
* Die vier Themenblöcke der Rückwand (Analysen, Trainings, 1:1 Coaching,
* Keynotes) liegen hier auf den drei Leistungsseiten der Website, wie in der
* Navigation: „Coaching & Analysen“ trägt die Analysen mit.
*/
import { routes, site } from "@/lib/site";
import type { Widen } from "./widen";
import { assertNoPlaceholders, collectStrings, ph } from "@/lib/legal";
import type { FactItem, Headline, TocRow } from "./home";
import { contact as contactHome, termine } from "./home";
/**
* Absolute Adresse auf der Hauptdomain. Die Seite wird unter zwei Hosts
* ausgeliefert (anelia-wolf.com/her-career/ und her-career.anelia-wolf.com);
* ein relativer Link würde auf der Subdomain in deren eigenem Pfadraum landen.
*/
const main = (path: string): string => `${site.url}${path}`;
export const herCareer = {
meta: {
title: "herCAREER Expo 2026",
/** Ohne Terminangaben stimmt auch nach der Messe (`flags.showHerCareer: false`). */
description:
"Anelia Wolf zur herCAREER Expo 2026: das Messe-Special, die Themen vom Stand und der direkte Draht zur Keynote Speakerin, Trainerin und Coach.",
/** Mit Stand und Keynote-Termin. Nur solange die Messe bevorsteht die Route wählt. */
descriptionMesse:
"Anelia Wolf auf der herCAREER Expo 2026 im MOC München: Stand P.10.2 in Halle 3, Keynote am 23.10. um 10:30 im Auditorium 2. Messe-Special, Themen und der direkte Draht.",
},
numeral: "2026",
/* Wie auf den Leistungsseiten: steht in der Inhaltsdatei, wird im Hero nicht gerendert. */
masthead: {
role: "Keynote Speakerin · Trainerin · Coach",
center: "herCAREER Expo 2026",
lang: "Deutsch & Englisch",
},
eyebrow: "herCAREER Expo 2026 · MOC München",
h1: { before: "Schön, dass du ", em: "hier", after: " bist." } satisfies Headline,
subline:
"Du hast den Code an der Rückwand gescannt. Hier steht, worüber wir am Stand sprechen das Messe-Special, meine drei Themen und der kürzeste Weg zu einem Gespräch.",
/* Kein Knopf im Hero (Kundenwunsch, 07.09.2026): Die eine Aufforderung steht im
Kontakt-Band am Fuß der Seite dort mit Mail-Betreff „herCAREER Expo 2026“. */
/* Kein Fakten-Stapel im Hero (Kundenwunsch, 07.09.2026): Stand, Messetermin und
Themen standen dort ein zweites Mal Standnummer und Termin trägt der
Termine-Ledger weiter unten, die Themen der Abschnitt „Worüber wir reden“. */
folio: { next: { label: "Messe-Special", href: "#messe-special" } },
/* ---------- 01 Messe-Special (ivory) ---------- */
special: {
id: "messe-special",
eyebrow: "Messe-Special",
h2: { before: "Dein Vorteil.", em: "", after: "" } satisfies Headline,
title: "Das Special",
/* Wortlaut kommt von Anelia (docs/OFFENE-DATEN.md, Punkt 2.7). Bis dahin stehen die
Zeilen sichtbar als „[…]“ auf der Seite, und der Build-Guard hält den Launch-Flip an. */
rows: [
{ label: "Das Angebot", value: ph("Messe-Special: Was bekommt, wer den Code scannt?") },
{ label: "Gültig bis", value: ph("Frist, z. B. Anfragen bis 30.11.2026") },
{ label: "So einlösen", value: ph("Bedingung, z. B. Stichwort in der Anfrage") },
] satisfies readonly FactItem[],
},
/* ---------- 02 Themen (ivory) ---------- */
themen: {
id: "themen",
eyebrow: "Themen",
h2: { before: "Worüber wir reden.", em: "", after: "" } satisfies Headline,
rows: [
{
numeral: "01",
title: "Coaching & Analysen",
oneLiner:
"Wer bin ich, wo stehe ich, wo will ich hin? Persönlichkeits- und Verhaltensanalyse als Grundlage, dann die Arbeit zu zweit.",
keywords: ["Potenzialanalyse", "1:1 Coaching", "Neuorientierung", "Leadership"],
link: { label: "Zum Coaching", href: main(routes.coaching) },
},
{
numeral: "02",
title: "Trainings & Workshops",
oneLiner:
"B2B-Vertriebstraining: Umsatzwachstum durch Neukundengewinnung und Bestandskundenaufbau, typgerechte Kommunikation im Team.",
keywords: ["Neukundengewinnung", "Bestandskunden", "Teamkommunikation"],
link: { label: "Zu den Trainings", href: main(routes.trainings) },
},
{
numeral: "03",
title: "Keynotes & Speaking",
oneLiner:
"Inspirierende Vorträge für Firmen-Events und Veranstaltungen Impulse, die im Gedächtnis bleiben.",
keywords: ["WOLF-Prinzip", "Mut zum Erfolg", "Deutsch & Englisch"],
link: { label: "Zu den Keynotes", href: main(routes.keynotes) },
},
] satisfies readonly TocRow[],
},
/* ---------- 03 Auf der Messe (ivory) nur mit flags.showHerCareer ---------- */
messe: {
id: "auf-der-messe",
eyebrow: "Auf der Messe",
/* Eine Quelle für beide Seiten: derselbe Ledger steht auf der Startseite. */
termine,
},
/* ---------- 04 Kontakt (ink) ---------- */
/** Ziel des Datenschutz-Links im Formular absolut, wie jeder Link von hier weg. */
privacyHref: main(routes.datenschutz),
/**
* Derselbe Block wie Sektion 06 der Startseite: Formular links, Mail, Telefon und
* LinkedIn rechts. Feldbeschriftungen, Links und Abbinder kommen unverändert von
* dort eine Quelle. Eigen sind nur die Nummer, die Überschrift und der Betreff:
* Mit „herCAREER Expo 2026“ ist im Postfach ohne Nachfrage zu sehen, welche
* Anfrage von der Messe kommt.
*/
contact: {
...contactHome,
numeral: "04",
h2: { before: "Lass uns ", em: "weiterreden", after: "." } satisfies Headline,
/* Hinweiszeile und Datenschutz-Link entfallen hier (Kundenwunsch, 07.09.2026);
die Rückmeldung nach dem Absenden („opened“) bleibt. Beide Rechtstexte stehen
weiter im Fuß jeder Seite. */
form: { ...contactHome.form, subject: "herCAREER Expo 2026", note: "", privacy: "" },
},
} as const;
/* Build-Guard: der Launch-Flip (`flags.showPlaceholders: false`) darf keine sichtbaren
„[…]“ übrig lassen. Offene Punkte stehen in CONTENT_TODO (src/lib/legal.ts). */
assertNoPlaceholders("src/content/her-career.ts", collectStrings(herCareer));
/** Struktur, nicht Wortlaut die englische Fassung erfüllt denselben Typ. */
export type HerCareerContent = Widen<typeof herCareer>;

View File

@@ -22,6 +22,8 @@ export interface LangRoutes {
keynotes: string; keynotes: string;
impressum: string; impressum: string;
datenschutz: string; datenschutz: string;
/** Messe-Landingpage, Ziel des QR-Codes auf der Rückwandgrafik. */
herCareer: string;
/** Abschnitt 06 Ziel des Haupt-CTA. */ /** Abschnitt 06 Ziel des Haupt-CTA. */
kontakt: string; kontakt: string;
} }
@@ -34,6 +36,7 @@ export const routesByLang: Record<Lang, LangRoutes> = {
keynotes: "/leistungen/keynotes/", keynotes: "/leistungen/keynotes/",
impressum: "/impressum/", impressum: "/impressum/",
datenschutz: "/datenschutz/", datenschutz: "/datenschutz/",
herCareer: "/her-career/",
kontakt: "/#kontakt", kontakt: "/#kontakt",
}, },
en: { en: {
@@ -43,6 +46,7 @@ export const routesByLang: Record<Lang, LangRoutes> = {
keynotes: "/en/services/keynotes/", keynotes: "/en/services/keynotes/",
impressum: "/en/imprint/", impressum: "/en/imprint/",
datenschutz: "/en/privacy/", datenschutz: "/en/privacy/",
herCareer: "/en/her-career/",
kontakt: "/en/#kontakt", kontakt: "/en/#kontakt",
}, },
}; };
@@ -73,6 +77,7 @@ export function counterpart(pathname: string, target: Lang): string {
[from.keynotes, to.keynotes], [from.keynotes, to.keynotes],
[from.impressum, to.impressum], [from.impressum, to.impressum],
[from.datenschutz, to.datenschutz], [from.datenschutz, to.datenschutz],
[from.herCareer, to.herCareer],
[from.home, to.home], [from.home, to.home],
]; ];
const hit = pairs.find(([a]) => clean === a); const hit = pairs.find(([a]) => clean === a);

View File

@@ -106,4 +106,5 @@ export const CONTENT_TODO: string[] = [
"Coaching, Rahmen-Tabelle: Dauer und Paket eintragen (src/content/coaching.ts).", "Coaching, Rahmen-Tabelle: Dauer und Paket eintragen (src/content/coaching.ts).",
"Trainings, Rahmen-Tabelle (Hero und Rahmen-Abschnitt): Gruppengröße eintragen (src/content/trainings.ts).", "Trainings, Rahmen-Tabelle (Hero und Rahmen-Abschnitt): Gruppengröße eintragen (src/content/trainings.ts).",
"Trainings, Modul „Neukundengewinnung“: Umfang der Trainingsreihe eintragen (src/content/trainings.ts).", "Trainings, Modul „Neukundengewinnung“: Umfang der Trainingsreihe eintragen (src/content/trainings.ts).",
"Messe-Landingpage /her-career/: Angebot, Frist und Bedingung des Messe-Specials eintragen in beiden Sprachen (src/content/her-career.ts, src/content/en/her-career.ts). Der QR-Code auf der Rückwandgrafik führt genau auf diesen Block.",
]; ];

View File

@@ -15,6 +15,12 @@ export const site = {
linkedin: "https://www.linkedin.com/in/anelia-wolf-a26071280", linkedin: "https://www.linkedin.com/in/anelia-wolf-a26071280",
/** herCAREER Expo (Termine-Ledger, Footer-Textlink). Kein Logo, nur Textlink. */ /** herCAREER Expo (Termine-Ledger, Footer-Textlink). Kein Logo, nur Textlink. */
hercareer: "https://www.her-career.com/", hercareer: "https://www.her-career.com/",
/**
* Messe-Landingpage. Diese Adresse steht als QR-Code auf der Rückwandgrafik am
* Stand sie ist gedruckt und damit unveränderlich. Der Vhost der Subdomain
* liefert dafür `out/her-career/index.html` aus (docs/SUBDOMAIN-HERCAREER.md).
*/
herCareerLanding: "https://her-career.anelia-wolf.com",
description: description:
"Anelia Wolf Keynote Speakerin, Trainerin und Coach für Persönlichkeit, Wirkung und Vertrieb. Sales Mentoring & Potential Development nach dem WOLF-Prinzip.", "Anelia Wolf Keynote Speakerin, Trainerin und Coach für Persönlichkeit, Wirkung und Vertrieb. Sales Mentoring & Potential Development nach dem WOLF-Prinzip.",
} as const; } as const;
@@ -50,6 +56,13 @@ export const flags = {
show24hPromise: false, show24hPromise: false,
showIndustry: false, showIndustry: false,
showTwins: true, showTwins: true,
/**
* Messe-Landingpage /her-career/ (QR-Code der Rückwandgrafik). `true`, solange die
* herCAREER Expo bevorsteht: nur dann trägt die Seite die Termin-Zeilen und steht
* in der Sitemap. Ab dem 24.10.2026 auf `false` dann fallen die Datumsangaben
* weg, und der Rest der Seite bleibt richtig (docs/HANDOVER.md, Abschnitt 6).
*/
showHerCareer: true,
/** Sprachumschalter im Kopf. Seit dem 06.09.2026 steht die englische Fassung unter /** Sprachumschalter im Kopf. Seit dem 06.09.2026 steht die englische Fassung unter
* /en/ (eigenes Wurzel-Layout, eigene Slugs) die Flagge zeigt also irgendwohin. */ * /en/ (eigenes Wurzel-Layout, eigene Slugs) die Flagge zeigt also irgendwohin. */
showLangSwitch: true, showLangSwitch: true,
@@ -64,6 +77,8 @@ export const routes = {
keynotes: "/leistungen/keynotes/", keynotes: "/leistungen/keynotes/",
impressum: "/impressum/", impressum: "/impressum/",
datenschutz: "/datenschutz/", datenschutz: "/datenschutz/",
/** Messe-Landingpage, Ziel des QR-Codes auf der Rückwandgrafik. */
herCareer: "/her-career/",
/** Abschnitt 06 der Startseite Ziel des Haupt-CTA „Kennenlerngespräch anfragen“. */ /** Abschnitt 06 der Startseite Ziel des Haupt-CTA „Kennenlerngespräch anfragen“. */
kontakt: "/#kontakt", kontakt: "/#kontakt",
} as const; } as const;