Modul 22 — Capstone: Statischer Site-Generator
Der Abschluss-Projekt: Baue einen Mini-Statischen-Site-Generator in Rust. Verbinde Parsing, Rendering, Templates, Testen und ein Binary im Workspace zu einem echten, vollständig getesteten Projekt.
Das Projekt: Ein eigener Site-Generator
Lektion 109 von 7
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:
- Markdown-artigen Text in Blöcke parsen (Überschriften, Absätze)
- Diese Blöcke als sauberes, sicheres HTML rendern
- Seiten in eine Vorlage (Layout) einbetten
- als Library + Binary im Workspace laufen
- 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.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Das Datenmodell: Blöcke
Lektion 110 von 7
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).
Übung: Block-Enum definieren
🧩 Übung m22-l2-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Parsen: Text in Blöcke zerlegen
Lektion 111 von 7
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.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Rendern: Sicheres HTML erzeugen
Lektion 112 von 7
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('&', "&")
.replace('<', "<")
.replace('>', ">")
}
Daraus wird <script>& zu <script>& — 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.
Übung: Blöcke zu HTML rendern
🧩 Übung m22-l4-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Layout: Seiten in eine Vorlage einbetten
Lektion 113 von 7
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).
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Workspace & CLI: Library + Binary
Lektion 114 von 7
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.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Absichern: Tests & die vollständige Übung
Lektion 115 von 7
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>&"), "<script>&");
}
#[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.
Rust in der realen Welt: KI-Werkzeuge und Systemprogrammierung
Rust ist nicht nur ein Lehrsprache — sie treibt aktuelle KI-Werkzeuge und Systemprojekte:
- OPENDEV (2026): Ein KI-Codierungsagent für das Terminal, in Rust geschrieben. Nutzt eine Dual-Agent-Architektur (Planung + Ausführung), adaptives Context-Compaction und ein Memory-System für projekt-spezifisches Wissen über Sessions hinweg.
- TreeCoder (2026): Ein Framework für Constraint-gesteuerte Code-Generierung mit LLMs — getestet auf Python, SQL und Rust. Erzwingt Korrektheit während des Decodings statt auf Prompt-Engineering zu vertrauen.
- Klang (2026): Eine Lehrsprache mit Rust-Execution-Core und drei Backends (Stack-VM, LLVM JIT/AOT, x86-64 NASM) — zeigt Rusts Rolle als Systemprogrammierung-Sprache.
Dein Capstone ist ein vollständiger Site-Generator: Parsing, Rendering, I/O, Tests. Dieselben Fähigkeiten — sichere Systemprogrammierung, Fehlerbehandlung, performante I/O — machen Rust zur Sprache der Wahl für KI-Infrastruktur.
Praxis-Szenario: Die Marketing-Seite
Die Marketing-Seite lädt drei Sekunden — WordPress mit 40 Plugins. Der CEO will schnell, sicher, wartbar. Du schlägst einen statischen Generator vor: Markdown rein, HTML raus, kein Server, keine Angriffsfläche.
Übung: Der komplette Site-Generator
🧩 Übung m22-l7-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren: