Messeseite: „EN“ führt zur englischen Messeseite, nicht zur Startseite
Auf her-career.anelia-wolf.com liegt die Seite unter „/“. usePathname() meldet dort „/“, und counterpart() leitete daraus folgerichtig die englische STARTSEITE ab – wer am Stand gescannt und dann auf „EN“ getippt hat, landete auf anelia-wolf.com/en/. Der Umschalter fragt jetzt den hreflang-Verweis im Kopf des Dokuments: Der hängt an der Seite, nicht an der Adresse, unter der sie ausgeliefert wird. Am selben Wurzelproblem: Der Sprachwechsel ist wegen der zwei Wurzel-Layouts ohnehin ein neues Dokument. Den macht jetzt der Browser statt router.push(), und über die Sprachgrenze wird nicht mehr vorgeladen – auf der Subdomain scheiterte der Vorabruf an CORS, mehrfach pro Seitenaufbau. Dazu zwei Sachen am Kopf der Messeseite: Das Monogramm steht hinter dem Schlusspunkt der Begrüßung, 0,783 em hoch (die Oberlänge der Playfair Display, im Browser gemessen) und auf der Grundlinie – und dafür nicht mehr in der Kopfzeile, wo es sonst zweimal auf einem Bildschirm stünde. Es steht dort dekorativ, sonst wandert der Text seines <title> über SplitText ins aria-label der Überschrift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ecf66c7340
commit
a37e48e4ef
8 files changed
+202
-23
No files matched your search
+18
-11
@@ -218,18 +218,25 @@ body:has(main #messe-special) header [data-collab] {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* Unter 640 px steht der Zusatz unter der Wortmarke, und dort trägt der Kopf kein
|
||||
Monogramm mehr: Mit ihm begann „ANELIA WOLF“ um 48 px eingerückt, während das
|
||||
Malzeichen darunter an der Seitenkante saß – zwei Zeilen, die an keiner Kante
|
||||
fluchteten. Ohne Monogramm beginnen beide links auf derselben Linie und lesen
|
||||
sich als eine Marke. Betrifft nur den Kopf der Messeseite auf dem Telefon: Der
|
||||
Fuß behält sein Monogramm, alle anderen Seiten auch. */
|
||||
@media (max-width: 639px) {
|
||||
body:has(main #messe-special) header .wordmark__mark {
|
||||
display: none;
|
||||
}
|
||||
/* Auf der Messeseite trägt der Kopf kein Monogramm mehr (Kundenwunsch, 10.09.2026).
|
||||
Seit die Marke hinter dem Schlusspunkt der Begrüßung steht (`h1Mark` in
|
||||
src/components/services/ServiceHero.tsx), stand sie auf demselben Bildschirm
|
||||
zweimal – oben klein im Kopf, darunter groß in der Überschrift. Eine der beiden
|
||||
musste weg, und die große ist die, die gemeint ist.
|
||||
|
||||
/* Und die Laufweite von 0.22em auf 0.15em. „ANELIA WOLF“ hat zehn Buchstabenlücken;
|
||||
Bis dahin galt dieselbe Regel schon unter 640 px, aus einem zweiten Grund: Mit
|
||||
Monogramm begann „ANELIA WOLF“ um 48 px eingerückt, während das Malzeichen darunter
|
||||
an der Seitenkante saß – zwei Zeilen, die an keiner Kante fluchteten. Ohne Monogramm
|
||||
beginnen beide links auf derselben Linie und lesen sich als eine Marke.
|
||||
|
||||
Nur der Kopf der Messeseite: Der Fuß behält sein Monogramm, alle anderen Seiten auch.
|
||||
Die Hülle („Umblättern“) ebenfalls – sie liegt über der ganzen Seite, nicht in der Zeile. */
|
||||
body:has(main #messe-special) header .wordmark__mark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
/* Die Laufweite von 0.22em auf 0.15em. „ANELIA WOLF“ hat zehn Buchstabenlücken;
|
||||
bei 15 px sind das 33 px allein an Sperrung, und die fehlten, damit die Zeile
|
||||
„ANELIA WOLF × herCAREER“ neben Sprachumschalter und Menü auf 335 px passt.
|
||||
Nur hier – auf allen anderen Seiten und ab 640 px bleibt die Wortmarke, wie sie ist. */
|
||||
|
||||
@@ -25,6 +25,18 @@ export interface BrandMarkProps {
|
||||
handoff?: boolean;
|
||||
/** Accessible name. */
|
||||
title?: string;
|
||||
/**
|
||||
* Die Marke steht nur als Bild da, ohne Namen: kein `<title>`, kein `role`/`aria-label`,
|
||||
* stattdessen `aria-hidden`. Nötig überall dort, wo sie INNERHALB von Text sitzt – in der
|
||||
* H1 der Messeseite etwa. Zwei Gründe, beide handfest:
|
||||
*
|
||||
* 1. Der Name stünde mitten im Satz, den ein Screenreader vorliest.
|
||||
* 2. Der Text eines `<title>`-Elements zählt zum `textContent` seines Vorfahren, und
|
||||
* <Lines/> baut mit SplitText (`aria: "auto"`) daraus das aria-label der Überschrift.
|
||||
* Ohne diesen Schalter hieß sie „Schön, dass Sie gescannt haben. Anelia Wolf –
|
||||
* Monogramm“ – ein `aria-hidden` am umgebenden <span> ändert daran nichts.
|
||||
*/
|
||||
decorative?: boolean;
|
||||
}
|
||||
|
||||
const ID_CLASS: Record<string, string> = {
|
||||
@@ -40,7 +52,9 @@ function escapeAttr(s: string): string {
|
||||
|
||||
function prepareSvg(
|
||||
raw: string,
|
||||
{ className, idPrefix, variant, handoff, title }: Required<Pick<BrandMarkProps, "variant" | "handoff" | "title">> &
|
||||
{ className, idPrefix, variant, handoff, title, decorative }: Required<
|
||||
Pick<BrandMarkProps, "variant" | "handoff" | "title" | "decorative">
|
||||
> &
|
||||
Pick<BrandMarkProps, "className" | "idPrefix">,
|
||||
): string {
|
||||
/* The file comes from a non-technical client via public/brand/ and is injected with
|
||||
@@ -90,6 +104,10 @@ function prepareSvg(
|
||||
return `<path${a}${selfClose}>`;
|
||||
});
|
||||
|
||||
/* Der Name der Marke, wo sie keinen tragen soll – siehe `decorative`. Das <title>
|
||||
muss WEG, nicht nur versteckt: Es zählt zum textContent der Überschrift. */
|
||||
if (decorative) svg = svg.replace(/<title\b[\s\S]*?<\/title>/gi, "");
|
||||
|
||||
svg = svg.replace(/<svg\b([^>]*)>/, (_, attrs: string) => {
|
||||
let a = attrs.replace(/\s(width|height)="[^"]*"/g, "");
|
||||
if (/\bclass="/.test(a)) {
|
||||
@@ -97,6 +115,11 @@ function prepareSvg(
|
||||
} else if (className) {
|
||||
a += ` class="${className}"`;
|
||||
}
|
||||
if (decorative) {
|
||||
a = a.replace(/\srole="[^"]*"/gi, "").replace(/\saria-label="[^"]*"/gi, "");
|
||||
a += ' aria-hidden="true"';
|
||||
return `<svg${a}>`;
|
||||
}
|
||||
if (!/\brole=/.test(a)) a += ' role="img"';
|
||||
if (!/\baria-label=/.test(a)) a += ` aria-label="${escapeAttr(title)}"`;
|
||||
return `<svg${a}>`;
|
||||
@@ -111,16 +134,30 @@ export function BrandMark({
|
||||
variant = "full",
|
||||
handoff = false,
|
||||
title = "Anelia Wolf – Monogramm",
|
||||
decorative = false,
|
||||
}: BrandMarkProps) {
|
||||
const raw = getLogoSvg();
|
||||
if (raw) {
|
||||
const html = prepareSvg(raw, { className, idPrefix, variant, handoff, title });
|
||||
const html = prepareSvg(raw, { className, idPrefix, variant, handoff, title, decorative });
|
||||
return <span className="contents" dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
/* Ersatzmarke, wenn public/brand/logo-mark.svg fehlt. <WolfMark/> wird nicht angefasst
|
||||
(Projektregel), deshalb bekommt sie hier einen leeren Namen – dann steht in ihrem
|
||||
<title> kein Text, der in die Überschrift wandern könnte – und die Hülle nimmt sie
|
||||
aus dem Vorlesetext. */
|
||||
const fallbackTitle = decorative ? "" : title;
|
||||
const hide = decorative ? { "aria-hidden": true as const } : {};
|
||||
if (variant === "v-only") {
|
||||
return (
|
||||
<span className="contents [&_.wolfmark-wolf]:hidden">
|
||||
<WolfMark className={className} idPrefix={idPrefix} title={title} />
|
||||
<span className="contents [&_.wolfmark-wolf]:hidden" {...hide}>
|
||||
<WolfMark className={className} idPrefix={idPrefix} title={fallbackTitle} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (decorative) {
|
||||
return (
|
||||
<span className="contents" {...hide}>
|
||||
<WolfMark className={className} idPrefix={idPrefix} title={fallbackTitle} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { counterpart, langSwitch, otherLang, type Lang } from "@/lib/i18n";
|
||||
import { TransitionLink } from "@/components/motion/TransitionLink";
|
||||
@@ -19,11 +20,44 @@ export interface LangSwitchProps {
|
||||
const BASE =
|
||||
"inline-flex items-center gap-1.5 font-mono text-[11px] uppercase tracking-[0.14em] transition-colors duration-300";
|
||||
|
||||
/**
|
||||
* Der Pfad des Gegenstücks, wie ihn das Dokument SELBST nennt.
|
||||
*
|
||||
* `<link rel="alternate" hreflang="…">` steht im Kopf jeder zweisprachigen Seite
|
||||
* (`alternatesFor()` in src/lib/i18n.ts) und hängt an der Seite, nicht an der Adresse,
|
||||
* unter der sie ausgeliefert wird. Genau darin liegt der Unterschied auf der Messeseite:
|
||||
* her-career.anelia-wolf.com liefert dieselbe Datei unter „/“ aus
|
||||
* (docs/SUBDOMAIN-HERCAREER.md). `usePathname()` sagt dort „/“, und aus „/“ leitet
|
||||
* `counterpart()` folgerichtig die englische STARTSEITE ab – wer am Stand gescannt und
|
||||
* dann auf „EN“ getippt hat, landete auf anelia-wolf.com/en/ statt auf der englischen
|
||||
* Messeseite. Der hreflang-Verweis im Kopf derselben Datei zeigt dagegen auf
|
||||
* /en/her-career/, egal unter welcher Adresse sie liegt.
|
||||
*
|
||||
* Übernommen wird nur der PFAD: relativ bleibt der Link auf dem Host, auf dem er
|
||||
* angeklickt wurde, und die Subdomain schickt ihn mit ihrem 301 auf die Hauptdomain –
|
||||
* so, wie sie es mit jedem anderen Pfad auch tut. Die absolute Adresse zu übernehmen
|
||||
* würde in der Entwicklung auf die Live-Site zeigen.
|
||||
*/
|
||||
function alternatePath(target: Lang): string | null {
|
||||
const el = document.querySelector<HTMLLinkElement>(`link[rel="alternate"][hreflang="${target}"]`);
|
||||
if (!el) return null;
|
||||
try {
|
||||
const url = new URL(el.href, window.location.href);
|
||||
return `${url.pathname}${url.hash}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Der Kopf des Dokuments ändert sich nicht, solange das Dokument steht. */
|
||||
const noSubscribe = () => () => {};
|
||||
|
||||
/**
|
||||
* Sprachumschalter im Kopf. Steht an der Stelle, an der vorher „Kontakt“ stand.
|
||||
*
|
||||
* Zielt auf das Gegenstück derselben Seite (`counterpart()`), nicht pauschal auf die
|
||||
* Startseite.
|
||||
* Zielt auf das Gegenstück derselben Seite, nicht pauschal auf die Startseite: erst
|
||||
* `counterpart()` aus dem Pfad, nach dem ersten Rendern der hreflang-Verweis des
|
||||
* Dokuments (`alternatePath()` – die Begründung steht dort).
|
||||
*
|
||||
* 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
|
||||
@@ -32,7 +66,15 @@ const BASE =
|
||||
export function LangSwitch({ lang, className, variant = "pair" }: LangSwitchProps) {
|
||||
const pathname = usePathname() ?? "/";
|
||||
const other = otherLang(lang);
|
||||
const href = counterpart(pathname, other);
|
||||
/* Zwei Quellen, absichtlich: Beim Vorrendern gibt es kein Dokument, das man fragen
|
||||
könnte – da trägt der Pfad die Antwort. Im Browser steht der hreflang-Verweis im
|
||||
Kopf, und der weiß es auch dann noch, wenn der Pfad nichts mehr hergibt. Auf allen
|
||||
Adressen außer der Messe-Subdomain kommt bei beiden dasselbe heraus. */
|
||||
const href = useSyncExternalStore(
|
||||
noSubscribe,
|
||||
() => alternatePath(other) ?? counterpart(pathname, other),
|
||||
() => counterpart(pathname, other),
|
||||
);
|
||||
|
||||
/* 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
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { gsap, EASE } from "@/lib/gsap";
|
||||
import { WolfMark } from "@/components/brand/WolfMark";
|
||||
import { chrome as chromeDe, type Chrome } from "@/content/chrome";
|
||||
import { crossesLangRoot } from "@/lib/i18n";
|
||||
import { endTransition, setTransitionPhase, subscribe } from "./transition-store";
|
||||
|
||||
export interface CurtainProps {
|
||||
@@ -49,6 +50,13 @@ export function Curtain({ children, chrome = chromeDe }: CurtainProps) {
|
||||
onComplete: () => {
|
||||
phase.current = "pushed";
|
||||
setTransitionPhase("pushed");
|
||||
/* Sprachwechsel: den Dokumentwechsel macht der Browser, nicht der Router
|
||||
(Begründung in crossesLangRoot(), src/lib/i18n.ts). Die Hülle bleibt
|
||||
liegen, bis das neue Dokument da ist – sie gehört zu diesem hier. */
|
||||
if (crossesLangRoot(pathname, href)) {
|
||||
window.location.assign(href);
|
||||
return;
|
||||
}
|
||||
router.push(href, { scroll: false });
|
||||
window.clearTimeout(timer.current);
|
||||
timer.current = window.setTimeout(() => {
|
||||
@@ -57,7 +65,7 @@ export function Curtain({ children, chrome = chromeDe }: CurtainProps) {
|
||||
},
|
||||
});
|
||||
});
|
||||
}, [router, chrome.curtain.fallbackLabel]);
|
||||
}, [router, pathname, chrome.curtain.fallbackLabel]);
|
||||
|
||||
// Route changed → the sheet continues upward and resets.
|
||||
useEffect(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { ComponentProps, MouseEvent } from "react";
|
||||
import { getLenis, hasJs, prefersReducedMotion } from "@/lib/gsap";
|
||||
import { crossesLangRoot } from "@/lib/i18n";
|
||||
import { isTransitioning, startTransition } from "./transition-store";
|
||||
|
||||
type NextLinkProps = ComponentProps<typeof Link>;
|
||||
@@ -54,8 +55,13 @@ export function scrollToHash(hash: string, immediate = false) {
|
||||
* modifier/middle clicks, external targets, same-page anchors (smooth
|
||||
* scroll instead), reduced motion, and while a transition is running.
|
||||
*/
|
||||
export function TransitionLink({ href, label, onClick, scroll, ...rest }: TransitionLinkProps) {
|
||||
export function TransitionLink({ href, label, onClick, scroll, prefetch, ...rest }: TransitionLinkProps) {
|
||||
const pathname = usePathname();
|
||||
/* Für einen Sprachwechsel gibt es keine Route zum Vorladen – er ist ein neues
|
||||
Dokument (crossesLangRoot(), src/lib/i18n.ts). Auf her-career.anelia-wolf.com
|
||||
scheitert der Vorabruf zusätzlich an der Herkunft: Der Pfad geht per 301 auf die
|
||||
Hauptdomain, und der Abruf stirbt an CORS – mehrfach pro Seitenaufbau. */
|
||||
const crossesLang = crossesLangRoot(pathname, href);
|
||||
|
||||
const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
||||
onClick?.(e);
|
||||
@@ -79,13 +85,27 @@ export function TransitionLink({ href, label, onClick, scroll, ...rest }: Transi
|
||||
return;
|
||||
}
|
||||
|
||||
/* Sprachwechsel: immer ein neues Dokument, nie ein Router-Sprung (Begründung in
|
||||
crossesLangRoot(), src/lib/i18n.ts). Mit Bewegung legt sich erst die Hülle
|
||||
darüber – den Wechsel selbst macht dann <Curtain/>; ohne Bewegung geht es
|
||||
direkt. Beides ist ein echter Seitenaufbau, kein router.push(). */
|
||||
if (crossesLang) {
|
||||
e.preventDefault();
|
||||
if (hasJs() && !prefersReducedMotion()) {
|
||||
startTransition(href, label); // ein zweiter Klick läuft ins Leere – das ist gewollt
|
||||
return;
|
||||
}
|
||||
window.location.assign(href);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasJs() || prefersReducedMotion()) return; // plain client navigation
|
||||
e.preventDefault();
|
||||
if (isTransitioning()) return; // ignore double clicks
|
||||
startTransition(href, label);
|
||||
};
|
||||
|
||||
return <Link href={href} scroll={scroll ?? false} onClick={handleClick} {...rest} />;
|
||||
return <Link href={href} scroll={scroll ?? false} prefetch={crossesLang ? false : prefetch} onClick={handleClick} {...rest} />;
|
||||
}
|
||||
|
||||
export default TransitionLink;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BrandMark } from "@/components/brand/BrandMark";
|
||||
import { Section } from "@/components/layout/Section";
|
||||
import { ServiceHero } from "@/components/services/ServiceHero";
|
||||
import { ServiceLayout } from "@/components/services/ServiceLayout";
|
||||
@@ -56,7 +57,15 @@ export function HerCareerBody({ content }: HerCareerBodyProps) {
|
||||
darunter – wer den Code gescannt hat, sieht den Nachlass ohne einen Scroll an einer
|
||||
Sektionsüberschrift vorbei. Die Breite ist begrenzt: über die vollen neun Spalten
|
||||
gezogen wäre er kein Gutschein mehr, sondern ein Banner. */}
|
||||
<ServiceHero content={content}>
|
||||
<ServiceHero
|
||||
content={content}
|
||||
/* Das Monogramm steht hinter dem Schlusspunkt der Begrüßung – die einzige
|
||||
Stelle der Seite, an der Marke und Satz eine Zeile teilen. Größe und
|
||||
Grundlinie regelt <ServiceHero/>; `idPrefix`, weil dieselbe Marke auf der
|
||||
Seite noch dreimal vorkommt (Kopf, Hülle, Fuß) und die Verlaufs-ids sonst
|
||||
kollidieren. */
|
||||
h1Mark={<BrandMark idPrefix="hc-h1" decorative />}
|
||||
>
|
||||
<Coupon content={coupon} className="max-w-[34rem]" />
|
||||
</ServiceHero>
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@ export interface ServiceHeroProps {
|
||||
* zu scrollen.
|
||||
*/
|
||||
children?: ReactNode;
|
||||
/**
|
||||
* Steht als letztes Zeichen in der H1, hinter dem Schlusspunkt. Auf der Messeseite
|
||||
* hängt hier das Monogramm – die Begrüßung endet mit der Marke, zu der sie gehört.
|
||||
*/
|
||||
h1Mark?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -55,7 +60,7 @@ export interface ServiceHeroProps {
|
||||
* Fließtext gehalten hat – eine Stufe größer und über die Spalten 3–11 statt
|
||||
* 3–9. So trägt die Messeseite ihren Hero mit einem einzigen Satz.
|
||||
*/
|
||||
export function ServiceHero({ content, photo, children, className }: ServiceHeroProps) {
|
||||
export function ServiceHero({ content, photo, children, h1Mark, className }: ServiceHeroProps) {
|
||||
/* `masthead` und `eyebrow` stehen weiter in den Inhaltsdateien, werden aber seit
|
||||
dem 06.09.2026 nicht mehr gerendert (Kundenwunsch): die Kopfzeile trug dieselben
|
||||
Angaben doppelt, und über der H1 stand faktisch die Überschrift ein zweites Mal. */
|
||||
@@ -107,6 +112,28 @@ export function ServiceHero({ content, photo, children, className }: ServiceHero
|
||||
{h1.before}
|
||||
{h1.em && <em className={cn("italic", h1GoldEm && "gold-text")}>{h1.em}</em>}
|
||||
{h1.after}
|
||||
{h1Mark && (
|
||||
/* Auf der Grundlinie, hinter dem Punkt, 0,783 em hoch: Das ist die
|
||||
Oberlänge der Playfair Display (im Browser an „h“ und „b“ gemessen;
|
||||
die Versalhöhe liegt bei 0,719 em) – die Marke wird damit genau so
|
||||
hoch, wie die Schrift dieser Zeile maximal wird, und keinen Punkt
|
||||
höher. Die viewBox der Marke sitzt eng auf der Zeichnung, deshalb
|
||||
ist Kastenhöhe gleich sichtbare Höhe.
|
||||
|
||||
Zwischen Punkt und Marke steht KEIN Leerzeichen im Markup – der
|
||||
Abstand kommt aus `ml`, damit an dieser Stelle keine Umbruchstelle
|
||||
entsteht und die Marke nicht allein in eine letzte Zeile rutscht.
|
||||
|
||||
Dekorativ (`aria-hidden`): Die Marke steht im Kopf schon mit Namen.
|
||||
In der Überschrift stünde er ein zweites Mal – und zwar mitten im
|
||||
Satz, den ein Screenreader vorliest. */
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="ml-[0.22em] inline-block align-baseline [&_svg]:h-[0.783em] [&_svg]:w-auto"
|
||||
>
|
||||
{h1Mark}
|
||||
</span>
|
||||
)}
|
||||
</Lines>
|
||||
{subline && (
|
||||
<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">
|
||||
|
||||
@@ -62,6 +62,35 @@ export function otherLang(lang: Lang): Lang {
|
||||
return lang === "de" ? "en" : "de";
|
||||
}
|
||||
|
||||
/**
|
||||
* Zu welcher Sprachfassung gehört ein Pfad? Alles unter `/en/` ist Englisch, alles
|
||||
* andere Deutsch – dieselbe Grenze, die `app/en/` von der Gruppe `app/(de)/` trennt.
|
||||
*/
|
||||
export function langOfPath(pathname: string): Lang {
|
||||
const p = pathname.split("#")[0].split("?")[0];
|
||||
return p === "/en" || p.startsWith("/en/") ? "en" : "de";
|
||||
}
|
||||
|
||||
/**
|
||||
* Wechselt ein Klick die Sprachfassung? Dann ist das Ziel ein neues Dokument, und
|
||||
* den Wechsel macht der Browser (`location.assign`), nicht der Client-Router.
|
||||
*
|
||||
* Zwei Gründe, der zweite ist der harte:
|
||||
*
|
||||
* 1. Deutsch und Englisch haben eigene Wurzel-Layouts (`<html lang>`), über diese
|
||||
* Grenze navigiert Next ohnehin mit einem vollständigen Seitenaufbau.
|
||||
* 2. Die Messeseite wird unter ZWEI Adressen ausgeliefert: anelia-wolf.com/her-career/
|
||||
* und her-career.anelia-wolf.com, wo dieselbe Datei unter „/“ liegt und jeder
|
||||
* andere Pfad per 301 auf die Hauptdomain geht (docs/SUBDOMAIN-HERCAREER.md).
|
||||
* Von dort löst der Router ein Ziel unter /en/ nicht mehr auf – sein Datenabruf
|
||||
* wird auf die Hauptdomain umgeleitet, und er landet auf der englischen
|
||||
* STARTSEITE statt auf der englischen Messeseite. Genau das sah, wer am Stand
|
||||
* den Code gescannt und dann auf „EN“ getippt hat.
|
||||
*/
|
||||
export function crossesLangRoot(from: string, to: string): boolean {
|
||||
return langOfPath(from) !== langOfPath(to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gegenstück einer Seite in der anderen Sprache. Nimmt den aktuellen Pfad und
|
||||
* liefert denselben Inhalt in der Zielsprache; unbekannte Pfade landen auf der
|
||||
|
||||
Reference in new issue
Block a user