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:
knsoandClaude Opus 5 committed 2026-09-07 22:39:32 +02:00
1 parent b9e1d4d15c
commit 33ec0df7e3
21 files changed
+941 -50

No files matched your search

+33
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} />;
}
+27
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} />;
}
+17
View File
@@ -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,
+12 -2
View File
@@ -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 },
];
+14 -3
View File
@@ -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",
)}
>
+4 -1
View File
@@ -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)]"
>
+9 -2
View File
@@ -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} />
</>
);
}
+37 -7
View File
@@ -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}
+7 -13
View File
@@ -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
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 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
View File
@@ -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} />
+5 -3
View File
@@ -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
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
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>;
+5
View File
@@ -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);
+1
View File
@@ -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
View File
@@ -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;