Brotkrumen-Pfad mit Strukturdaten
Produktseite eines Tuttlinger Spanntechnik-Herstellers mit klickbarem Pfad: aria-current, Trenner aus CSS, auf dem Telefon nur der Weg eine Ebene zurück, JSON-LD BreadcrumbList aus denselben Daten.
Ein Pfad über der H1 zeigt, wo die Seite im Angebot steht: Start › Produkte › Spannsysteme › Nullpunkt-Spannsysteme › KSN 120. Er ist eine geordnete Liste in einem <nav aria-label="Brotkrumen">, die aktuelle Seite trägt aria-current="page", Trenner kommen aus CSS. Auf dem Telefon kürzt er sich auf den Weg eine Ebene zurück oder klappt die Mitte zu einem Knopf „…“ ein. Dieselben Daten erzeugen das JSON-LD BreadcrumbList. Jeder Eintrag hat einen view-transition-name, damit der Pfad beim Seitenwechsel stehen bleibt und nur der neue Teil hinzukommt.
Wer über eine Suchmaschine mitten auf einer Produkt- oder Leistungsseite landet, sieht sofort, wo er ist und wie er eine Ebene höher kommt. Das spart Menüklicks und hält Besucher, die nicht genau das Gesuchte gefunden haben. Für den Betrieb ist es die billigste Form von Ordnung: Die Seitenstruktur wird sichtbar, ohne dass ein Mega-Menü sie erklären muss.
Wann einsetzen
Auf jeder Seite ab der zweiten Ebene in Mehrseitern: Leistungen, Produkte, Ratgeber, Standorte, Stellen. Besonders bei Industrie, Handwerk, Praxis, Kanzlei, Kfz und Immobilien mit vielen Unterseiten.
Wann nicht
Nicht auf Einseitern und nicht auf der Startseite. Nicht als Ersatz für die Hauptnavigation. Nicht bei flachen Seiten mit nur einer Ebene: Ein Pfad aus „Start › Kontakt“ sagt nichts. Nicht für Filterzustände oder Suchergebnisse.
Varianten
- Voll: ganzer Pfad am Desktop, auf dem Telefon nur „‹ Eine Ebene zurück“
- Gekürzt: lange Pfade mit eingeklapptem Mittelteil hinter einem Knopf „…“
- Übergang: Pfad bleibt beim Seitenwechsel per View Transition stehen, nur neue Einträge kommen hinzu
Umsetzung
- HTML: <nav aria-label="Brotkrumen"><ol> mit <li><a href> je Ebene, letzter Eintrag <a href aria-current="page"> oder <span aria-current="page">.
- Trenner über li + li::before mit content: "›" / "" (leerer Alternativtext), nicht als Zeichen im HTML.
- Lange Namen mit max-inline-size und
text-overflow: ellipsis nur in der Mitte, die aktuelle Seite nie kürzen; der volle Name steht im title und im Text. - Mobil über eine Container Query auf das <nav>: unter 34rem nur der Elterneintrag als „‹ Nullpunkt-Spannsysteme“; Variante gekürzt: erster Eintrag, ein <button aria-expanded> „…“, die letzten zwei.
- JSON-LD BreadcrumbList aus denselben Daten wie das HTML (position ab 1, name, item als absolute URL).
- Für Seitenwechsel
@view-transition{ navigation: auto } und je Eintragview-transition-name: krume-1 … krume-n, damit gleiche Ebenen stehen bleiben. - Fokus sichtbar, Trefferfläche mindestens 24 × 24 px (WCAG 2.5.8).
Robust umsetzen
Telefon. Unter 34rem Breite nur der Weg eine Ebene zurück, mit Pfeil und großer Trefferfläche, oder der eingeklappte Mittelteil hinter „…“. Kein waagerechtes Scrollen des Pfads.
Ohne Bewegung. Neue Einträge und der ausgeklappte Mittelteil erscheinen ohne Weg. Die View Transition fällt weg.
Barrierefrei. nav mit eigenem Namen „Brotkrumen“, damit er sich von der Hauptnavigation unterscheidet. Geordnete Liste, Screenreader sagen „Liste mit 5 Einträgen“. aria-current="page" am letzten Eintrag. Trenner nicht vorgelesen. Der Knopf „…“ heißt „Weitere 2 Ebenen zeigen“ und trägt aria-expanded. Gekürzte Namen stehen vollständig im Text, nicht nur im title.
Code
<!DOCTYPE html>
<!--
Brotkrumen-Pfad mit Strukturdaten
Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/brotkrumen-pfad/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Brotkrumen-Pfad mit Strukturdaten · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Strichpunkt Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/strichpunkt-sans:vf@latest/latin-full-normal.woff2') format('woff'); font-weight: 400 900; font-stretch: 100% 200%; font-display: swap; }
@font-face { font-family: 'Archivo'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/archivo:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-stretch: 62% 125%; font-display: swap; }
/* Rezept industrie-b2b-mittelstand: Blechgrau, Graphit, Verkehrsblau nur für Handlung, aktiv und Fokus */
:root {
--grund: #F3F3F0; --text: #111417; --leise: #4E555B; --zelle: #E4E5E1; --raster: rgba(17, 20, 23, .13);
--stahl: #16202A; --signal: #0E518D;
--headline: 'Strichpunkt Sans', system-ui, sans-serif;
--schrift: 'Archivo', system-ui, sans-serif;
--ein: cubic-bezier(0.25, 1, 0.5, 1); /* power3.out, token-swiss-knackig */
--aus: cubic-bezier(0.5, 0, 0.75, 0); /* power3.in */
--t-hover: 200ms; --t-ein: 350ms; --t-aus: 245ms; --versatz: 40ms; --weg: 12px;
--zelle-innen: clamp(16px, 2vw, 24px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: var(--schrift); -webkit-font-smoothing: antialiased; }
:root:has(:focus-visible) { scroll-behavior: auto; }
html { scroll-padding-bottom: 96px; } /* Fokus nicht unter dem Pult */
body { min-height: 100svh; }
:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }
.code-label { font: 600 .75rem/1.2 var(--schrift); font-stretch: 75%; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
a { color: inherit; }
/* ——— Kopf ——— */
.kopf { background: var(--stahl); color: #fff; display: flex; align-items: center; justify-content: space-between; gap: 12px 28px; flex-wrap: wrap; padding: 0 clamp(16px, 3vw, 40px); min-height: 60px; }
.logo { font: 600 20px/1 var(--headline); font-stretch: 118%; letter-spacing: .02em; text-decoration: none; display: flex; gap: 10px; align-items: baseline; }
.logo small { font: 600 .75rem/1 var(--schrift); font-stretch: 75%; letter-spacing: .08em; text-transform: uppercase; color: #AEB6BF; }
.hauptnav { display: flex; gap: 22px; list-style: none; padding: 0; }
.hauptnav a { text-decoration: none; font-size: .9375rem; color: #D5DAE0; padding: 20px 0; display: inline-block; }
.hauptnav a[aria-current="true"] { color: #fff; box-shadow: inset 0 -2px 0 #fff; }
.kopf-rechts { display: flex; align-items: center; gap: 18px; }
.sprache { font: 600 .75rem/1 var(--schrift); font-stretch: 75%; letter-spacing: .08em; color: #D5DAE0; }
.anfrage { background: var(--signal); color: #fff; text-decoration: none; font: 600 .9375rem/1 var(--schrift); padding: 12px 16px; transition: background-color var(--t-hover) var(--ein); }
.anfrage:hover { background: #0B4478; }
.kopf :focus-visible { outline-color: #fff; }
@media (max-width: 900px) { .hauptnav { display: none; } }
/* ——— Raster mit Plus-Markern (Signatur des Rezepts) ——— */
.blatt { position: relative; max-width: 1440px; margin: 0 auto; padding: 0 clamp(16px, 3vw, 40px) 100px;
background-image: linear-gradient(to right, var(--raster) 1px, transparent 1px); background-size: calc((100% - 2 * clamp(16px, 3vw, 40px)) / 12) 100%; background-position: clamp(16px, 3vw, 40px) 0; background-repeat: repeat-x; background-clip: content-box; }
@media (max-width: 820px) { .blatt { background-size: calc((100% - 2 * clamp(16px, 3vw, 40px)) / 4) 100%; } }
/* ——— Brotkrumen ——— */
.pfad-zeile { border-bottom: 1px solid var(--raster); background: var(--grund); position: relative; }
.pfad-zeile::before, .pfad-zeile::after { content: "+"; position: absolute; bottom: -.62em; font: 400 14px/1 var(--schrift); color: var(--leise); }
.pfad-zeile::before { left: -.3em; } .pfad-zeile::after { right: -.3em; }
nav.krumen { container: krumen / inline-size; }
.krumen ol { list-style: none; padding: 14px 0; margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 0; font-size: .9375rem; line-height: 1.3; min-height: 52px; }
.krumen li { display: inline-flex; align-items: center; min-width: 0; view-transition-name: var(--vt, none); }
.krumen li + li::before { content: "/" / ""; display: inline-block; margin: 0 10px; color: var(--leise); font-stretch: 75%; }
.krumen a { display: inline-flex; align-items: center; min-height: 24px; padding: 2px 0; text-decoration: none; color: var(--leise); background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat; transition: background-size var(--t-hover) var(--ein), color var(--t-hover) var(--ein); max-width: 26ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.krumen a:hover { color: var(--text); background-size: 100% 1px; }
.krumen [aria-current="page"] { color: var(--text); font-weight: 600; max-width: none; white-space: normal; }
.krumen a[aria-current="page"] { background-size: 0 1px; cursor: default; }
.krumen .zurueck { display: none; }
.krumen .mehr button { font: 600 .9375rem/1 var(--schrift); color: var(--text); background: var(--zelle); border: 1px solid var(--raster); min-width: 32px; min-height: 28px; padding: 0 8px; cursor: pointer; transition: background-color var(--t-hover) var(--ein); }
.krumen .mehr button:hover { background: #D9DAD5; }
.krumen li.neu { animation: krume-ein var(--t-ein) var(--ein) both; animation-delay: calc(var(--n, 0) * var(--versatz)); }
@keyframes krume-ein { from { opacity: 0; translate: calc(-1 * var(--weg)) 0; } }
/* Telefon: nur der Weg eine Ebene zurück */
@container krumen (width < 34rem) {
.voll li { display: none; }
.voll li.eltern { display: inline-flex; }
.voll li.eltern::before { content: none; }
.voll li.eltern a { color: var(--text); font-weight: 600; max-width: 100%; }
.voll li.eltern a::before { content: "‹" / ""; font-size: 1.25em; margin-right: 8px; line-height: 1; }
.voll li.eltern a { min-height: 44px; }
}
/* Gekürzt und schmal: die aktuelle Seite steht als H1 darunter, im Pfad nur noch für Screenreader */
@container krumen (width < 34rem) {
.krumen:not(.voll) li:last-child { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.krumen:not(.voll) a { min-height: 44px; }
}
/* ——— Seite ——— */
.seite { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; }
@media (min-width: 1000px) { .seite { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); } }
.inhalt { padding: clamp(28px, 4vw, 56px) var(--zelle-innen) 40px 0; view-transition-name: inhalt; min-width: 0; }
.inhalt .code-label { margin-bottom: 18px; }
h1 { font: 600 clamp(2.25rem, 1.4rem + 3.2vw, 4.25rem)/.98 var(--headline); font-stretch: 116%; letter-spacing: -.03em; text-wrap: balance; hyphens: manual; overflow-wrap: break-word; view-transition-name: titel; }
.inhalt > p { font-size: 1.0625rem; line-height: 1.55; max-width: 60ch; margin-top: 20px; color: var(--text); text-wrap: pretty; }
.kinder { list-style: none; padding: 0; margin-top: 32px; border-top: 1px solid var(--raster); }
.kinder a { display: grid; grid-template-columns: 5.5em minmax(0, 1fr) auto; gap: 16px; align-items: baseline; padding: 16px 0; border-bottom: 1px solid var(--raster); text-decoration: none; transition: background-color var(--t-hover) var(--ein); }
.kinder a:hover { background: var(--zelle); }
.kinder a:hover .pfeil { translate: 6px 0; }
.kinder .code-label { margin: 0; }
.kinder b { font: 600 1.25rem/1.2 var(--schrift); }
.kinder span.info { display: block; color: var(--leise); font-size: .9375rem; margin-top: 4px; font-weight: 400; }
.pfeil { font-size: 1.25rem; color: var(--signal); transition: translate var(--t-hover) var(--ein); }
.daten { width: 100%; border-collapse: collapse; margin-top: 30px; font-size: .9375rem; font-stretch: 87%; font-variant-numeric: tabular-nums; }
.daten th, .daten td { padding: 12px 10px; border-bottom: 1px solid var(--raster); text-align: left; }
.daten thead th { background: var(--zelle); font: 600 .75rem/1.2 var(--schrift); font-stretch: 75%; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
.daten td.w { text-align: right; font-weight: 600; } .daten td.e { color: var(--leise); width: 4em; }
.handlung { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 28px; }
.handlung a { text-decoration: none; padding: 14px 18px; font-weight: 600; }
.handlung .primaer { background: var(--signal); color: #fff; }
.handlung .neben { box-shadow: inset 0 0 0 1px var(--text); }
.seite-rechts { border-left: 1px solid var(--raster); padding: clamp(28px, 4vw, 56px) 0 40px var(--zelle-innen); display: grid; gap: 28px; align-content: start; min-width: 0; }
@media (max-width: 999px) { .seite-rechts { border-left: 0; border-top: 1px solid var(--raster); padding-left: 0; } }
.zeichnung { background: #fff; border: 1px solid var(--raster); padding: 16px; }
.zeichnung svg { display: block; width: 100%; height: auto; }
.telefon { border: 1px solid var(--raster); background: #fff; }
.telefon-kopf { display: flex; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid var(--raster); }
.telefon-rahmen { width: min(100%, 360px); margin: 0 auto; padding: 0 14px 14px; border-inline: 1px dashed var(--raster); }
.telefon-rahmen .krumen ol { padding: 8px 0; min-height: 0; }
.telefon-rahmen h1 { font-size: 1.75rem; margin-top: 6px; view-transition-name: none; }
details.ld { border: 1px solid var(--raster); background: #fff; }
details.ld summary { cursor: pointer; padding: 12px 14px; display: flex; justify-content: space-between; gap: 12px; list-style: none; }
details.ld summary::-webkit-details-marker { display: none; }
details.ld summary::after { content: "+"; font-size: 1.1rem; line-height: 1; }
details.ld[open] summary::after { content: "–"; }
details.ld pre { margin: 0; padding: 14px; border-top: 1px solid var(--raster); font: 400 12px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--text); max-height: 340px; overflow: auto; }
details.ld pre .s { color: var(--signal); }
/* View Transitions: Pfad bleibt stehen, Inhalt wechselt waagerecht */
::view-transition-group(*) { animation-duration: var(--t-ein); animation-timing-function: var(--ein); }
::view-transition-old(inhalt) { animation: inhalt-aus var(--t-aus) var(--aus) both; }
::view-transition-new(inhalt) { animation: inhalt-ein var(--t-ein) var(--ein) both; }
::view-transition-new(titel) { animation: inhalt-ein var(--t-ein) var(--ein) both; }
::view-transition-old(titel) { animation: inhalt-aus var(--t-aus) var(--aus) both; }
@keyframes inhalt-aus { to { opacity: 0; translate: calc(-1 * var(--weg)) 0; } }
@keyframes inhalt-ein { from { opacity: 0; translate: var(--weg) 0; } }
/* Pult */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 30; display: flex; flex-wrap: wrap; gap: 2px; align-items: center; padding: 4px; background: var(--stahl); box-shadow: 0 8px 26px rgba(0, 0, 0, .22); }
.pult .code-label { color: #AEB6BF; padding: 0 10px; }
.pult button { font: 600 .75rem/1 var(--schrift); font-stretch: 75%; letter-spacing: .08em; text-transform: uppercase; color: #fff; background: transparent; border: 0; padding: 10px 12px; cursor: pointer; transition: background-color var(--t-hover) var(--ein); }
.pult button:hover { background: rgba(255, 255, 255, .1); }
.pult button[aria-pressed="true"] { background: var(--signal); }
.pult button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.vorschau body { overflow: hidden; }
.vorschau details.ld, .vorschau .handlung { display: none; }
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-delay: 0s !important; }
:root.reduziert ::view-transition-group(*), :root.reduziert ::view-transition-old(*), :root.reduziert ::view-transition-new(*) { animation: none !important; }
@media (hover: none) { .krumen a { padding-block: 8px; } }
@media (max-width: 360px) { .pult .code-label { display: none; } .kinder a { grid-template-columns: minmax(0, 1fr) auto; } .kinder a > .code-label { display: none; } }
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body>
<header class="kopf">
<a class="logo" href="#seite" data-geh="start">KERBER <small>Spanntechnik · Tuttlingen</small></a>
<ul class="hauptnav">
<li><a href="#seite" data-geh="produkte" aria-current="true">Produkte</a></li>
<li><a href="#seite">Anwendungen</a></li>
<li><a href="#seite">Service und Downloads</a></li>
<li><a href="#seite">Unternehmen</a></li>
</ul>
<div class="kopf-rechts"><span class="sprache">DE · EN</span><a class="anfrage" href="#seite">Projekt anfragen</a></div>
</header>
<div class="blatt" id="seite">
<div class="pfad-zeile"><nav class="krumen voll" aria-label="Brotkrumen" id="krumen"><ol></ol></nav></div>
<div class="seite">
<main class="inhalt" id="inhalt" tabindex="-1"></main>
<aside class="seite-rechts" aria-label="Pfad auf dem Telefon und Strukturdaten">
<div class="telefon" aria-hidden="true">
<div class="telefon-kopf"><span class="code-label">Derselbe Pfad bei 360 px</span><span class="code-label">Container Query</span></div>
<div class="telefon-rahmen" id="telefon" inert></div>
</div>
<div class="zeichnung" id="zeichnung" hidden></div>
<details class="ld" id="ld-box">
<summary><span class="code-label">JSON-LD · BreadcrumbList</span></summary>
<pre id="ld-code" tabindex="0" aria-label="JSON-LD der Brotkrumen"></pre>
</details>
</aside>
</div>
</div>
<script type="application/ld+json" id="ld"></script>
<nav class="pult" aria-label="Variante wählen">
<span class="code-label">Variante</span>
<button type="button" data-v="voll" aria-pressed="true">Voll</button>
<button type="button" data-v="gekuerzt" aria-pressed="false">Gekürzt</button>
<button type="button" data-v="uebergang" aria-pressed="false">Übergang</button>
</nav>
<script>
(() => {
const H = document.documentElement;
const reduziert = () => H.classList.contains('reduziert');
const $ = s => document.querySelector(s);
const BASIS = 'https://www.kerber-spanntechnik.example';
/* Seitenbaum der erfundenen Firma; jede Seite kennt ihre Eltern */
const SEITEN = {
start: { name: 'Start', titel: 'Spanntechnik für Fräszentren', code: '00 · Start', pfad: '/', text: 'Nullpunkt-Spannsysteme, Kraftspannblöcke und Schraubstöcke, gefertigt in Tuttlingen.', kinder: ['produkte'] },
produkte: { name: 'Produkte', titel: 'Produkte', code: '01 · Produkte', pfad: '/produkte/', eltern: 'start', text: 'Drei Produktfamilien, alle mit Datenblatt, CAD-Modell und zuständiger Anwendungstechnik.', kinder: ['spannsysteme', 'schraubstoecke'] },
spannsysteme: { name: 'Spannsysteme', titel: 'Spannsysteme', code: '01.2 · Spannsysteme', pfad: '/produkte/spannsysteme/', eltern: 'produkte', text: 'Werkstücke in Sekunden wechseln, ohne neu zu messen. Für 3- und 5-Achs-Bearbeitung.', kinder: ['nullpunkt', 'kraftspann'] },
schraubstoecke: { name: 'Schraubstöcke', titel: 'Schraubstöcke', code: '01.3 · Schraubstöcke', pfad: '/produkte/schraubstoecke/', eltern: 'produkte', text: 'Zentrische und feste Backen, Spannweite 0 bis 310 mm.', kinder: [] },
nullpunkt: { name: 'Nullpunkt-Spannsysteme', titel: 'Nullpunkt-Spannsysteme', code: '01.2.1 · Nullpunkt', pfad: '/produkte/spannsysteme/nullpunkt/', eltern: 'spannsysteme', text: 'Pneumatisch öffnen, mechanisch spannen: Die Federkraft hält das Werkstück auch ohne Luftdruck.', kinder: ['ksn120', 'ksn160'] },
kraftspann: { name: 'Kraftspannblöcke', titel: 'Kraftspannblöcke', code: '01.2.2 · Kraftspann', pfad: '/produkte/spannsysteme/kraftspannbloecke/', eltern: 'spannsysteme', text: 'Hydraulische Blöcke für die Serienfertigung.', kinder: [] },
ksn120: { name: 'KSN 120 Modul', titel: 'Nullpunkt-Spannmodul KSN 120', code: 'Art.-Nr. 4410-120', pfad: '/produkte/spannsysteme/nullpunkt/ksn-120/', eltern: 'nullpunkt', text: 'Einbaumodul für Maschinentische und Paletten. Öffnen ab 6 bar, Spannen per Federpaket.', produkt: [['Einzugskraft', '25', 'kN'], ['Haltekraft', '75', 'kN'], ['Wiederholgenauigkeit', '< 0,005', 'mm'], ['Öffnungsdruck', '6', 'bar']] },
ksn160: { name: 'KSN 160 Modul', titel: 'Nullpunkt-Spannmodul KSN 160', code: 'Art.-Nr. 4410-160', pfad: '/produkte/spannsysteme/nullpunkt/ksn-160/', eltern: 'nullpunkt', text: 'Für schwere Paletten bis 1.200 kg.', produkt: [['Einzugskraft', '40', 'kN'], ['Haltekraft', '105', 'kN'], ['Wiederholgenauigkeit', '< 0,005', 'mm'], ['Öffnungsdruck', '6', 'bar']] },
/* nur Variante gekürzt: eine Ebene mehr und ein langer Produktname */
fraes: { name: 'Module für Fräszentren', titel: 'Module für Fräszentren', code: '01.2.1.4 · Fräszentren', pfad: '/produkte/spannsysteme/nullpunkt/fraeszentren/', eltern: 'nullpunkt', text: 'Flache Bauform für Maschinen mit wenig Verfahrweg in Z.', kinder: ['ksn120d'] },
ksn120d: { name: 'KSN 120 Doppelmodul mit Abblasfunktion', titel: 'KSN 120 Doppelmodul mit Abblasfunktion', code: 'Art.-Nr. 4412-120', pfad: '/produkte/spannsysteme/nullpunkt/fraeszentren/ksn-120-doppel/', eltern: 'fraes', text: 'Zwei Module auf einer Platte, Abblasen der Auflage vor jedem Spannvorgang.', produkt: [['Einzugskraft', '2 × 25', 'kN'], ['Haltekraft', '2 × 75', 'kN'], ['Rasterabstand', '200', 'mm'], ['Öffnungsdruck', '6', 'bar']] }
};
const v0 = new URLSearchParams(location.search).get('v');
let variante = ['voll', 'gekuerzt', 'uebergang'].includes(v0) ? v0 : 'voll';
let aktuell = variante === 'gekuerzt' ? 'ksn120d' : 'ksn120';
let mitteOffen = false, altePfad = [];
const kette = id => { const k = []; let s = id; while (s) { k.unshift(s); s = SEITEN[s].eltern; } return k; };
const kinderVon = id => {
const s = SEITEN[id];
if (id === 'nullpunkt') return variante === 'gekuerzt' ? ['fraes', 'ksn160'] : ['ksn120', 'ksn160'];
return s.kinder || [];
};
function krumenHTML(ids, fuerTelefon) {
const lang = variante === 'gekuerzt' && ids.length > 4 && !mitteOffen;
return ids.map((id, i) => {
const s = SEITEN[id], letzte = i === ids.length - 1;
const eltern = i === ids.length - 2 ? ' eltern' : '';
const neu = !fuerTelefon && !altePfad.includes(id) ? ' neu' : '';
const vt = !fuerTelefon && variante === 'uebergang' ? ` style="--vt: krume-${i}; --n: ${i}"` : ` style="--n: ${Math.max(0, i - 1)}"`;
if (lang && i > 0 && i < ids.length - 2) {
if (i !== 1) return '';
const anzahl = ids.length - 3;
return `<li class="mehr"${vt}><button type="button" aria-expanded="false" aria-label="Weitere ${anzahl} Ebenen zeigen" data-mehr>…</button></li>`;
}
const inhalt = letzte
? `<a href="#seite" aria-current="page">${s.name}</a>`
: `<a href="#seite" data-geh="${id}" title="${s.name}">${s.name}</a>`;
return `<li class="${(eltern + neu).trim()}"${vt}>${inhalt}</li>`;
}).join('');
}
function ldDaten(ids) {
return { '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: ids.map((id, i) => {
const e = { '@type': 'ListItem', position: i + 1, name: SEITEN[id].name };
if (i < ids.length - 1) e.item = BASIS + SEITEN[id].pfad;
return e;
}) };
}
function zeichnung(id) {
const doppel = id === 'ksn120d';
const modul = x => `<g transform="translate(${x} 0)">
<rect x="-60" y="-60" width="120" height="120" fill="none" stroke="#111417" stroke-width="1.4"/>
<circle r="46" fill="none" stroke="#111417" stroke-width="1"/>
<circle r="17" fill="#E4E5E1" stroke="#111417" stroke-width="1.4"/>
<circle r="6" fill="none" stroke="#111417" stroke-width="1"/>
${[[-44, -44], [44, -44], [-44, 44], [44, 44]].map(([a, b]) => `<circle cx="${a}" cy="${b}" r="5" fill="none" stroke="#111417" stroke-width="1"/><path d="M${a - 8} ${b}h16M${a} ${b - 8}v16" stroke="#4E555B" stroke-width=".6"/>`).join('')}
<path d="M-70 0H70M0 -70V70" stroke="#4E555B" stroke-width=".6" stroke-dasharray="10 3 2 3"/>
</g>`;
const breite = doppel ? 460 : 300;
return `<svg viewBox="${-breite / 2} -110 ${breite} 230" role="img" aria-label="Technische Zeichnung, Draufsicht: ${doppel ? 'zwei quadratische Module im Abstand von 200 mm' : 'quadratisches Modul 120 mm'} mit Spannbolzen in der Mitte und vier Befestigungsbohrungen M8.">
${doppel ? modul(-100) + modul(100) : modul(0)}
<g font-family="Archivo" font-size="9" font-stretch="75%" fill="#4E555B" letter-spacing=".6">
<path d="M${doppel ? -160 : -60} 82V96M${doppel ? 160 : 60} 82V96M${doppel ? -160 : -60} 90H${doppel ? 160 : 60}" stroke="#4E555B" stroke-width=".8"/>
<text x="0" y="106" text-anchor="middle">${doppel ? '320' : '120'}</text>
${doppel ? '<path d="M-100 -82V-96M100 -82V-96M-100 -90H100" stroke="#4E555B" stroke-width=".8"/><text x="0" y="-98" text-anchor="middle">200 RASTER</text>' : '<text x="0" y="-74" text-anchor="middle">4 × M8 · Ø 92</text>'}
</g>
</svg>`;
}
function render() {
const ids = kette(aktuell), s = SEITEN[aktuell];
const nav = $('#krumen');
nav.classList.toggle('voll', variante !== 'gekuerzt');
nav.querySelector('ol').innerHTML = krumenHTML(ids, false);
altePfad = ids.slice();
/* Telefon-Ansicht: dieselbe Liste in einem 360 px breiten Container */
$('#telefon').innerHTML = `<nav class="krumen${variante !== 'gekuerzt' ? ' voll' : ''}"><ol>${krumenHTML(ids, true).replace(/ data-geh="[^"]+"/g, '').replace(/ data-mehr/g, '')}</ol></nav><h1>${s.titel}</h1>`;
const kinder = kinderVon(aktuell);
let html = `<p class="code-label">${s.code}</p><h1 id="h-seite">${s.titel.replace('Nullpunkt-Spannmodul', 'Nullpunkt-Spannmodul').replace('Abblasfunktion', 'Abblasfunktion')}</h1><p>${s.text}</p>`;
if (kinder.length) html += `<ul class="kinder">${kinder.map(k => { const c = SEITEN[k]; return `<li><a href="#seite" data-geh="${k}"><span class="code-label">${c.code.split(' · ')[0].replace('Art.-Nr. ', '')}</span><span><b>${c.name}</b><span class="info">${c.text}</span></span><span class="pfeil" aria-hidden="true">→</span></a></li>`; }).join('')}</ul>`;
if (s.produkt) html += `<table class="daten"><caption class="code-label" style="text-align:left;margin-bottom:8px">Technische Daten · Stand 04.11.2026</caption><thead><tr><th scope="col">Kenngröße</th><th scope="col" style="text-align:right">Wert</th><th scope="col">Einheit</th></tr></thead><tbody>${s.produkt.map(([a, b, c]) => `<tr><th scope="row" style="font-weight:400">${a}</th><td class="w">${b}</td><td class="e">${c}</td></tr>`).join('')}</tbody></table><div class="handlung"><a class="primaer" href="#seite">Projekt anfragen</a><a class="neben" href="#seite">Datenblatt PDF, 420 KB</a></div>`;
$('#inhalt').innerHTML = html;
const z = $('#zeichnung'); z.hidden = !s.produkt; if (s.produkt) z.innerHTML = zeichnung(aktuell);
const ld = ldDaten(ids);
$('#ld').textContent = JSON.stringify(ld);
$('#ld-code').innerHTML = JSON.stringify(ld, null, 2).replace(/&/g, '&').replace(/</g, '<').replace(/("https?:[^"]+")/g, '<span class="s">$1</span>');
}
function geh(id, fokus) {
if (!SEITEN[id] || id === aktuell) return;
mitteOffen = false;
const tun = () => { aktuell = id; render(); };
if (variante === 'uebergang' && document.startViewTransition && !reduziert()) {
/* Ohne „neu“-Einblendung: die View Transition übernimmt die Bewegung */
const t = document.startViewTransition(() => { altePfad = kette(id); tun(); });
if (fokus) t.finished.then(() => $('#inhalt').focus({ preventScroll: true }));
} else { tun(); if (fokus) $('#inhalt').focus({ preventScroll: true }); }
}
document.addEventListener('click', e => {
const a = e.target.closest('[data-geh]');
if (a) { e.preventDefault(); pause(); geh(a.dataset.geh, true); return; }
const m = e.target.closest('[data-mehr]');
if (m) {
pause(); mitteOffen = true;
const ids = kette(aktuell); altePfad = [ids[0], ...ids.slice(-2)];
$('#krumen ol').innerHTML = krumenHTML(ids, false); altePfad = ids.slice();
const erster = $('#krumen li:nth-child(2) a'); if (erster) erster.focus();
}
});
function setzeVariante(v, still) {
variante = v;
aktuell = v === 'gekuerzt' ? 'ksn120d' : 'ksn120';
mitteOffen = false; altePfad = kette(aktuell);
document.querySelectorAll('.pult button').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === v ? 'true' : 'false'));
if (!still) { const q = new URLSearchParams(location.search); q.set('v', v); history.replaceState(null, '', '?' + q.toString()); }
render();
}
document.querySelectorAll('.pult button').forEach(b => b.addEventListener('click', () => { pause(); setzeVariante(b.dataset.v); }));
const offenAmDesktop = matchMedia('(min-width: 1000px)');
$('#ld-box').open = offenAmDesktop.matches;
/* Vorführung: hoch durch den Pfad und wieder hinein */
let lauf = null;
function neustart() { clearTimeout(lauf); setzeVariante(variante, true); }
function vorfuehren() {
clearTimeout(lauf);
const weg = variante === 'gekuerzt'
? [() => $('[data-mehr]') && $('[data-mehr]').click(), () => geh('nullpunkt'), () => geh('fraes'), () => geh('ksn120d')]
: [() => geh('nullpunkt'), () => geh('spannsysteme'), () => geh('nullpunkt'), () => geh('ksn120')];
let i = 0;
const takt = () => { if (i < weg.length) { weg[i++](); lauf = setTimeout(takt, 1400); } else lauf = setTimeout(vorfuehren, 800); };
lauf = setTimeout(takt, 900);
}
function pause() { clearTimeout(lauf); }
function weiter() { vorfuehren(); }
setzeVariante(variante, true);
})();
</script>
</body>
</html>