Sidebar-System - So funktioniert's
Eine verständliche Erklärung, wie die beiden Sidebars arbeiten, wie sie generiert werden und wie ScrollSpy den aktiven Abschnitt hervorhebt.
Das Sidebar-System im Überblick
DevPanicZone hat ein ausgeklügeltes Sidebar-System mit zwei unabhängigen Sidebars , die unterschiedlich funktionieren:
- Inhalt: Inhaltsverzeichnis der aktuellen Seite
-
Generierung:
Server-seitig via
generate-navigation.js -
Basis:
h2&h3Überschriften - Features: ScrollSpy, Smooth Scroll
- Inhalt: Alle Tutorials der Kategorie, gruppiert nach Unterkategorie
-
Generierung:
Server-seitig via
generate-navigation.js -
Basis:
TUTORIAL_ORDER+getAllTutorialsForCategory() - Features: Active-Link Highlighting, Gruppenüberschriften
-
Server-seitig:
HTML wird beim
npm run builderstellt und in die Datei geschrieben - Browser-seitig: JavaScript würde im Browser HTML erstellen (langsamer, fehleranfällig)
Das bedeutet: Wenn du die Seite öffnest, ist das HTML schon da! JavaScript ist nur für Interaktionen (Öffnen/Schließen, ScrollSpy) zuständig.
Architektur & Zusammenspiel
Mehrere Dateien arbeiten zusammen, um das Sidebar-System zu realisieren:
Die beteiligten Dateien
scripts/
└── generate-navigation.js → Generiert Sidebar-HTML
assets/
├── css/
│ └── sidebar.css → Styling beider Sidebars
└── js/
├── sidebar.js → Öffnen/Schließen, Event-Handling
├── scroll-spy.js → Active-Link beim Scrollen
└── main.js → Header, Footer, Theme Toggle
Ablauf: Von der Generierung bis zur Anzeige
-
Build-Zeit:
generate-navigation.jserstellt HTML für beide Sidebars - HTML-Datei: Sidebar-HTML ist fest im HTML eingebettet
- Seite lädt: Browser rendert die Sidebars (noch nicht sichtbar)
-
JavaScript lädt:
sidebar.jsundscroll-spy.jswerden aktiv - User-Interaktion: Klick auf Button → Sidebar öffnet sich
- Scrollen: ScrollSpy hebt aktiven Link hervor
Linke Sidebar (TOC) - Inhaltsverzeichnis
Die linke Sidebar zeigt ein Inhaltsverzeichnis der aktuellen Seite. Sie wird aus den Überschriften generiert.
1. Generierung (generate-navigation.js)
Die Funktion
addIdsToHeadingsAndGenerateSidebar()
macht folgendes:
function addIdsToHeadingsAndGenerateSidebar(html) {
const dom = new JSDOM(html);
const doc = dom.window.document;
// 1. Finde alle h2 und h3 Überschriften
const headings = doc.querySelectorAll('h2, h3');
const anchors = [];
const usedIds = new Set();
headings.forEach(heading => {
// Überspringe Überschriften mit class="no-toc"
if (heading.classList.contains('no-toc')) return;
let id = heading.id;
// 2. Generiere ID aus dem Text
if (!id) {
id = heading.textContent.trim().toLowerCase()
.replace(/[äöüÄÖÜß]/g, match => {
const map = { 'ä': 'ae', 'ö': 'oe', 'ü': 'ue' };
return map[match];
})
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-');
// 3. Vermeide Duplikate
let finalId = id;
let counter = 1;
while (usedIds.has(finalId)) {
finalId = `${id}-${counter}`;
counter++;
}
// 4. Füge ID zum Heading hinzu!
heading.id = finalId;
id = finalId;
}
usedIds.add(id);
// 5. Erstelle Sidebar-Link
const level = heading.tagName === 'H2' ? 'toc-level-1' : 'toc-level-2';
anchors.push(
`<li class="${level}"><a href="#${id}">${heading.textContent.trim()}</a></li>`
);
});
return {
html: dom.serialize(), // HTML mit IDs!
anchors: anchors.join('\n')
};
}
Schritt für Schritt
-
Finde alle
<h2>und<h3>Überschriften -
Überspringe Überschriften mit
class="no-toc" - Generiere eine ID aus dem Überschriften-Text (z.B. "Was ist CSS?" → "was-ist-css")
-
Füge die ID direkt in die Überschrift ein:
<h2 id="was-ist-css"> -
Erstelle einen Link:
<a href="#was-ist-css">Was ist CSS?</a> - Gib sowohl das modifizierte HTML als auch die Links zurück
2. HTML-Struktur
Das generierte HTML wird zwischen den Markern eingefügt:
<aside class="tutorial-sidebar" id="tutorialSidebar">
<div class="sidebar-header">
<h3 class="sidebar-title no-toc">Auf dieser Seite</h3>
<button class="sidebar-close" id="sidebarClose">×</button>
</div>
<nav class="sidebar-toc">
<ul class="toc-list">
<!-- Sidebar-Anchor-Start -->
<li class="toc-level-1"><a href="#was-ist-css">Was ist CSS?</a></li>
<li class="toc-level-1"><a href="#selektoren">Selektoren</a></li>
<li class="toc-level-2"><a href="#element-selektor">Element-Selektor</a></li>
<li class="toc-level-2"><a href="#class-selektor">Class-Selektor</a></li>
<!-- Sidebar-Anchor-End -->
</ul>
</nav>
</aside>
-
.toc-level-1→ h2-Überschriften (Hauptabschnitte) -
.toc-level-2→ h3-Überschriften (Unterabschnitte, eingerückt)
3. Interaktivität (sidebar.js)
Das
sidebar.js
Script steuert das Öffnen und Schließen:
document.addEventListener('DOMContentLoaded', () => {
const sidebar = document.getElementById('tutorialSidebar');
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebarClose = document.getElementById('sidebarClose');
const overlay = document.createElement('div');
overlay.className = 'sidebar-overlay';
document.body.appendChild(overlay);
// Öffne Sidebar
function openSidebar() {
sidebar.classList.add('is-open');
overlay.classList.add('is-visible');
document.body.style.overflow = 'hidden'; // Verhindert Scrollen
}
// Schließe Sidebar
function closeSidebar() {
sidebar.classList.remove('is-open');
overlay.classList.remove('is-visible');
document.body.style.overflow = '';
}
// Event Listeners
sidebarToggle.addEventListener('click', openSidebar);
sidebarClose.addEventListener('click', closeSidebar);
overlay.addEventListener('click', closeSidebar);
// ESC-Taste
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeSidebar();
});
// Schließe bei Klick auf TOC-Link
const tocLinks = document.querySelectorAll('.toc-link');
tocLinks.forEach(link => {
link.addEventListener('click', closeSidebar);
});
});
Was passiert beim Öffnen?
-
Klasse
.is-openwird zur Sidebar hinzugefügt - CSS transform bewegt die Sidebar ins Sichtfeld
- Overlay wird sichtbar (dunkler Hintergrund)
- Body-Scrolling wird deaktiviert
4. Smooth Scroll mit Offset
Wenn du auf einen TOC-Link klickst, scrollt die Seite sanft zur Überschrift - mit einem Offset:
tocLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault(); // Verhindere Standard-Sprung
const targetId = this.getAttribute('href'); // z.B. "#was-ist-css"
const targetSection = document.querySelector(targetId);
if (targetSection) {
const headerOffset = 100; // 100px Abstand von oben
const elementPosition = targetSection.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
});
});
Warum der Offset?
- Der Header ist sticky und würde die Überschrift überdecken
- 100px Offset stellt sicher, dass die Überschrift sichtbar ist
ScrollSpy - Aktiven Link hervorheben
ScrollSpy ist ein System, das beim Scrollen den aktuell sichtbaren Abschnitt in der Sidebar hervorhebt.
Wie funktioniert ScrollSpy?
Das
scroll-spy.js
Script trackt deine Scroll-Position und vergleicht sie mit den Positionen der
Überschriften:
class ScrollSpy {
constructor() {
this.tocLinks = document.querySelectorAll('.toc-list a');
this.headings = [];
this.currentActive = null;
this.ticking = false;
this.init();
}
init() {
if (this.tocLinks.length === 0) return;
// 1. Sammle alle Überschriften mit IDs
this.tocLinks.forEach(link => {
const id = link.getAttribute('href').substring(1); // Entferne #
const heading = document.getElementById(id);
if (heading) {
this.headings.push({
id: id,
element: heading,
link: link
});
}
});
// 2. Event Listeners
window.addEventListener('scroll', () => this.onScroll(), { passive: true });
// 3. Initial Check
this.updateActiveLink();
}
onScroll() {
// Throttling für Performance
if (!this.ticking) {
window.requestAnimationFrame(() => {
this.updateActiveLink();
this.ticking = false;
});
this.ticking = true;
}
}
updateActiveLink() {
const scrollPosition = window.scrollY + 100; // 100px Offset
let activeHeading = null;
// Finde die Überschrift, die gerade sichtbar ist
for (let i = 0; i < this.headings.length; i++) {
const heading = this.headings[i];
const headingTop = heading.element.offsetTop;
if (scrollPosition >= headingTop) {
activeHeading = heading;
} else {
break;
}
}
// Wenn ganz unten, nimm die letzte Überschrift
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 10) {
activeHeading = this.headings[this.headings.length - 1];
}
// Update nur wenn sich was geändert hat
if (activeHeading && activeHeading !== this.currentActive) {
this.setActiveLink(activeHeading);
}
}
setActiveLink(activeHeading) {
// Entferne .active von allen Links
this.tocLinks.forEach(link => link.classList.remove('active'));
// Setze .active auf den aktiven Link
if (activeHeading && activeHeading.link) {
activeHeading.link.classList.add('active');
this.currentActive = activeHeading;
}
}
}
// Initialisiere ScrollSpy
new ScrollSpy();
Schritt für Schritt erklärt
1. Initialisierung
Beim Laden der Seite:
-
Finde alle Links in der TOC (
.toc-list a) - Für jeden Link: Finde die zugehörige Überschrift via ID
- Speichere Link + Überschrift zusammen in einem Array
// Link: <a href="#was-ist-css">Was ist CSS?</a>
// Heading: <h2 id="was-ist-css">Was ist CSS?</h2>
// Gespeichert als:
{
id: 'was-ist-css',
element: <h2> Element,
link: <a> Element
}
2. Scroll-Tracking
Beim Scrollen:
- Hole aktuelle Scroll-Position + 100px Offset
- Durchlaufe alle Überschriften von oben nach unten
- Finde die Überschrift, die gerade sichtbar ist (scrollPosition >= headingTop)
- Das ist der aktive Abschnitt!
Der Header ist sticky und nimmt Platz ein. Mit 100px Offset wird eine Überschrift als "aktiv" markiert, wenn sie ca. unter dem Header ist - nicht erst wenn sie ganz oben am Viewport klebt.
3. Performance-Optimierung (Throttling)
Das Scroll-Event wird SEHR oft gefeuert (bei jedem Pixel!). Um die Performance zu schonen:
onScroll() {
if (!this.ticking) {
window.requestAnimationFrame(() => {
this.updateActiveLink();
this.ticking = false;
});
this.ticking = true;
}
}
Was passiert hier?
-
requestAnimationFramewartet bis zum nächsten Browser-Repaint - Dadurch läuft die Funktion maximal 60x pro Sekunde (statt 1000x!)
-
tickingFlag verhindert mehrfache Aufrufe
4. Active-Klasse setzen
Wenn der aktive Abschnitt sich ändert:
-
Entferne
.activevon ALLEN TOC-Links -
Füge
.activezum aktiven Link hinzu - CSS hebt den Link hervor (z.B. blaue Farbe, fetter Text)
.toc-list a {
color: var(--text-secondary);
}
.toc-list a.active {
color: var(--accent-blue);
font-weight: 600;
border-left: 3px solid var(--accent-blue);
}
Edge Cases
ScrollSpy behandelt auch spezielle Fälle:
Ganz oben auf der Seite
Wenn keine Überschrift im Viewport ist → Erste Überschrift wird aktiv
Ganz unten auf der Seite
Wenn du am Ende der Seite bist → Letzte Überschrift wird aktiv (auch wenn sie nicht mehr sichtbar ist)
// Wenn ganz unten
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 10) {
activeHeading = this.headings[this.headings.length - 1];
}
Rechte Sidebar (Tutorial-Nav)
Die rechte Sidebar zeigt alle Tutorials der aktuellen Kategorie. Sie wird ebenfalls server-seitig generiert!
1. Generierung (generate-navigation.js)
Die Funktion
generateSidebarNavList()
erstellt die Tutorial-Liste,
gruppiert nach Unterkategorien
:
function generateSidebarNavList(filePath) {
// 1. Ermittle die Hauptkategorie (z.B. "css")
const category = getCategoryFromPath(filePath);
const currentUrl = getRelativeUrl(filePath);
// 2. Hole ALLE Tutorials dieser Kategorie, gruppiert
const allTutorials = getAllTutorialsForCategory(category);
let html = '';
// 3. Iteriere durch alle Unterkategorien
Object.keys(allTutorials).forEach(subcategoryKey => {
const group = allTutorials[subcategoryKey];
// 4. Gruppen-Überschrift einfügen
html += `<li class="sidebar-group-title">${group.displayName}</li>\n`;
// 5. Tutorials dieser Gruppe
group.tutorials.forEach(tutorial => {
const isActive = tutorial.url === currentUrl;
const activeClass = isActive ? ' class="active"' : '';
html += `<li><a href="${tutorial.url}"${activeClass}>${tutorial.title}</a></li>\n`;
});
});
return html.trim();
}
Schritt für Schritt:
- Ermittle die Hauptkategorie aus dem Dateipfad (z.B. "css")
-
Hole
alle Tutorials
dieser Kategorie via
getAllTutorialsForCategory() - Iteriere durch alle Unterkategorien (z.B. "css-basics", "css-advanced")
-
Füge eine
Gruppenüberschrift
mit
.sidebar-group-titleein - Erstelle Links für alle Tutorials der Gruppe
-
Das aktuelle Tutorial bekommt
class="active"
Früher zeigte die rechte Sidebar nur Tutorials der aktuellen Unterkategorie . Jetzt zeigt sie alle Tutorials der Hauptkategorie , gruppiert nach Unterkategorien. So kannst du direkt zwischen "CSS Basics" und "CSS Advanced" wechseln.
2. HTML-Struktur
Das generierte HTML enthält jetzt Gruppenüberschriften für jede Unterkategorie:
<aside class="tutorial-related">
<div class="sidebar-header">
<h3 class="sidebar-title no-toc">Tutorials</h3>
<button class="sidebar-close" id="tutorialNavClose">×</button>
</div>
<div class="sidebar-nav">
<ul class="sidebar-nav-list">
<!-- Gruppe: CSS Basics -->
<li class="sidebar-group-title">CSS Basics</li>
<li><a href="/tutorials/css/css-basics/css-einbinden.html">CSS einbinden</a></li>
<li><a href="/tutorials/css/css-basics/css-grundlagen.html" class="active">CSS Grundlagen</a></li>
<li><a href="/tutorials/css/css-basics/css-color-units.html">CSS Farben & Einheiten</a></li>
<!-- Gruppe: CSS Advanced -->
<li class="sidebar-group-title">CSS Advanced</li>
<li><a href="/tutorials/css/css-advanced/css-flexbox.html">CSS Flexbox</a></li>
<li><a href="/tutorials/css/css-advanced/css-grid.html">CSS Grid</a></li>
<!-- Gruppe: CSS Specials -->
<li class="sidebar-group-title">CSS Specials</li>
<li><a href="/tutorials/css/css-specials/css-organization.html">CSS Organization</a></li>
</ul>
</div>
</aside>
-
.sidebar-group-title→ Überschrift für eine Unterkategorie (nicht klickbar) -
.active→ Das aktuell angezeigte Tutorial
3. Interaktivität (sidebar.js)
Das Öffnen/Schließen funktioniert genau wie bei der linken Sidebar:
const tutorialRelated = document.querySelector('.tutorial-related');
const tutorialNavToggle = document.getElementById('tutorialNavToggle');
function openTutorialNav() {
tutorialRelated.classList.add('is-open');
overlay.classList.add('is-visible');
document.body.style.overflow = 'hidden';
}
function closeTutorialNav() {
tutorialRelated.classList.remove('is-open');
overlay.classList.remove('is-visible');
document.body.style.overflow = '';
}
tutorialNavToggle.addEventListener('click', openTutorialNav);
Das Zusammenspiel aller Komponenten
Lass uns das Ganze nochmal von Anfang bis Ende durchgehen:
Phase 1: Build-Zeit (npm run build)
-
generate-navigation.jswird ausgeführt - Es findet alle HTML-Dateien
-
Für jede Datei:
- Findet h2/h3 Überschriften
- Generiert IDs und fügt sie zu Überschriften hinzu
- Erstellt TOC-Links für linke Sidebar
- Erstellt Tutorial-Liste für rechte Sidebar
- Schreibt alles ins HTML
- Ergebnis: HTML-Dateien mit fertigem Sidebar-HTML
Phase 2: Seite lädt im Browser
- Browser lädt HTML-Datei
-
Beide Sidebars sind im HTML, aber nicht sichtbar (CSS:
transform: translateX(-100%)) -
CSS-Dateien laden:
-
style.css→ Basis-Styling -
sidebar.css→ Sidebar-Styling
-
-
JavaScript-Dateien laden:
-
main.js→ Header, Theme Toggle -
sidebar.js→ Sidebar-Interaktionen -
scroll-spy.js→ Active-Link-Tracking
-
Phase 3: User-Interaktion
User klickt auf Sidebar-Button
- Button-Click Event
-
sidebar.jsfügt.is-openKlasse hinzu - CSS Transition schiebt Sidebar ins Sichtfeld
- Overlay wird sichtbar
User scrollt auf der Seite
- Scroll Event
-
scroll-spy.jsprüft Scroll-Position - Findet aktive Überschrift
-
Fügt
.activeKlasse zum passenden TOC-Link hinzu - CSS hebt den Link hervor
User klickt auf TOC-Link
- Link-Click Event
-
sidebar.jsverhindert Standard-Verhalten - Smooth Scroll zur Überschrift (mit 100px Offset)
- Sidebar schließt sich automatisch
-
scroll-spy.jsaktualisiert aktiven Link
CSS-Architektur
Die Sidebars nutzen moderne CSS-Features für Performance und Responsiveness:
Positionierung (Fixed)
.tutorial-sidebar {
position: fixed;
top: 0;
left: 0;
width: 280px;
height: 100vh;
/* Sidebar ist standardmäßig versteckt */
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 1000;
}
Warum
transform
statt
left
?
-
transformnutzt GPU-Beschleunigung → butterweiche Animation -
lefttriggert Reflow → langsamer und ruckelig
Open-State
.tutorial-sidebar.is-open {
transform: translateX(0); /* Schiebt Sidebar ins Sichtfeld */
}
Responsive Design
/* Desktop: Sidebars dauerhaft sichtbar */
@media (min-width: 1200px) {
.tutorial-sidebar {
transform: translateX(0);
position: sticky; /* Statt fixed */
top: 100px;
}
.sidebar-toggle {
display: none; /* Buttons ausblenden */
}
}
Auf Desktop:
- Sidebars sind dauerhaft sichtbar
- Floating Buttons werden ausgeblendet
-
Sidebars scrollen mit (
position: sticky)
Auf Mobile:
- Sidebars sind versteckt
- Floating Buttons sind sichtbar
- Sidebars öffnen als Overlay
Best Practices
1. IDs für Überschriften
Wenn du IDs manuell vergibst (statt sie generieren zu lassen):
- Nutze Kleinbuchstaben
- Nutze Bindestriche statt Leerzeichen
- Vermeide Sonderzeichen
-
Beispiel:
id="was-ist-css"
2. no-toc Klasse
Nutze
class="no-toc"
für Überschriften, die NICHT im Inhaltsverzeichnis erscheinen sollen:
<h3 class="no-toc">Diese Überschrift erscheint nicht in der Sidebar</h3>
3. Tutorial-Reihenfolge
Halte
TUTORIAL_ORDER
immer aktuell:
- Neue Tutorials sofort eintragen
- Reihenfolge logisch gestalten (Basics → Advanced)
-
Nach Änderungen
npm run buildausführen
4. Performance
Das System ist bereits optimiert:
- HTML wird beim Build generiert (nicht im Browser!)
- ScrollSpy nutzt Throttling
- CSS Transforms für Animationen
-
passive: truefür Scroll-Events
Häufige Probleme
Problem: Sidebar-Links führen nirgends hin
Ursache:
- IDs fehlen in den Überschriften
Lösung:
-
Führe
npm run buildaus - Prüfe ob Überschriften jetzt IDs haben
-
Falls nicht: Überprüfe
addIdsToHeadingsAndGenerateSidebar()
Problem: ScrollSpy funktioniert nicht
Ursache:
- Script nicht geladen oder IDs fehlen
Lösung:
-
Prüfe ob
scroll-spy.jseingebunden ist - Öffne Browser-Konsole auf Fehler prüfen
- Prüfe ob Überschriften IDs haben
Problem: Rechte Sidebar zeigt falsche Tutorials
Ursache:
-
Falsche Unterkategorie oder
TUTORIAL_ORDERfehlt
Lösung:
-
Prüfe ob Unterkategorie in
TUTORIAL_ORDERexistiert -
Prüfe Ordnerstruktur (z.B.
/tutorials/css/css-basics/) -
Führe
npm run buildaus
Problem: Sidebar öffnet sich nicht
Ursache:
- JavaScript-Fehler oder falsche IDs
Lösung:
- Öffne Browser-Konsole
- Prüfe auf Fehler
-
Prüfe ob
sidebar.jsgeladen ist -
Prüfe HTML-IDs:
tutorialSidebar,sidebarToggle, etc.
Zusammenfassung
Das Sidebar-System ist ein Zusammenspiel mehrerer Komponenten:
-
Generierung:
generate-navigation.js - Basis: h2/h3 Überschriften
- Features: ScrollSpy, Smooth Scroll
-
Interaktion:
sidebar.js
-
Generierung:
generate-navigation.js -
Basis:
TUTORIAL_ORDER+getAllTutorialsForCategory() - Features: Active-Link, Gruppenüberschriften
-
Interaktion:
sidebar.js
- Beide Sidebars werden server-seitig generiert
- JavaScript ist nur für Interaktionen zuständig
- ScrollSpy tracked die Scroll-Position
- Alles ist responsive und performance-optimiert
Mehr aus DevPanicZone!
Tutorials werden geladen...