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 parent
b9e1d4d15c
commit
33ec0df7e3
21 files changed
+941
-50
No files matched your search
@@ -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} />;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
+12
-2
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
@@ -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