Navigation Script – So funktioniert's

Eine verständliche Erklärung, wie das generate-navigation.js Script alle Navigationen automatisch erstellt und aktualisiert.

Was macht das Script?

Das generate-navigation.js Script ist das Herzstück der automatischen Navigation auf DevPanicZone. Es durchsucht alle Tutorial-Dateien und erstellt automatisch:

  • Header & Footer auf allen Seiten
  • Main Navigation (Dropdown-Menü mit allen Kategorien)
  • Breadcrumbs (Navigationspfad: Home › Kategorie › Tutorial)
  • Prev/Next Navigation (Vorheriges/Nächstes Tutorial)
  • Sidebar TOC (Inhaltsverzeichnis aus Überschriften)
  • Sidebar Tutorial-Liste (Alle Tutorials der Kategorie)
  • Tutorial-Buttons (Übersicht auf Kategorieseiten)
Warum automatisch?

Stell dir vor, du müsstest bei jedem neuen Tutorial ALLE anderen Tutorial-Seiten manuell aktualisieren! Das Script macht das automatisch mit einem einzigen Befehl: npm run build

Aufbau des Scripts

Das Script ist in logische Abschnitte unterteilt. Lass uns jeden einzelnen durchgehen!

1. Konfiguration & Setup

Ganz am Anfang werden die wichtigsten Einstellungen definiert:

JavaScript
const fs = require('fs');
const path = require('path');
const { JSDOM } = require('jsdom');

// Pfade vom scripts/ Ordner aus
const tutorialsDir = path.join(__dirname, '..', 'tutorials');
const assetsDir = path.join(__dirname, '..', 'assets');

// Blacklist: Diese Ordner werden ignoriert
const BLACKLIST_FOLDERS = ['noupload', 'node_modules', '.git', 'assets'];

Was passiert hier?

  • fs → Zum Lesen und Schreiben von Dateien
  • path → Zum Arbeiten mit Dateipfaden
  • JSDOM → Zum Parsen und Manipulieren von HTML
  • tutorialsDir → Zeigt auf den /tutorials Ordner
  • BLACKLIST_FOLDERS → Ordner, die das Script ignorieren soll
Warum __dirname, '..' ?

Das Script liegt in /scripts/generate-navigation.js. Um auf /tutorials zu kommen, muss es einen Ordner HOCH .. und dann in den tutorials Ordner.

2. Deine Konfiguration

Diese Konstanten definierst DU – sie steuern, wie die Navigationen aussehen:

JavaScript
// Kategorie-Namen für Main-Navigation
const CATEGORY_NAMES = {
    'html': 'HTML',
    'css': 'CSS',
    'javascript': 'JavaScript'
};

// Reihenfolge der Tutorials
const TUTORIAL_ORDER = {
    'tutorials/html/html-basics': [
        'html-basics.html',
        'html-grundgeruest.html'
    ]
};

// Eigene Titel für Links
const CUSTOM_TITLES = {
    'tutorials/html/html-basics/html-basics.html': 'Was ist HTML?'
};

Das bedeutet:

  • CATEGORY_NAMES → Welche Kategorien im Dropdown erscheinen
  • TUTORIAL_ORDER → In welcher Reihenfolge Tutorials erscheinen (wichtig für Prev/Next!)
  • CUSTOM_TITLES → Alternative Namen für Links (z.B. statt "html-basics" → "Was ist HTML?")

Das Script hat fertige HTML-Templates für Header und Footer gespeichert:

JavaScript
const HEADER_TEMPLATE = `<!-- Header -->
<header class="site-header">
    <div class="container">
        <!-- Logo, Theme Toggle, Navigation... -->
    </div>
</header>`;

const FOOTER_TEMPLATE = `<footer class="site-footer">
    <div class="container">
        <!-- Copyright, Links... -->
    </div>
</footer>`;

Vorteil: Wenn du den Header ändern willst, änderst du ihn nur HIER – dann wird er auf ALLEN Seiten aktualisiert!

Hilfsfunktionen

Das Script hat kleine Helferlein, die bestimmte Aufgaben übernehmen:

findHtmlFiles() – HTML-Dateien finden

JavaScript
function findHtmlFiles(dir, fileList = []) {
    const files = fs.readdirSync(dir);
    files.forEach(file => {
        const filePath = path.join(dir, file);
        const stat = fs.statSync(filePath);
        
        if (stat.isDirectory()) {
            // Blacklist-Ordner ignorieren
            if (BLACKLIST_FOLDERS.includes(file)) {
                return;
            }
            // Rekursiv in Unterordner gehen
            findHtmlFiles(filePath, fileList);
        } else if (file.endsWith('.html')) {
            fileList.push(filePath);
        }
    });
    return fileList;
}

Was macht die Funktion?

  1. Liest alle Dateien in einem Ordner
  2. Ist es ein Ordner? → Prüfe, ob er auf der Blacklist steht
  3. Ist es keine Blacklist? → Gehe rekursiv in den Ordner (Funktion ruft sich selbst auf!)
  4. Ist es eine HTML-Datei? → Füge sie zur Liste hinzu
Was bedeutet "rekursiv"?

Die Funktion ruft sich selbst auf! Das ermöglicht es, durch ALLE Unterordner zu gehen, egal wie tief verschachtelt.

getCategoryFromPath() – Kategorie ermitteln

JavaScript
function getCategoryFromPath(filePath) {
    const relativePath = path.relative(tutorialsDir, filePath);
    const parts = relativePath.split(path.sep);
    return parts[0]; // Erste Ebene (html, css, etc.)
}

Beispiel:

  • Pfad: /tutorials/html/html-basics/html-basics.html
  • Relative Pfad: html/html-basics/html-basics.html
  • Aufteilen: ['html', 'html-basics', 'html-basics.html']
  • Erste Ebene: html ← Das ist die Kategorie!

getSubcategoryFromPath() – Unterkategorie ermitteln

JavaScript
function getSubcategoryFromPath(filePath) {
    const relativePath = path.relative(tutorialsDir, filePath);
    const parts = relativePath.split(path.sep);
    if (parts.length >= 2) {
        return 'tutorials/' + parts.slice(0, 2).join('/');
    }
    return 'tutorials/' + parts[0];
}

Beispiel:

  • Pfad: /tutorials/html/html-basics/html-basics.html
  • Parts: ['html', 'html-basics', 'html-basics.html']
  • Erste 2 Ebenen: ['html', 'html-basics']
  • Ergebnis: tutorials/html/html-basics

Warum wichtig?

Das Script braucht diese Info, um in TUTORIAL_ORDER die richtige Reihenfolge zu finden!

Jetzt wird's interessant! Diese Funktionen erstellen die eigentlichen Navigationen:

generateMainNavigation() – Dropdown-Menü

JavaScript
function generateMainNavigation() {
    const categories = Object.keys(CATEGORY_NAMES);
    const navItems = [];
    
    categories.forEach(category => {
        const categoryName = CATEGORY_NAMES[category];
        
        // Hat diese Kategorie Tutorials?
        const hasEntry = Object.keys(TUTORIAL_ORDER).some(key => 
            key.startsWith('tutorials/' + category + '/')
        );
        
        if (hasEntry) {
            navItems.push(
                `<li><a href="/tutorials/${category}/">${categoryName}</a></li>`
            );
        }
    });
    
    return navItems;
}

Schritt für Schritt:

  1. Hole alle Kategorien aus CATEGORY_NAMES
  2. Für jede Kategorie: Prüfe, ob es Tutorials dafür gibt (in TUTORIAL_ORDER)
  3. Wenn ja: Erstelle einen Link für das Dropdown
  4. Gib alle Links zurück

generateBreadcrumbs() – Navigationspfad

JavaScript
function generateBreadcrumbs(filePath, currentTitle) {
    const category = getCategoryFromPath(filePath);
    const categoryName = CATEGORY_NAMES[category] || category;
    const basename = path.basename(filePath);
    
    // Kategorieseite: Home › Kategorie
    if (basename === 'index.html') {
        return `<a href="/">Home</a>
                <span>›</span>
                <span>${categoryName}</span>`;
    }
    
    // Tutorial-Seite: Home › Kategorie › Tutorial
    return `<a href="/">Home</a>
            <span>›</span>
            <a href="/tutorials/${category}/">${categoryName}</a>
            <span>›</span>
            <span>${currentTitle}</span>`;
}

Unterschied:

  • Kategorieseite: Home › CSS (CSS ist nicht klickbar)
  • Tutorial-Seite: Home › CSS › Flexbox (CSS ist klickbar!)

generatePrevNextNav() – Vorheriges/Nächstes

JavaScript
function generatePrevNextNav(filePath) {
    const subcategory = getSubcategoryFromPath(filePath);
    const tutorials = getOrderedTutorials(subcategory);
    const currentIndex = tutorials.findIndex(t => t.filePath === filePath);
    
    if (currentIndex === -1) return '';
    
    const prev = currentIndex > 0 ? tutorials[currentIndex - 1] : null;
    const next = currentIndex < tutorials.length - 1 ? 
                 tutorials[currentIndex + 1] : null;
    
    let navHtml = '';
    
    // Vorheriges Tutorial
    if (prev) {
        navHtml += `<div class="tutorial-nav-prev">
            <a href="${prev.url}">
                <span>← Vorheriges</span>
                <span>${prev.title}</span>
            </a>
        </div>`;
    } else {
        navHtml += '<div class="tutorial-nav-prev"></div>';
    }
    
    // Nächstes Tutorial (DIREKT anschließend ohne Leerzeichen!)
    if (next) {
        navHtml += `<div class="tutorial-nav-next">
            <a href="${next.url}">
                <span>Nächstes →</span>
                <span>${next.title}</span>
            </a>
        </div>`;
    } else {
        navHtml += '<div class="tutorial-nav-next"></div>';
    }
    
    return navHtml;
}

Logik:

  1. Finde die Unterkategorie (z.B. tutorials/html/html-basics)
  2. Hole die Tutorial-Liste aus TUTORIAL_ORDER
  3. Finde die Position des aktuellen Tutorials in der Liste
  4. Vorheriges = Index - 1, Nächstes = Index + 1
  5. Erstelle HTML für beide Buttons (oder leere divs, wenn es keinen Prev/Next gibt)
Wichtig: Keine Leerzeichen!

Die beiden <div>-Elemente müssen DIREKT aneinander ohne Leerzeichen, damit das CSS sie nebeneinander anzeigt (Flexbox)!

addIdsToHeadingsAndGenerateSidebar() – Sidebar TOC

Diese Funktion macht mehrere Dinge auf einmal:

  1. Findet alle <h2> und <h3> Überschriften
  2. Fügt ihnen IDs hinzu (falls keine vorhanden)
  3. Prüft ob Custom Titles in TOC_CUSTOM_TITLES definiert sind
  4. Erstellt die Sidebar-Links mit Custom oder Original-Titel
JavaScript
function addIdsToHeadingsAndGenerateSidebar(html, filename) {
    const dom = new JSDOM(html);
    const doc = dom.window.document;
    const headings = doc.querySelectorAll('h2, h3');
    
    const anchors = [];
    const usedIds = new Set();
    
    headings.forEach(heading => {
        // Überschriften mit class="no-toc" überspringen
        if (heading.classList.contains('no-toc')) return;
        
        let id = heading.id;
        
        // ID generieren wenn keine vorhanden
        if (!id) {
            id = heading.textContent.trim().toLowerCase()
                .replace(/[äöüÄÖÜß]/g, match => {
                    const map = { 
                        'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 
                        'Ä': 'Ae', 'Ö': 'Oe', 'Ü': 'Ue', 'ß': 'ss' 
                    };
                    return map[match];
                })
                .replace(/[^\w\s-]/g, '')  // Sonderzeichen entfernen
                .replace(/\s+/g, '-');      // Leerzeichen → Bindestriche
            
            // Duplikate vermeiden
            let finalId = id;
            let counter = 1;
            while (usedIds.has(finalId)) {
                finalId = `${id}-${counter}`;
                counter++;
            }
            
            // ID ins Heading einfügen!
            heading.id = finalId;
            id = finalId;
        }
        
        usedIds.add(id);
        
        const level = heading.tagName === 'H2' ? 'toc-level-1' : 'toc-level-2';
        
        // Custom Title verwenden falls vorhanden, sonst Original-Text
        let displayTitle = heading.textContent.trim();
        if (TOC_CUSTOM_TITLES[filename] && TOC_CUSTOM_TITLES[filename][id]) {
            displayTitle = TOC_CUSTOM_TITLES[filename][id];
        }
        
        // Sidebar-Link erstellen
        anchors.push(
            `<li class="${level}"><a href="#${id}">${displayTitle}</a></li>`
        );
    });
    
    return {
        html: dom.serialize(),  // HTML mit IDs!
        anchors: anchors.join('\n')
    };
}

Beispiel Normal:

  • Überschrift: <h2>Was ist CSS?</h2>
  • Wird zu: <h2 id="was-ist-css">Was ist CSS?</h2>
  • Sidebar-Link: <a href="#was-ist-css">Was ist CSS?</a>

Beispiel mit Custom Title:

  • Überschrift: <h2 id="die-top-15-tags">HTML Cheat Sheet – Die Top 15 Tags</h2>
  • Custom Title in TOC_CUSTOM_TITLES: 'die-top-15-tags': 'Top 15 Tags'
  • Sidebar-Link: <a href="#die-top-15-tags">Top 15 Tags</a> (gekürzt!)
Custom Titles für TOC

Lange Überschriften können in der Sidebar zu breit werden. Mit TOC_CUSTOM_TITLES kannst du kurze, prägnante Titel für die Sidebar definieren, während die Original-Überschrift im Haupttext erhalten bleibt:

const TOC_CUSTOM_TITLES = {
    'html-basics.html': {
        'die-top-15-tags': 'Top 15 Tags',
        'html-attribute-erklaert': 'HTML Attribute'
    },
    'css-farben.html': {
        'rgb-rgba-hex': 'Farbformate'
    }
};
Wichtig:

Die ID muss die finale ID sein (nach Slug-Generierung), also z.B. 'die-top-15-tags' nicht 'die-Top-15-Tags'.

Warum Umlaute ersetzen?

IDs wie #für-anfänger können in URLs Probleme machen. Besser: #fuer-anfaenger

HTML aktualisieren

Die Hauptfunktion updateHtmlFile() koordiniert alle Updates:

JavaScript
function updateHtmlFile(filePath) {
    // 1. HTML-Datei einlesen
    let html = fs.readFileSync(filePath, 'utf8');
    
    // 2. Metadaten sammeln
    const category = getCategoryFromPath(filePath);
    const basename = path.basename(filePath);
    const currentTitle = CUSTOM_TITLES[basename] || extractTitle(html);
    
    // 3. IDs zu Headings hinzufügen (ZUERST!)
    let sidebarAnchorsHtml = '';
    if (basename !== 'index.html') {
        const result = addIdsToHeadingsAndGenerateSidebar(html);
        html = result.html;  // Nutze das HTML mit IDs!
        sidebarAnchorsHtml = result.anchors;
    }
    
    // 4. Header aktualisieren
    html = updateHeader(html);
    
    // 5. Footer aktualisieren
    html = updateFooter(html);
    
    // 6. Tutorial-Buttons (nur für Kategorieseiten)
    if (basename === 'index.html') {
        html = updateTutorialButtons(html, category);
    }
    
    // 7. Main Navigation einfügen
    const mainNavItems = generateMainNavigation();
    html = html.replace(
        /<!-- Main-Nav-Start -->.*?<!-- Main-Nav-End -->/s,
        `<!-- Main-Nav-Start -->\n${mainNavItems.join('\n')}\n<!-- Main-Nav-End -->`
    );
    
    // 8. Breadcrumbs einfügen
    if (isInTutorials) {
        const breadcrumbs = generateBreadcrumbs(filePath, currentTitle);
        html = html.replace(
            /<nav class="breadcrumbs">.*?<\/nav>/s,
            `<nav class="breadcrumbs">${breadcrumbs}</nav>`
        );
    }
    
    // 9. Prev/Next Navigation
    if (basename !== 'index.html') {
        const prevNext = generatePrevNextNav(filePath);
        html = html.replace(
            /<nav class="tutorial-nav">.*?<\/nav>/s,
            `<nav class="tutorial-nav">${prevNext}</nav>`
        );
    }
    
    // 10. Sidebar Anchors
    if (basename !== 'index.html') {
        html = html.replace(
            /<!-- Sidebar-Anchor-Start -->.*?<!-- Sidebar-Anchor-End -->/s,
            `<!-- Sidebar-Anchor-Start -->\n${sidebarAnchorsHtml}\n<!-- Sidebar-Anchor-End -->`
        );
    }
    
    // 11. Sidebar Navigation List
    if (basename !== 'index.html') {
        const navList = generateSidebarNavList(filePath);
        html = html.replace(
            /<ul class="sidebar-nav-list">.*?<\/ul>/s,
            `<ul class="sidebar-nav-list">\n${navList}\n</ul>`
        );
    }
    
    // 12. Datei speichern
    fs.writeFileSync(filePath, html, 'utf8');
}
Wichtig: Die Reihenfolge ist entscheidend!
  1. Zuerst IDs zu Headings hinzufügen
  2. Dann Header/Footer ersetzen
  3. Danach alle Navigationen einfügen
  4. Zum Schluss Datei speichern
Warum IDs zuerst?

Die Sidebar-Links verweisen auf IDs (#meine-ueberschrift). Diese IDs müssen also existieren, BEVOR die Sidebar generiert wird!

Regex Patterns erklärt

Das Script nutzt Regular Expressions (Regex), um HTML zu finden und zu ersetzen. Das sieht kompliziert aus, ist aber logisch:

Pattern für Kommentar-Marker

JavaScript
/<!-- Main-Nav-Start -->.*?<!-- Main-Nav-End -->/s

Aufschlüsselung:

  • / → Start des Regex
  • <!-- Main-Nav-Start --> → Findet den Start-Kommentar
  • .*? → Findet ALLES dazwischen (aber so wenig wie möglich)
  • <!-- Main-Nav-End --> → Findet den End-Kommentar
  • /s → "Dotall" Mode – . matched auch Zeilenumbrüche

Beispiel:

HTML
<!-- Main-Nav-Start -->
<li><a href="/tutorials/html/">HTML</a></li>
<li><a href="/tutorials/css/">CSS</a></li>
<!-- Main-Nav-End -->

Das Regex findet ALLES zwischen den Markern und ersetzt es mit neuem HTML!

Pattern für HTML-Tags

JavaScript
/<nav class="breadcrumbs">.*?<\/nav>/s

Aufschlüsselung:

  • <nav class="breadcrumbs"> → Findet das öffnende Tag
  • .*? → Findet den Inhalt
  • <\/nav> → Findet das schließende Tag (/ muss escaped werden!)
  • /s → Dotall Mode

Die Hauptlogik

Ganz am Ende des Scripts steht die Funktion, die alles startet:

JavaScript
function generateAllNavigation() {
    console.log('🚀 Starte Navigation-Generierung...\n');
    
    // 1. Alle HTML-Dateien finden
    const tutorialFiles = findHtmlFiles(tutorialsDir);
    
    console.log(`📄 Gefundene Tutorial-Dateien: ${tutorialFiles.length}\n`);
    
    // 2. Jede Datei aktualisieren
    tutorialFiles.forEach(filePath => {
        const relativePath = path.relative(tutorialsDir, filePath);
        console.log(`  ✓ Aktualisiere: ${relativePath}`);
        updateHtmlFile(filePath);
    });
    
    console.log('\n✅ Navigation erfolgreich generiert!');
}

// Script ausführen!
generateAllNavigation();

Ablauf:

  1. Finde alle HTML-Dateien im /tutorials Ordner
  2. Für jede gefundene Datei: Rufe updateHtmlFile() auf
  3. Zeige Fortschritt im Terminal
  4. Fertig!
Das war's!

Mit npm run build führst du dieses Script aus und es aktualisiert ALLE Seiten automatisch! 🎉

Best Practices & Tipps

1. Immer vor dem Committen ausführen

Gewöhne dir an, vor jedem Git-Commit npm run build auszuführen. So sind alle Navigationen garantiert aktuell!

2. Teste nach Änderungen

Nach größeren Änderungen am Script:

  • Führe npm run build aus
  • Öffne mehrere Tutorial-Seiten
  • Prüfe Prev/Next, Sidebar, Breadcrumbs
  • Teste die Links!

3. Backup vor Script-Änderungen

Wenn du das Script änderst:

  • Erstelle eine Kopie: generate-navigation-backup.js
  • Teste deine Änderungen auf einer Datei
  • Erst dann auf allen Dateien ausführen

4. Console-Logs nutzen

Füge bei Problemen Console-Logs hinzu:

JavaScript
console.log('Aktuelle Kategorie:', category);
console.log('Gefundene Tutorials:', tutorials);
console.log('Current Index:', currentIndex);

Häufige Probleme

Problem: Script findet keine Dateien

Ursache:

  • Blacklist oder Pfad falsch

Lösung:

  • Prüfe BLACKLIST_FOLDERS
  • Prüfe ob Script in /scripts/ liegt
  • Teste: console.log(tutorialsDir)

Problem: Prev/Next zeigt falsches Tutorial

Ursache:

  • Reihenfolge in TUTORIAL_ORDER falsch

Lösung:

  • Öffne generate-navigation.js
  • Finde die richtige Unterkategorie
  • Korrigiere die Reihenfolge
  • Führe npm run build aus

Problem: Sidebar-Links führen nirgends hin

Ursache:

  • IDs wurden nicht zu Headings hinzugefügt

Lösung:

  • Prüfe ob addIdsToHeadingsAndGenerateSidebar ausgeführt wird
  • Prüfe ob html = result.html verwendet wird
  • Führe npm run build nochmal aus

Problem: Dropdown fehlt eine Kategorie

Ursache:

  • Keine Tutorials in TUTORIAL_ORDER für diese Kategorie

Lösung:

  • Füge Tutorials zu TUTORIAL_ORDER hinzu
  • Oder: Entferne die Kategorie aus CATEGORY_NAMES

Zusammenfassung

Das generate-navigation.js Script ist ein mächtiges Werkzeug, das dir viel manuelle Arbeit abnimmt:

Was es macht:

  • Findet alle HTML-Dateien
  • Erstellt Header & Footer
  • Generiert alle Navigationen
  • Fügt IDs zu Überschriften hinzu
  • Aktualisiert alle Seiten

Was du tun musst:

  • Neue Tutorials in TUTORIAL_ORDER eintragen
  • Optional: Custom-Titel in CUSTOM_TITLES
  • npm run build ausführen
  • Fertig!

Mehr aus DevPanicZone!

Tutorials werden geladen...