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:

Linke Sidebar (TOC)
  • Inhalt: Inhaltsverzeichnis der aktuellen Seite
  • Generierung: Server-seitig via generate-navigation.js
  • Basis: h2 & h3 Überschriften
  • Features: ScrollSpy, Smooth Scroll
Rechte Sidebar (Tutorial-Nav)
  • Inhalt: Alle Tutorials der Kategorie
  • Generierung: Server-seitig via generate-navigation.js
  • Basis: TUTORIAL_ORDER Konfiguration
  • Features: Active-Link Highlighting
Wichtig: Server-seitig vs. Browser-seitig Beide Sidebars werden server-seitig generiert!
  • Server-seitig: HTML wird beim npm run build erstellt 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

Plain Text
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

  1. Build-Zeit: generate-navigation.js erstellt HTML für beide Sidebars
  2. HTML-Datei: Sidebar-HTML ist fest im HTML eingebettet
  3. Seite lädt: Browser rendert die Sidebars (noch nicht sichtbar)
  4. JavaScript lädt: sidebar.js und scroll-spy.js werden aktiv
  5. User-Interaktion: Klick auf Button → Sidebar öffnet sich
  6. 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:

JavaScript
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

  1. Finde alle <h2> und <h3> Überschriften
  2. Überspringe Überschriften mit class="no-toc"
  3. Generiere eine ID aus dem Überschriften-Text (z.B. "Was ist CSS?" → "was-ist-css")
  4. Füge die ID direkt in die Überschrift ein: <h2 id="was-ist-css">
  5. Erstelle einen Link: <a href="#was-ist-css">Was ist CSS?</a>
  6. Gib sowohl das modifizierte HTML als auch die Links zurück

2. HTML-Struktur

Das generierte HTML wird zwischen den Markern eingefügt:

HTML
<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>
CSS-Klassen erklärt
  • .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:

JavaScript
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?

  1. Klasse .is-open wird zur Sidebar hinzugefügt
  2. CSS transform bewegt die Sidebar ins Sichtfeld
  3. Overlay wird sichtbar (dunkler Hintergrund)
  4. 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:

JavaScript
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 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:

JavaScript
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:

  1. Finde alle Links in der TOC (.toc-list a)
  2. Für jeden Link: Finde die zugehörige Überschrift via ID
  3. Speichere Link + Überschrift zusammen in einem Array
JavaScript
// 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:

  1. Hole aktuelle Scroll-Position + 100px Offset
  2. Durchlaufe alle Überschriften von oben nach unten
  3. Finde die Überschrift, die gerade sichtbar ist (scrollPosition >= headingTop)
  4. Das ist der aktive Abschnitt!
Warum 100px Offset?

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:

JavaScript
onScroll() {
    if (!this.ticking) {
        window.requestAnimationFrame(() => {
            this.updateActiveLink();
            this.ticking = false;
        });
        this.ticking = true;
    }
}

Was passiert hier?

  • requestAnimationFrame wartet bis zum nächsten Browser-Repaint
  • Dadurch läuft die Funktion maximal 60x pro Sekunde (statt 1000x!)
  • ticking Flag verhindert mehrfache Aufrufe

4. Active-Klasse setzen

Wenn der aktive Abschnitt sich ändert:

  1. Entferne .active von ALLEN TOC-Links
  2. Füge .active zum aktiven Link hinzu
  3. CSS hebt den Link hervor (z.B. blaue Farbe, fetter Text)
CSS
.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)

JavaScript
// 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:

JavaScript
function generateSidebarNavList(filePath) {
    // 1. Finde die Unterkategorie
    const subcategory = getSubcategoryFromPath(filePath);
    // z.B. "tutorials/css/css-basics"
    
    // 2. Hole die Tutorial-Liste aus TUTORIAL_ORDER
    const tutorials = getOrderedTutorials(subcategory);
    
    // 3. Finde die aktuelle URL
    const currentUrl = getRelativeUrl(filePath);
    
    // 4. Erstelle HTML für jedes Tutorial
    return tutorials.map(tutorial => {
        const isActive = tutorial.url === currentUrl;
        const activeClass = isActive ? ' class="active"' : '';
        return `<li><a href="${tutorial.url}"${activeClass}>${tutorial.title}</a></li>`;
    }).join('\n');
}

Schritt für Schritt:

  1. Ermittle die Unterkategorie aus dem Dateipfad
  2. Hole die Tutorial-Liste aus TUTORIAL_ORDER
  3. Für jedes Tutorial: Erstelle einen Link
  4. Das aktuelle Tutorial bekommt class="active"
  5. Füge alle Links zusammen

2. HTML-Struktur

Das generierte HTML wird automatisch in die rechte Sidebar eingefügt:

HTML
<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">
            <li><a href="/tutorials/css/css-basics/css-einbinden.html">CSS einbinden</a></li>
            <li><a href="/tutorials/css/css-basics/css-selektoren.html" class="active">CSS Selektoren</a></li>
            <li><a href="/tutorials/css/css-basics/css-box-model.html">CSS Box Model</a></li>
        </ul>
    </div>
</aside>

3. Interaktivität (sidebar.js)

Das Öffnen/Schließen funktioniert genau wie bei der linken Sidebar:

JavaScript
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)

  1. generate-navigation.js wird ausgeführt
  2. Es findet alle HTML-Dateien
  3. 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
  4. Ergebnis: HTML-Dateien mit fertigem Sidebar-HTML

Phase 2: Seite lädt im Browser

  1. Browser lädt HTML-Datei
  2. Beide Sidebars sind im HTML, aber nicht sichtbar (CSS: transform: translateX(-100%))
  3. CSS-Dateien laden:
    • style.css → Basis-Styling
    • sidebar.css → Sidebar-Styling
  4. 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

  1. Button-Click Event
  2. sidebar.js fügt .is-open Klasse hinzu
  3. CSS Transition schiebt Sidebar ins Sichtfeld
  4. Overlay wird sichtbar

User scrollt auf der Seite

  1. Scroll Event
  2. scroll-spy.js prüft Scroll-Position
  3. Findet aktive Überschrift
  4. Fügt .active Klasse zum passenden TOC-Link hinzu
  5. CSS hebt den Link hervor

User klickt auf TOC-Link

  1. Link-Click Event
  2. sidebar.js verhindert Standard-Verhalten
  3. Smooth Scroll zur Überschrift (mit 100px Offset)
  4. Sidebar schließt sich automatisch
  5. scroll-spy.js aktualisiert aktiven Link

CSS-Architektur

Die Sidebars nutzen moderne CSS-Features für Performance und Responsiveness:

Positionierung (Fixed)

CSS
.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?

  • transform nutzt GPU-Beschleunigung → butterweiche Animation
  • left triggert Reflow → langsamer und ruckelig

Open-State

CSS
.tutorial-sidebar.is-open {
    transform: translateX(0); /* Schiebt Sidebar ins Sichtfeld */
}

Responsive Design

CSS
/* 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:

HTML
<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 build ausfü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: true für Scroll-Events

Häufige Probleme

Ursache:

  • IDs fehlen in den Überschriften

Lösung:

  • Führe npm run build aus
  • 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.js eingebunden 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_ORDER fehlt

Lösung:

  • Prüfe ob Unterkategorie in TUTORIAL_ORDER existiert
  • Prüfe Ordnerstruktur (z.B. /tutorials/css/css-basics/)
  • Führe npm run build aus

Problem: Sidebar öffnet sich nicht

Ursache:

  • JavaScript-Fehler oder falsche IDs

Lösung:

  • Öffne Browser-Konsole
  • Prüfe auf Fehler
  • Prüfe ob sidebar.js geladen ist
  • Prüfe HTML-IDs: tutorialSidebar, sidebarToggle, etc.

Zusammenfassung

Das Sidebar-System ist ein Zusammenspiel mehrerer Komponenten:

Linke Sidebar (TOC)
  • Generierung: generate-navigation.js
  • Basis: h2/h3 Überschriften
  • Features: ScrollSpy, Smooth Scroll
  • Interaktion: sidebar.js
Rechte Sidebar (Tutorial-Nav)
  • Generierung: generate-navigation.js
  • Basis: TUTORIAL_ORDER
  • Features: Active-Link
  • Interaktion: sidebar.js
Das Wichtigste
  • 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...