Skip to content
Lektion 109 von 1150/115 abgeschlossen
Modul 22 — Capstone: Statischer Site-Generator

Das Projekt: Ein eigener Site-Generator

Willkommen zum Capstone

Dieses Modul fasst dein bisheriges Wissen zu einem echten Projekt zusammen: einem Statischen Site-Generator (SSG). Ein SSG verwandelt Textdateien in HTML-Seiten — so funktionieren unzählige Blogs und Dokumentation- Seiten (z. B. viele Rust-Crates).

Dein Mini-SSG soll:

  1. Markdown-artigen Text in Blöcke parsen (Überschriften, Absätze)
  2. Diese Blöcke als sauberes, sicheres HTML rendern
  3. Seiten in eine Vorlage (Layout) einbetten
  4. als Library + Binary im Workspace laufen
  5. mit Tests gegen Regressionsfehler abgesichert sein

Das verknüpft Module 6 (Collections), 7 (Fehler), 8 (Traits/Generics), 10 (Closures/Iteratoren), 15 (Testen), 16 (Workspace & cargo), 18 (I/O) und 19 (Makros).

Warum dieses Projekt?

Ein SSG ist vollständig im Browser-Sandbox lauffähig: Es arbeitet auf Text im Speicher (String → Blöcke → HTML) statt auf Dateien oder Netzwerk. So kannst du jeden Schritt direkt ausprobieren — und am Ende trotzdem die Dateisystem-Version als echtes CLI verstehen.

📝 Schnellprüfung

Was ist die Kernaufgabe eines Statischen Site-Generators?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 110 von 1150/115 abgeschlossen
Modul 22 — Capstone: Statischer Site-Generator

Das Datenmodell: Blöcke

Ein Enum für den Inhalt

Das Herzstück ist unser Datenmodell: ein Block-Enum, das die Bausteine einer Seite beschreibt. Der Parser füllt es, der Renderer liest es — beide hängen nur an diesem Typ:

#[derive(Debug, PartialEq, Clone)]
pub enum Block {
    Heading(u32, String),   // Level (1,2,3...) und Text
    Paragraph(String),
}

Heading(u32, String) ist eine Struct-Variante mit zwei Feldern: der Überschrifts-Ebene und dem Text. PartialEq und Debug erhalten wir per derive — wichtig für Tests (assert_eq!) und Fehlermeldungen.

Enum als gemeinsame Schnittstelle

Parser und Renderer kommunizieren nur über den Block-Typ. Solange beide denselben Typ kennen, kannst du sie unabhängig testen und erweitern. Ein sauber gewähltes Datenmodell ist die halbe Architektur — eine Lektion aus Modul 5 (Enums) und 8 (Traits).

📝 Schnellprüfung

Warum ist PartialEq auf unserem Block-Enum wichtig?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 111 von 1150/115 abgeschlossen
Modul 22 — Capstone: Statischer Site-Generator

Parsen: Text in Blöcke zerlegen

Von Zeilen zu Blöcken

Unser Parser liest den Text Zeile für Zeile und baut Blöcke:

  • Zeilen, die mit # beginnen → Überschrift (Anzahl der # = Ebene)
  • leere Zeile → schließt einen laufenden Absatz
  • sonst → sammelt die Zeile in den aktuellen Absatz
pub fn parse(text: &str) -> Vec<Block> {
    let mut blocks = Vec::new();
    let mut para = Vec::new();

    for line in text.lines() {
        if line.starts_with('#') {
            if !para.is_empty() {
                blocks.push(Block::Paragraph(para.join(" ")));
                para.clear();
            }
            let level = line.chars().take_while(|&c| c == '#').count() as u32;
            let content = line
                .chars()
                .skip(level as usize)
                .collect::<String>()
                .trim()
                .to_string();
            blocks.push(Block::Heading(level, content));
        } else if line.trim().is_empty() {
            if !para.is_empty() {
                blocks.push(Block::Paragraph(para.join(" ")));
                para.clear();
            }
        } else {
            para.push(line.trim().to_string());
        }
    }
    if !para.is_empty() {
        blocks.push(Block::Paragraph(para.join(" ")));
    }
    blocks
}

Ein Absatz über mehrere Zeilen wird mit join(" ") zu einem Paragraph verbunden.

Das Zustands-Muster im Parser

Der Parser hält einen kleinen Zustand (para) während er die Zeilen durchläuft. Bei Strukturwechseln (Überschrift, Leerzeile) "leert" er den Zustand in die Ausgabe. Dieses Muster — Zustand halten, an Grenzen freigeben — taucht in fast jedem Parser wieder auf.

📝 Schnellprüfung

Was passiert, wenn der Parser eine leere Zeile trifft?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 112 von 1150/115 abgeschlossen
Modul 22 — Capstone: Statischer Site-Generator

Rendern: Sicheres HTML erzeugen

Von Blöcken zu HTML

Der Renderer macht das Gegenteil des Parsers: Er übersetzt Blocks in HTML-Strings:

pub fn render_block(block: &Block) -> String {
    match block {
        Block::Heading(level, text) => format!("<h{level}>{}</h{level}>", escape(text)),
        Block::Paragraph(text) => format!("<p>{}</p>", escape(text)),
    }
}

HTML-Escaping ist Sicherheitspflicht

Nutzertext darf nie unverändert in HTML landen — sonst ist ein <script>-Einschub („XSS") möglich. Wir ersetzen die gefährlichen Zeichen:

pub fn escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
}

Daraus wird <script>& zu &lt;script&gt;&amp; — im Browser harmloser Text.

Trust boundaries: Nutzereingabe != HTML

Jede Stelle, an der fremder Text in eine Ausgabe fließt, ist eine Trust Boundary. Das Escaping gehört in den Renderer, bevor HTML gebaut wird — nicht an den Aufrufer, und nicht weglassbar. Das ist kein Nice-to-have, sondern Grundlage sicherer Ausgaben.

📝 Schnellprüfung

Warum escapen wir HTML-Sonderzeichen?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 113 von 1150/115 abgeschlossen
Modul 22 — Capstone: Statischer Site-Generator

Layout: Seiten in eine Vorlage einbetten

Die ganze Seite zusammenbauen

Eine echte HTML-Seite braucht <!DOCTYPE html>, <head>, <title> und <body>. Unsere render_page-Funktion baut das vollständige Dokument:

pub fn render_page(title: &str, blocks: &[Block]) -> String {
    let body: String = blocks
        .iter()
        .map(render_block)
        .collect::<Vec<_>>()
        .join("\n");
    format!(
        "<!DOCTYPE html>\n<html>\n<head><meta charset=\"utf-8\">\
        \n<title>{}</title></head>\n<body>\n{}\n</body>\n</html>\n",
        escape(title),
        body
    )
}

blocks.iter().map(render_block) wandelt jeden Block in HTML um (map), sammelt das Ergebnis (collect) und fügt die Teile mit \n zusammen (join) — ein Paradebeispiel für die Iterator-Werkzeuge aus Modul 10.

In einen Writer schreiben (I/O)

Statt nur Strings zu liefern, nutzen wir das Write-Trait (Modul 18) — so können wir in Vec<u8>, Dateien oder sogar Netzwerk-Streams schreiben:

use std::io::Write;

pub fn build_to_writer<W: Write>(
    w: &mut W,
    title: &str,
    blocks: &[Block],
) -> std::io::Result<()> {
    let html = render_page(title, blocks);
    w.write_all(html.as_bytes())
}

<W: Write> ist ein generischer Parameter: Die Funktion akzeptiert jeden Schreiber, der das Write-Trait erfüllt.

Generisch über Write = maximale Wiederverwendung

Dank W: Write ist unser Builder nicht an konkrete Ausgaben gekoppelt: Im Sandbox-Test schreiben wir in einen Vec<u8>, in der CLI-Version in eine Datei via std::fs::File — beides implementiert Write. Ein Interface, viele Ziele (Modul 8, 18).

📝 Schnellprüfung

Was ermöglicht der generische Parameter W: Write?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 114 von 1150/115 abgeschlossen
Modul 22 — Capstone: Statischer Site-Generator

Workspace & CLI: Library + Binary

Trennung von Logik und Einstieg

Unser Generator lebt in einer Library (src/lib.rs) — reine, wiederverwendbare Logik. Der Einstiegspunkt (Binary) ist ein dünnes CLI, das Dateien liest und HTML schreibt:

// src/main.rs (Binary) — nur der Einstieg
use std::fs;
use minissg::{parse, render_page};

fn main() -> std::io::Result<()> {
    let src = fs::read_to_string("index.md")?;
    let blocks = parse(&src);
    let html = render_page("Meine Seite", &blocks);
    fs::write("index.html", html)?;
    Ok(())
}

In der Cargo.toml ist die Binary nur eine Datei, die die Library als Pfad-Abhängigkeit nutzt (Workspace-Konzept aus Modul 16):

[package]
name = "minissg"

[dependencies]
minissg-lib = { path = "lib" }

Dünne Binary, dicke Library

Der gesamte Test- und Wartungsaufwand steckt in der Library — dort liegen Parsing, Rendering und Layout. Die Binary ist bewusst dünn: Sie verdrahtet nur Datei → Parsen → Rendern → Datei. Dank der Trennung kannst du die Logik per Unit-Tests prüfen, ohne ein CLI zu starten.

📝 Schnellprüfung

Warum liegt die Logik in einer Library statt in der Binary?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 115 von 1150/115 abgeschlossen
Modul 22 — Capstone: Statischer Site-Generator

Absichern: Tests & die vollständige Übung

Testen gegen Regressionsfehler

Ein Projekt ist nur fertig, wenn es absicherbar ist. Unsere drei wichtigsten Tests decken Parser, Sicherheit und Ausgabe ab:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_headings_and_paragraphs() {
        let blocks = parse("# Titel\n\nein Absatz.\nund weiter.");
        assert_eq!(
            blocks,
            vec![
                Block::Heading(1, "Titel".to_string()),
                Block::Paragraph("ein Absatz. und weiter.".to_string()),
            ]
        );
    }

    #[test]
    fn escapes_html() {
        assert_eq!(escape("<script>&"), "&lt;script&gt;&amp;");
    }

    #[test]
    fn renders_writer_output() {
        let mut buf = Vec::new();
        let blocks = parse("# Hi");
        build_to_writer(&mut buf, "Seite", &blocks).unwrap();
        let html = String::from_utf8(buf).unwrap();
        assert!(html.contains("<h1>Hi</h1>"));
        assert!(html.contains("<title>Seite</title>"));
    }
}

Jeder Test sichert eine Eigenschaft: sinnvolles Parsen, sichere Ausgabe, korrekter Writer-Pfad. Diese drei zusammen fangen die häufigsten Regressionsfehler.

Was macht ein gutes Capstone aus?

Ein Capstone verbindet viele Module zu einem getesteten, funktionierenden Ganzen — nicht zu einem Haufen endloser Features. Wer hier Parst, rendert, Layout, Workspace und Tests beherrscht, hat den Rust-Pfad dieses Kurses durchdrungen: von Grundlagen bis zu sicherer, wiederverwendbarer Programmarchitektur.

📝 Schnellprüfung

Welchem Zweck dient der escapes_html-Test?

Übung: Der komplette Site-Generator

🧩 Übung m22-l7-e1

🔮 Vorhersage: Was passiert beim Ausführen?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Modul-Checkpoint

Bestehe das Modul mit 80 %. Der Versuch zählt — beantwortete Fragen werden nicht erneut angezeigt; erst nach dem Absenden gibt es Auflösung und Erklärung.

Checkpoint: Modul 22 — Statischer Site-Generator

Bestehensgrenze: 80 %. Der Versuch wird bewertet und zählt für das Modul-Gate.

This site uses essential cookies for Stripe payments. No tracking cookies.