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:
@@ -1,8 +1,9 @@
|
||||
# Anelia Wolf – Website
|
||||
|
||||
Statische Website für Anelia Wolf (Keynote Speakerin, Trainerin, Coach).
|
||||
Zwölf Routen in zwei Sprachen: Startseite, drei Leistungsseiten, Impressum und
|
||||
Datenschutz – deutsch auf `/`, englisch unter `/en/` – plus die 404.
|
||||
Vierzehn Routen in zwei Sprachen: Startseite, drei Leistungsseiten, die Messeseite
|
||||
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.
|
||||
Kein Backend, keine Datenbank, keine Formulare.
|
||||
@@ -56,6 +57,7 @@ mit ändern.
|
||||
| `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) |
|
||||
| `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
|
||||
|
||||
@@ -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 |
|
||||
| `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 |
|
||||
| `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
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ Adressen, Englisch liegt vollstaendig unter `/en/`:
|
||||
| `/leistungen/keynotes/` | Keynotes & Speaking |
|
||||
| `/impressum/` | Impressum |
|
||||
| `/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 |
|
||||
| `/en/` | English start page |
|
||||
| `/en/services/coaching/` | Coaching & Assessments |
|
||||
@@ -33,6 +34,7 @@ Adressen, Englisch liegt vollstaendig unter `/en/`:
|
||||
| `/en/services/keynotes/` | Keynotes & Speaking |
|
||||
| `/en/imprint/` | Imprint (Übersetzung, verbindlich bleibt `/impressum/`) |
|
||||
| `/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`.
|
||||
|
||||
@@ -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,
|
||||
hreflang und der Sprachumschalter lesen alle aus dieser Tabelle.
|
||||
|
||||
**Umschalter.** Die beiden Flaggen im Kopf (`LangSwitch.tsx`) fuehren nicht
|
||||
pauschal auf die Startseite, sondern auf das Gegenstueck derselben Seite
|
||||
(`counterpart()`). Unter 1024 px ist im Kopf kein Platz; dort steht der
|
||||
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
|
||||
zugaengliche Name nennt die Sprache ausgeschrieben.
|
||||
**Umschalter.** Der Umschalter (`LangSwitch.tsx`) steht auf jeder Breite im Kopf
|
||||
und fuehrt nicht pauschal auf die Startseite, sondern auf das Gegenstueck
|
||||
derselben Seite (`counterpart()`). Zwei Formen:
|
||||
|
||||
- ab 1024 px beide Sprachen nebeneinander, die aktive still in Gold,
|
||||
- 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">`,
|
||||
`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
|
||||
ä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)
|
||||
@@ -330,6 +348,7 @@ Freigaben vorliegen. Kein Flag verlangt eine Codeänderung an Komponenten.
|
||||
| `show24hPromise` | `false` | Zusage „Antwort innerhalb von 24 Stunden“ |
|
||||
| `showIndustry` | `false` | Branchennennung im Nebentätigkeits-Kontext |
|
||||
| `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. |
|
||||
|
||||
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
|
||||
„Keynote · herCAREER Expo 2026 · 23.10. · 10:30“) und
|
||||
`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
|
||||
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 |
|
||||
| `npm run lint` (ESLint) | fehlerfrei |
|
||||
| `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) |
|
||||
| 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**. |
|
||||
@@ -530,7 +556,8 @@ Produktions-Build aus `out/` ist er nicht vorhanden.
|
||||
- [ ] Freigegebene Blöcke per Flag eingeschaltet (FAQ, Stimmen, Speaker-PDF …)
|
||||
- [ ] `hostOutsideEu` korrekt gesetzt, AV-Verträge mit Hoster und E-Mail-Anbieter
|
||||
- [ ] 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
|
||||
- [ ] Kalendereintrag für den 24.10.2026 (Termine-Block, Abschnitt 6)
|
||||
|
||||
|
||||
239
docs/OFFENE-DATEN.md
Normal file
239
docs/OFFENE-DATEN.md
Normal 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 7–14) – 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.7–2.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
128
docs/SUBDOMAIN-HERCAREER.md
Normal 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.
|
||||
33
src/app/(de)/her-career/page.tsx
Normal file
33
src/app/(de)/her-career/page.tsx
Normal 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} />;
|
||||
}
|
||||
27
src/app/en/her-career/page.tsx
Normal file
27
src/app/en/her-career/page.tsx
Normal 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} />;
|
||||
}
|
||||
@@ -82,6 +82,12 @@
|
||||
--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 {
|
||||
--color-ink: var(--ink);
|
||||
--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);
|
||||
}
|
||||
|
||||
/* 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
|
||||
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,
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
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). */
|
||||
export const dynamic = "force-static";
|
||||
|
||||
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: "coaching", changeFrequency: "monthly", priority: 0.8 },
|
||||
{ page: "trainings", changeFrequency: "monthly", priority: 0.8 },
|
||||
{ page: "keynotes", changeFrequency: "monthly", priority: 0.8 },
|
||||
...herCareerPages,
|
||||
{ page: "impressum", changeFrequency: "yearly", priority: 0.2 },
|
||||
{ page: "datenschutz", changeFrequency: "yearly", priority: 0.2 },
|
||||
];
|
||||
|
||||
@@ -27,7 +27,11 @@ export function Wordmark({ size = "nav", href = "/", className, mark, chrome = c
|
||||
<TransitionLink
|
||||
href={href}
|
||||
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 && (
|
||||
<span
|
||||
@@ -40,12 +44,19 @@ export function Wordmark({ size = "nav", href = "/", className, mark, chrome = c
|
||||
<span
|
||||
className={cn(
|
||||
"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
|
||||
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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -25,9 +25,11 @@ export interface ProcessContactProps {
|
||||
content?: HomeContent["contact"];
|
||||
/** Ziel des Datenschutz-Links im Formular. */
|
||||
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 (
|
||||
<div data-theme="dark" className="relative bg-transparent text-fg">
|
||||
<Rule variant="gold" />
|
||||
@@ -35,6 +37,7 @@ export function ProcessContact({ content: contact = contactDe, privacyHref }: Pr
|
||||
id={contact.id}
|
||||
theme="dark"
|
||||
numeral={contact.numeral}
|
||||
total={total}
|
||||
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)]"
|
||||
>
|
||||
|
||||
@@ -129,10 +129,17 @@ export function Header({ mark, chrome = chromeDe, nav = navDe, legal = legalDe,
|
||||
</nav>
|
||||
|
||||
<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" />}
|
||||
<a
|
||||
data-menu-inert=""
|
||||
/* Haken für globals.css: Auf der Messeseite entfällt dieser Knopf. */
|
||||
data-header-cta=""
|
||||
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]"
|
||||
>
|
||||
@@ -161,7 +168,7 @@ export function Header({ mark, chrome = chromeDe, nav = navDe, legal = legalDe,
|
||||
</div>
|
||||
</Container>
|
||||
</header>
|
||||
<OverlayMenu open={open} onClose={() => setOpen(false)} chrome={chrome} legal={legal} lang={lang} />
|
||||
<OverlayMenu open={open} onClose={() => setOpen(false)} chrome={chrome} legal={legal} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,27 +5,57 @@ import { cn } from "@/lib/utils";
|
||||
import { counterpart, langSwitch, otherLang, type Lang } from "@/lib/i18n";
|
||||
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.
|
||||
*
|
||||
* Zeigt beide Sprachen: die aktive als stiller Zustand, die andere als Link auf das
|
||||
* Gegenstück derselben Seite (`counterpart()`), nicht pauschal auf die Startseite.
|
||||
* Zielt auf das Gegenstück derselben Seite (`counterpart()`), nicht pauschal auf die
|
||||
* Startseite.
|
||||
*
|
||||
* 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
|
||||
* 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 other = otherLang(lang);
|
||||
const href = counterpart(pathname, other);
|
||||
|
||||
const base =
|
||||
"inline-flex items-center gap-1.5 font-mono text-[11px] uppercase tracking-[0.14em] transition-colors duration-300";
|
||||
/* Kompakt: Die aktive Sprache steht nicht da – sie steht ja im Dokument. Gezeigt wird,
|
||||
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 (
|
||||
<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} />
|
||||
{langSwitch[lang].short}
|
||||
</span>
|
||||
@@ -35,7 +65,7 @@ export function LangSwitch({ lang, className }: { lang: Lang; className?: string
|
||||
label={langSwitch[other].label}
|
||||
hrefLang={other}
|
||||
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} />
|
||||
{langSwitch[other].short}
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { flags, legal as legalDe, site } from "@/lib/site";
|
||||
import type { Lang } from "@/lib/i18n";
|
||||
import { LangSwitch } from "./LangSwitch";
|
||||
import { legal as legalDe, site } from "@/lib/site";
|
||||
import { chrome as chromeDe, type Chrome } from "@/content/chrome";
|
||||
import { gsap, EASE, MM, getLenis, refreshSoon } from "@/lib/gsap";
|
||||
import { TransitionLink } from "@/components/motion/TransitionLink";
|
||||
@@ -17,8 +15,6 @@ export interface OverlayMenuProps {
|
||||
chrome?: Chrome;
|
||||
/** Impressum/Datenschutz der jeweiligen Sprache. */
|
||||
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). */
|
||||
@@ -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
|
||||
* 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 pathname = usePathname();
|
||||
const onCloseRef = useRef(onClose);
|
||||
@@ -168,7 +164,9 @@ export function OverlayMenu({ open, onClose, chrome = chromeDe, legal = legalDe,
|
||||
data-lenis-prevent=""
|
||||
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">
|
||||
{chrome.menu.items.map((item) => (
|
||||
<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}
|
||||
label={item.label}
|
||||
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-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
|
||||
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">
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
{chrome.menu.linkedin} <span aria-hidden="true">↗</span>
|
||||
</a>
|
||||
|
||||
78
src/components/pages/HerCareerBody.tsx
Normal file
78
src/components/pages/HerCareerBody.tsx
Normal 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 3–12 on desktop – as on the service pages. */
|
||||
const BODY = "col-span-12 lg:col-span-10 lg:col-start-3";
|
||||
/** Heading column: cols 3–8. */
|
||||
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;
|
||||
@@ -19,8 +19,10 @@ export interface ServiceHeroContent {
|
||||
/** The italic word may also carry `.gold-text` (keynotes – the page's one gradient word). */
|
||||
h1GoldEm?: boolean;
|
||||
subline: string;
|
||||
cta: Cta;
|
||||
facts: readonly FactItem[];
|
||||
/** Haupt-Knopf. Ohne Angabe trägt der Hero nur Headline und Subline. */
|
||||
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/>). */
|
||||
folio: { next?: { label: string; href: string } };
|
||||
/** 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. */
|
||||
const { numeral, masthead, h1, h1GoldEm, subline, cta, facts, folio, alt, caption } = content;
|
||||
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 10–12 zu stellen. */
|
||||
const hasAside = hasPhoto || Boolean(facts?.length);
|
||||
|
||||
return (
|
||||
<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">
|
||||
{subline}
|
||||
</Reveal>
|
||||
<Reveal play="load" delay={1.2} className="mt-8">
|
||||
<Button {...cta} />
|
||||
</Reveal>
|
||||
{cta && (
|
||||
<Reveal play="load" delay={1.2} className="mt-8">
|
||||
<Button {...cta} />
|
||||
</Reveal>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasAside && (
|
||||
<div
|
||||
className={cn(
|
||||
"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",
|
||||
)}
|
||||
>
|
||||
<Reveal play="load" delay={0.6} className={cn(hasPhoto && "lg:self-end")}>
|
||||
<Facts items={facts} />
|
||||
</Reveal>
|
||||
{facts && facts.length > 0 && (
|
||||
<Reveal play="load" delay={0.6} className={cn(hasPhoto && "lg:self-end")}>
|
||||
<Facts items={facts} />
|
||||
</Reveal>
|
||||
)}
|
||||
{photo && (
|
||||
<Portrait
|
||||
src={photo}
|
||||
@@ -106,6 +116,7 @@ export function ServiceHero({ content, photo, className }: ServiceHeroProps) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Folio next={folio.next} play="load" delay={1.3} />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { GRID } from "@/components/layout/Section";
|
||||
import { Lines } from "@/components/motion/Lines";
|
||||
import { Reveal } from "@/components/motion/Reveal";
|
||||
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 { ContactLinks } from "./ContactLinks";
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface CtaBandProps {
|
||||
cta: ButtonProps;
|
||||
/** Mono micro line under the button (falls back to `cta.micro`). */
|
||||
micro?: string;
|
||||
/** Kontaktzeilen in der Sprache der Seite. Ohne Angabe die deutsche Fassung. */
|
||||
contact?: HomeContent["contact"]["links"];
|
||||
id?: 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
|
||||
* 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 (
|
||||
<section id={id} data-theme="dark" className={cn("relative text-fg", className)}>
|
||||
<Rule variant="gold" className="relative z-[1]" />
|
||||
@@ -55,7 +57,7 @@ export function CtaBand({ h2, cta, micro, id = "kontakt", className }: CtaBandPr
|
||||
</Reveal>
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-start-9 lg:col-span-4">
|
||||
<ContactLinks />
|
||||
<ContactLinks content={contact} />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
103
src/content/en/her-career.ts
Normal file
103
src/content/en/her-career.ts
Normal 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
147
src/content/her-career.ts
Normal 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>;
|
||||
@@ -22,6 +22,8 @@ export interface LangRoutes {
|
||||
keynotes: string;
|
||||
impressum: string;
|
||||
datenschutz: string;
|
||||
/** Messe-Landingpage, Ziel des QR-Codes auf der Rückwandgrafik. */
|
||||
herCareer: string;
|
||||
/** Abschnitt 06 – Ziel des Haupt-CTA. */
|
||||
kontakt: string;
|
||||
}
|
||||
@@ -34,6 +36,7 @@ export const routesByLang: Record<Lang, LangRoutes> = {
|
||||
keynotes: "/leistungen/keynotes/",
|
||||
impressum: "/impressum/",
|
||||
datenschutz: "/datenschutz/",
|
||||
herCareer: "/her-career/",
|
||||
kontakt: "/#kontakt",
|
||||
},
|
||||
en: {
|
||||
@@ -43,6 +46,7 @@ export const routesByLang: Record<Lang, LangRoutes> = {
|
||||
keynotes: "/en/services/keynotes/",
|
||||
impressum: "/en/imprint/",
|
||||
datenschutz: "/en/privacy/",
|
||||
herCareer: "/en/her-career/",
|
||||
kontakt: "/en/#kontakt",
|
||||
},
|
||||
};
|
||||
@@ -73,6 +77,7 @@ export function counterpart(pathname: string, target: Lang): string {
|
||||
[from.keynotes, to.keynotes],
|
||||
[from.impressum, to.impressum],
|
||||
[from.datenschutz, to.datenschutz],
|
||||
[from.herCareer, to.herCareer],
|
||||
[from.home, to.home],
|
||||
];
|
||||
const hit = pairs.find(([a]) => clean === a);
|
||||
|
||||
@@ -106,4 +106,5 @@ export const CONTENT_TODO: string[] = [
|
||||
"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, 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.",
|
||||
];
|
||||
|
||||
@@ -15,6 +15,12 @@ export const site = {
|
||||
linkedin: "https://www.linkedin.com/in/anelia-wolf-a26071280",
|
||||
/** herCAREER Expo (Termine-Ledger, Footer-Textlink). Kein Logo, nur Textlink. */
|
||||
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:
|
||||
"Anelia Wolf – Keynote Speakerin, Trainerin und Coach für Persönlichkeit, Wirkung und Vertrieb. Sales Mentoring & Potential Development nach dem WOLF-Prinzip.",
|
||||
} as const;
|
||||
@@ -50,6 +56,13 @@ export const flags = {
|
||||
show24hPromise: false,
|
||||
showIndustry: false,
|
||||
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
|
||||
* /en/ (eigenes Wurzel-Layout, eigene Slugs) – die Flagge zeigt also irgendwohin. */
|
||||
showLangSwitch: true,
|
||||
@@ -64,6 +77,8 @@ export const routes = {
|
||||
keynotes: "/leistungen/keynotes/",
|
||||
impressum: "/impressum/",
|
||||
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“. */
|
||||
kontakt: "/#kontakt",
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user