--- name: generate-lesson description: Generiert eine vollständige Japanisch-Lektion (Lesson + LessonPages + LessonContent + QuizQuestions) für japanese-learning.ch. Claudio schreibt die Inhalte (Kana/Kanji/Vokabeln/Grammatik/Quiz) selbst — keine OpenAI/Gemini-API-Aufrufe ausser für Bilder (DALL-E). Auto-aktivieren, wenn Claudio "neue Lektion", "Lektion generieren", "Content für N5/N4", "Mayuko braucht eine Lektion über X", "erstelle eine Lektion zu Thema Y" sagt oder per `/generate-lesson` aufruft. Nutzt JLPT-Vokabellisten und Minna no Nihongo als Ausgangsquellen, schreibt direkt in die lokale Postgres-DB (docker-compose), verifiziert via Playwright, committet via Git. --- # generate-lesson — Anfänger-Lektionen für japanese-learning.ch ## Auftrag Erstelle eine **komplette, sofort nutzbare Lektion** für deutschsprachige Anfänger (inkl. Claudio selbst, der die Seite dogfoodet). **Du schreibst den gesamten Text-Content selbst** (keine OpenAI/Gemini-Calls). Der Skill ist der Orchestrator: Er gibt dir Schema, Guardrails und die Persistierungs-/Verifikations-/Git-Schritte vor; du produzierst den japanischen Content als strukturiertes JSON. Mayuko (Claudios Frau, japanische Lehrerin) prüft Inhalte fachlich — sie ist Reviewerin, nicht Lernerin. **Wichtig:** Mayuko wird intern als Fachreviewerin geführt, ist aber **öffentlich nicht erwähnt** (nicht auf der Website, nicht in Marketing-Texten, nicht in PR). Aktuell — kann sich später ändern. **Einzige Ausnahme: Bilder.** Für `thumbnail_url` und `Vocabulary.image_url` kannst du DALL-E per Script aufrufen (siehe §7). --- ## 1. Start-Check (immer zuerst) Bevor du überhaupt Content schreibst: 0. **Cloud-Sync-Status prüfen.** Liegt `.last_cloud_sync.json` vor und ist der `taken_at`-Timestamp <12h alt? Wenn nein (oder Datei fehlt): Empfehle dem User vor dem Generieren einen `/sync-cloud-db` Cloud→Lokal-Pull. Grund: Der spätere Push vergleicht den lokalen Stand gegen diesen Snapshot — ohne aktuellen Snapshot scheitert der Push am Drift-Check (seit Audit 2026-04-25). Wenn der User explizit ohne vorherigen Pull arbeiten will, OK — er weiss dann, dass beim Push ein Drift-Fehler kommen kann. 1. **Lies [learnings.md](learnings.md).** Dort steht, was in vorherigen Runs geklappt hat und was nicht. Wende diese Regeln strikt an. 2. **Lies [improve-jpl/SKILL.md](../improve-jpl/SKILL.md).** Die Produkt-Vision (Anfänger-First mit Mayuko-Fachreview, JLPT-Leitprinzip §1.5, Nicht-Ziele) gilt uneingeschränkt. - **Coverage prüfen** vor Themen-Wahl: `python .claude/skills/generate-lesson/pipeline.py coverage 5 --show-missing 30` zeigt fehlende N5-Vokabeln/Kanji. Themen so wählen, dass möglichst viele fehlende Items abgedeckt werden, statt schon vorhandene zu doppeln. - **Validator ist STRENG** (Mayuko-Direktive 2026-04-25): jedes Vokabel-Wort muss in `sources/jlpt_n5_canonical.json` stehen, sonst ERROR. Eigennamen: `data.is_proper_noun=true`. Bewusste Ausnahmen: `data.is_canonical_override=true` + `data.source_note="…"`. Kanji im `example_sentence_japanese` müssen im N5-Kanji-Set stehen — sonst Hiragana schreiben. 3. **Docker-Stack muss laufen — zweistufiger Check:** - **a) Docker-Desktop-Prozess:** `docker compose ps db` schlägt mit "cannot find the file specified" / "docker daemon not running" fehl, wenn Docker Desktop nicht läuft. In dem Fall: `Start-Process "C:\Program Files\Docker\Docker\Docker Desktop.exe"` (PowerShell) — Start dauert 30–60 s. - **b) DB-Container:** Danach `docker compose up db -d` und mit `docker exec postgres_db pg_isready -U app_user -d japanese_learning` warten, bis "accepting connections" erscheint. 4. **DB-Status prüfen:** Führe `python .claude/skills/generate-lesson/pipeline.py status` aus. Das zeigt, welche Themen/JLPT-Level wenig approved Content haben — Grundlage für Vorschlag bei Aufruf ohne Argumente. 5. **Admin-Credentials aus `.env`:** Für alle verifikationsbezogenen Logins gilt `ADMIN_EMAIL` und `ADMIN_PASSWORD` aus der lokalen `.env`. NIEMALS hardcoden. Login-Form-Feld heisst **`email`** (nicht `username`), Post-Login-Redirect ist **`/admin`** (nicht `/dashboard`). ## 2. Input-Modi | Aufruf | Verhalten | |---|---| | `/generate-lesson` (ohne Args) | DB-Gap-Analyse → 3 Themen-Vorschläge mit Begründung → User wählt | | `/generate-lesson N5 Familie` | Direktes Thema, JLPT-Level N5. Wenn thematisch passendes MNN-Kapitel existiert, **orientiere dich daran** (siehe §2a). | | `/generate-lesson --from-mnn 3` | Quelle: Minna no Nihongo Kapitel 3 (lies `scripts/mnn_data/beginner1_lesson03.json`). Siehe §2a. | | `/generate-lesson --from-jlpt N5` | Zufälliges noch nicht abgedecktes N5-Thema | | `/generate-lesson Hiragana` / `Katakana` | **Schreibsystem-Lektion** — Draft mit `"kind": "kana"` statt Vocabulary/Grammar. Siehe §2b. | ## 2a. Minna-no-Nihongo-Quellen (WICHTIG) **Rohdaten liegen vor:** `scripts/mnn_data/beginner1_lesson01.json` … `beginner2_lesson50.json` (50 Lektionen). Jede Datei enthält `vocabulary[]`, `vocabulary_countries[]`, `grammar[]`, `conversation{title, lines[]}` und teils `additional_conversations[]`. Die bestehenden 10 Lektionen in der DB (IDs 131–141, Titel `MNN L1…L5` EN+DE) wurden per `scripts/import_mnn.py` **direkt** importiert, ohne AI, als wörtliche Übernahme. **Wie Claude MNN nutzt:** 1. **MNN ist Vorlage, nicht Copy-Paste.** Die bestehenden 10 Lektionen sind Wort-für-Wort-Import. Deine Aufgabe: auf derselben thematischen/grammatikalischen/vokabulären **Linie** eine **neue Lektion** schreiben, mit: - **Anderen Namen/Personen.** MNN nutzt Mike Miller, Satou Keiko, Guputa-san, Yamada. Du nutzt andere Namen (z.B. Tanaka Haruto, Lisa Weber, Ueno Sensei). Keine MNN-Originalfiguren wiederverwenden. - **Leicht anderen Beispielsätzen** — gleiche Grammatik, gleiche Vokabelsätze, aber neu formuliert. Kein Satz wird 1:1 aus der MNN-JSON übernommen. - **Gleichem Grammatik-Kern** — wenn MNN-Kapitel 3 Demonstrativpronomen lehrt, lehrst auch du Demonstrativpronomen, mit denselben Strukturen (`これ/それ/あれ + は + N + です`). 2. **Vokabeln übernehmen:** Die Vokabel-Liste aus MNN-JSON darfst du weitgehend übernehmen (Vocabulary-Tabelle ist eh dedupliziert — Wörter wie `わたし` sind schon da). Füge Romaji hinzu, wenn fehlt. 3. **Konversation ist Pflicht** — siehe §4 Page-Struktur "Dialog/Anwendung". Nutze dieselbe `speaker / japanese / romaji / english`-Struktur wie in der MNN-JSON, aber mit deinen neuen Namen und leicht anderem Verlauf. 4. **Zusätzlicher DB-Gap-Check:** Bevor du eine neue Lektion zu MNN-Kapitel N schreibst, prüfe ob `MNN L{N}:` bereits existiert (`Lesson.title` LIKE `MNN L{N}:%`). Wenn ja, wähle einen Titel ohne `MNN L{N}:`-Präfix, damit es keine Kollision mit dem Original-Import gibt (z.B. `N5 Selbstvorstellung — Tanaka trifft Lisa`). **Konversations-Plaintext-Format** (entspricht `_format_conversation` in `scripts/import_mnn.py:170`): ``` Tanaka: こんにちは。わたしは たなかです。 (Konnichiwa. Watashi wa Tanaka desu.) → Guten Tag. Ich bin Tanaka. Lisa: はじめまして。リサです。ドイツから きました。 (Hajimemashite. Risa desu. Doitsu kara kimashita.) → Freut mich. Ich bin Lisa. Ich komme aus Deutschland. ``` Leerzeile zwischen Sprechern, Einrückung der Romaji-Zeile mit zwei Leerzeichen, `→` für die deutsche Übersetzung. ## 2b. Schreibsystem-Lektionen (`kind: "kana"`) — Hiragana / Katakana Hiragana- und Katakana-Lektionen sind eine **Sonderform**. Statt JLPT-Vokabeln und Grammatik wird das **Schriftsystem selbst** unterrichtet. Die Pipeline kennt dafür den Discriminator `"kind": "kana"` im Draft (Default ist `"vocabulary"`, kann weggelassen werden). **Was anders ist als bei einer Vocabulary-Lektion:** | Bereich | Vocabulary-Lektion (Default) | Kana-Lektion (`kind: "kana"`) | |---|---|---| | Lesson-`kind`-Feld | `"vocabulary"` (oder weglassen) | `"kana"` (Pflicht!) | | Vokabel-Einträge | 15–25 Pflicht | **0 erlaubt** (Validator bricht ab) | | Grammatik-Einträge | 2–4 Pflicht | **0 erlaubt** (Validator bricht ab) | | Kana-Einträge | 0 (oder optional) | **5–20 Pflicht** | | Quiz-Fragen | 10–18 | 8–16 | | Pages | ≥5 | ≥4 | | Dialog-Page | Pflicht | weggelassen (kein Konversationskontext) | | Audio-Pipeline | Pflicht | **übersprungen** (keine Dialog-Page) | | Slideshow-Pipeline | Pflicht | **übersprungen** | | text-audio | Pflicht für Prosa-Pages | bleibt aktiv für Markdown-Pages | | Vokabel-Bilder | jede Vokabel | **entfällt** (keine Vokabeln) | | Thumbnail | Pflicht | Pflicht | | N5-Canonical-Vokabel-Check | aktiv | übersprungen | | N5-Kanji-Disziplin-Check | aktiv | übersprungen (Kana-Lektion enthält per Definition keine Kanji-Beispielsätze) | | Romaji/Umlaut/Markdown-Hierarchie-Check | aktiv | bleibt aktiv | | Modul-Zuweisung | passendes N5-Themen-Modul | `n5-hiragana` (id=30) bzw. `n5-katakana` (id=31) | **Page-Struktur einer Kana-Lektion (Zielbild):** ``` Lesson (kind="kana", title="Hiragana 1 — …", jlpt_level=5, thumbnail_url=DALL-E-URL) ├─ LessonPage 1: "Einführung" (page_type='normal') │ └─ LessonContent: text — Was ist Hiragana? Warum lernen? Wie liest man die │ Tabelle? Markdown-Hierarchie Pflicht (## H2 + **bold** + Liste/Quote). │ ├─ LessonPage 2: "Die Zeichen" (page_type='normal') │ └─ LessonContent: kana ×N — jede Zeile ein Kana-Element mit │ character + romanization + type. Optional stroke_order_info / │ example_sound_url. │ ├─ LessonPage 3: "Aussprache & Schreibhinweise" (page_type='normal') │ └─ LessonContent: text — Wie spricht man die Vokale? Welche Reihen- │ Strukturen wiederholen sich? Welche Häkchen unterscheiden は/ほ? │ Markdown-Hierarchie Pflicht. │ ├─ LessonPage 4: "Übung" (page_type='quiz_carousel') │ └─ LessonContent mit QuizQuestions ×8-16 — Mix aus │ multiple_choice (Zeichen → Romaji) + matching (5 Paare Zeichen↔Romaji) │ + true_false (z.B. "「く」 wird 'ku' gelesen."). │ └─ LessonPage 5: "Zusammenfassung & nächste Schritte" (page_type='normal') └─ LessonContent: text — Wiederholung, Lerntipps, Vorschau auf nächste Hiragana-Lektion. ``` **Schema des `kana`-Content-Items im Draft:** ```json { "content_type": "kana", "data": { "character": "あ", "romanization": "a", "type": "hiragana", "stroke_order_info": null, "example_sound_url": null } } ``` **Pipeline-Schritte bei Kana-Lektion:** 1. `validate` — neuer Validator-Pfad (Vocab/Grammar verboten, kana-Budget aktiv). 2. `images` — generiert nur Thumbnail (keine Vokabel-Icons, weil keine Vokabeln). 3. `insert` — `_get_or_create_kana()` deduppt über `character` (UNIQUE-Constraint). 4. `audio` — überspringen (kein Dialog). 5. `text-audio` — laufen lassen für die Prosa-Markdown-Pages. 6. `slideshow` — überspringen. 7. Modul-Zuweisung: `category_id=30` für Hiragana, `31` für Katakana. **Quiz-Distraktoren bei Kana:** ähnliche Zeichen wählen (ね/れ/わ/ぬ verwechselbar, さ/き/ち, シ/ツ, ソ/ン, etc.) — fördert echtes Lesen, nicht Raten. **Bestandsschutz:** `Kana.character` ist UNIQUE. Wenn ein Zeichen bereits existiert (z.B. die initialen 10 Hiragana あ-こ in der DB), wird die bestehende ID wiederverwendet — kein Update auf bestehende Eintragsdaten (schützt manuelle Edits). ## 2c. Kanji-Lessons (eigenes Modul `n5-kanji-grundlagen`, seit 2026-04-27) Kanji-Lessons sind Vocabulary-Lessons (`kind: "vocabulary"`, default), die didaktisch ein **Kanji-Set** in den Mittelpunkt stellen. Sie bekommen aber eine **eigene Modul-Heimat**: `n5-kanji-grundlagen` (id=38, display_order=3, icon=漢) zwischen Katakana (2) und Zahlen-Zeit (4). **Warum eigenes Modul:** - Pädagogische Hierarchie: Hiragana → Katakana → **Kanji-Grundlagen** → Themen (Familie, Reise, Alltag, etc.). - Lerner findet alle Kanji-Karten an einer Stelle, nicht verstreut über Themen-Module. - Mayuko-Direktive (JLPT-Leitprinzip): Schreibsystem zuerst komplett, dann Themen. **Bestand 2026-04-27 — 8 Lessons im Modul:** - 164: Kanji 1 — Zahlen 一-十 (order=1) - 167: Kanji 2 — Tage und Wochentage 日月火水木金土 (order=2) - 168: Kanji 3 — Menschen 人男女子友 (order=3) - 169: Kanji 4 — Natur 山川木雨 (order=4) - 170: Kanji 5 — Position 大小上下中右左 (order=5) - 171: Kanji 6 — Familie 父母兄姉弟妹 (order=6) - 172: Kanji 7 — Grosse Zahlen, Geld & Zeit 百千万円半年時分 (order=7) - 173: Kanji 8 — Eigenschaften 新古高安長短多少早 (order=8) **Page-Struktur einer Kanji-Lesson:** Wie eine Vocabulary-Lesson, aber Vokabel-Pages enthalten **abwechselnd `kanji`- und `vocabulary`-Items**: erst die Kanji-Karte (mit On/Kun/Strichzahl/Bedeutung), dann eine oder mehrere Vokabeln, die das Kanji nutzen. Beispiel aus Lesson 171: ```json { "content_type": "kanji", "data": { "character": "父", "meaning": "Vater / father", "onyomi": "フ", "kunyomi": "ちち", "jlpt_level": 5, "stroke_count": 4, "radical": "父" } }, { "content_type": "vocabulary", "data": { "word": "父", "reading": "ちち", "romaji": "chichi", ... } } ``` **Pipeline-Schritt `_get_or_create_kanji`** (in pipeline.py seit 2026-04-27): deduppt über `character` (UNIQUE), erstellt sonst neuen Kanji-Record mit On/Kun/Strichzahl/Radikal/JLPT/status='approved'/created_by_ai=True. Bestehende Records werden **nicht** überschrieben. **Coverage-Backfill als schneller Hebel:** Bestehende Kanji-Lessons haben oft 0 Kanji-Items (nur Vocabulary mit Kanji-Word). Ein direkter SQL-INSERT in die `kanji`-Tabelle für die thematisch bereits abgedeckten Zeichen hebt die Coverage sprunghaft, ohne neue Lesson zu schreiben. Vor neuer Kanji-Lesson immer prüfen: ```sql -- Welche Kanji existieren in der DB als Karteikarten? SELECT character FROM kanji WHERE jlpt_level=5 ORDER BY character; -- Welche Kanji werden in Lessons als Vocabulary-Word genutzt, fehlen aber als Kanji-Record? ``` **Modul-Zuweisung:** Nach `insert` IMMER `category_id=38`, nicht in Themen-Module einsortieren. Order-Index = max(order_index)+1 im Modul. **Override für Kanji ausserhalb elzup-canonical:** Siehe §3 "Lesson-Level-Kanji-Override" — die canonical-Liste fehlt 兄/姉/弟/妹/新/古/安/短/多/少/早, daher `additional_n5_kanji` + Source-Note Pflicht. ## 3. Harte Constraints (Nicht-Verhandelbar) Verletzung ⇒ sofortiger Abbruch, keine Insertion: - **Quiz-Typen NUR**: `multiple_choice`, `true_false`, `matching`. **KEIN** `fill_blank` (siehe CLAUDE.md §"Quiz-System"). - **JLPT-Level NUR**: N5 oder N4. Kein N3/N2/N1 (siehe improve-jpl §"Nicht-Ziele"). - **Keine Umlaut-Fallbacks** in **allen** deutschen Texten (Einleitung, Grammar-Explanation, Quiz-Frage/Hint/Explanation/Feedback, Dialog-Übersetzung, Zusammenfassung, Lesson.description/title, LessonPage.title): **immer Ü/Ö/Ä/ß** (oder ss), **niemals UE/OE/AE/SS**. "Schüler" statt "Schueler", "höflich" statt "hoeflich", "Grüße" statt "Gruesse", "für" statt "fuer". UTF-8 durchgängig. Gilt auch in `content_text`, `hint`, `explanation`, `feedback`, `option_text` — überall, wo deutscher Fliesstext steht. - **Rōmaji NEBEN JEDEM japanischen Zeichen, das der Lerner nicht automatisch lesen kann — überall, ausnahmslos.** Jedes Auftreten von Kanji/Hiragana/Katakana in einem Text-Feld bekommt in derselben Zeile Rōmaji in Klammern `(romaji)`. Folgende Felder sind alle betroffen: - **`LessonContent.content_text`** (Einleitung, Grammatik-Erklärung, Dialog, Zusammenfassung): jedes JP-Wort/Phrase mit Rōmaji in Klammern. Beispiel: `Dort begrüßt dich das Personal mit 「いらっしゃいませ」 (irasshaimase, 'Willkommen')`. In jedem Satz neu, nicht nur beim ersten Vorkommen. - **`Grammar.title`**: wenn der Titel JP-Zeichen enthält (`〜をください (höfliche Bitte)`), muss Rōmaji dabei sein → `〜をください (~ wo kudasai — höfliche Bitte)`. - **`Grammar.structure`**: wenn die Struktur JP-Zeichen enthält (`[Nomen] + を + ください`), muss direkt danach die Rōmaji-Variante stehen. Da das `structure`-Feld nur **einzelne Zeile** ist: Rōmaji in derselben Zeile in Klammern, z.B. `[Nomen] + を + ください ([noun] + wo + kudasai)`. Zusätzlich bleibt das `Grammar.romaji`-Feld (wird separat gerendert). Beide redundant zu haben ist OK — das Template zeigt mal das eine, mal das andere, der Lerner sieht es so oder so. - **`Grammar.explanation`** (DE-Erklärung): jeder JP-Ausdruck in Klammern mit Rōmaji. `「を」 (wo, ausgesprochen 'o')` statt nur `「を」`. - **`Grammar.example_sentences`**: jeder JP-Satz JP-Zeile → Rōmaji-Zeile → DE-Zeile (dreizeilig). - **`Grammar.tts_example_jp` (PFLICHT seit 2026-04-30)**: genau EIN vollständiger JP-Satz, **rein japanisch** (kein Romaji, keine Übersetzung, keine Klammern mit lateinischer Schrift). Endet mit `。`, `!` oder `?`. Wird vom Audio-Button auf der Grammatik-Karte mit der ja-JP-Stimme vorgelesen — die `/api/tts`-Route lehnt deutschen Text mit lang=ja hart ab (HTTP 400). Beispiele: ✅ `わたしはマイク・ミラーです。` ❌ `Watashi wa Maiku Miraa desu.` ❌ `わたしは学生です。 (Watashi wa gakusei desu.)`. Pflicht-JLPT-Niveau identisch zu `example_sentences` — keine N4+-Kanji in N5-Lektionen. - **`QuizQuestion.question_text`, `.hint`, `.explanation`**: wenn JP-Zeichen darin stehen, Rōmaji in Klammern. `Was bedeutet 「水」 (mizu)?` statt `Was bedeutet 「水」?`. - **`QuizOption.option_text`, `.feedback`**: wenn JP darin steht, Rōmaji in Klammern. In Matching-Optionen `肉 (niku) | Fleisch` statt `肉 | Fleisch`. - **`Vocabulary.romaji`** (Datenbankfeld, eigene Spalte): Hepburn-Rōmaji des Wortes. - **`Vocabulary.example_sentence_english`** beginnt mit Rōmaji-Satz, Format `"Romaji — Deutsche Übersetzung"` (Em-Dash). Die Übersetzung steht auf der Karten-Rückseite unter dem japanischen Beispielsatz und ist explizit auf Deutsch (Plattform-Sprache). Englisch ist nur als Übergangs-Fallback erlaubt, wenn alte Daten noch nicht migriert sind. - **`Vocabulary.example_sentence_japanese`** bleibt rein JP — dort ist Rōmaji redundant, weil `reading` + `romaji` in derselben Vokabel-Karte stehen. - Ziel: Ein deutschsprachiger Anfänger (inkl. Claudio) kann **jeden Satz** überall in der Lektion westlich aussprechen, selbst wenn er ein Kanji nicht kennt. - **Instruction-Language**: default `'german'` (deutschsprachige Anfänger sind die primäre Zielgruppe). Englisch nur auf explizite User-Anweisung. - **Beispielsätze dürfen NUR Kanji/Vokabeln des eigenen oder eines niedrigeren JLPT-Levels enthalten.** N5-Lektion darf keine N3-Kanji im Beispielsatz haben. Wenn unvermeidbar: schreibe den Satz in Hiragana. - **Bekannte N5-Vokabel-Falle:** Viele „klassische" N5-Familien-Vokabeln (家族, 兄弟, 両親, 子供, 兄, 姉, 弟, 妹, お父さん, お母さん, お兄さん, お姉さん) enthalten Kanji, die NICHT im N5-Kanji-Set (80 Zeichen) stehen — 兄, 姉, 弟, 妹, 家, 族, 親, 供 sind alle erst N4. Validator wirft ERROR. Lösung: in `content_text`, `Grammar.example_sentences` und `LessonContent.text` nur die Hiragana-Variante schreiben (かぞく, きょうだい, りょうしん, あに, あね, おとうと, いもうと, おとうさん, おかあさん, …). Im `Vocabulary.word`-Feld bleibt die Kanji-Form (das ist die Karteikarte selbst). Im N5-Kanji-Set sind aus Familie nur: 人, 子, 女, 男, 父, 母, 友. Auch andere N5-Vokabeln können solche „N4-Kanji-N5-Vokabeln" sein — bei Validator-Hinweis stets Hiragana wählen. - **`created_by_ai = True`** für alle generierten Kana/Kanji/Vocabulary/Grammar-Einträge. `LessonContent.generated_by_ai = True` ebenfalls. - **`status = 'approved'`** direkt (User-Entscheidung 2026-04-20). - **Duplicate-Check**: Vor jedem INSERT in Kana/Kanji/Vocabulary/Grammar prüfen ob `character`/`word`/`title` schon existiert → bestehende ID wiederverwenden, NICHT neue Zeile erzeugen. - **Markdown JA — KEIN roher HTML in `content_text`** (content_type `text`). Seit 2026-04-25 rendert das Template [lesson_view.html:683/916](../../app/templates/lesson_view.html#L683) mit dem `| markdown_safe`-Filter ([app/__init__.py](../../app/__init__.py)) — Markdown wird zu Bleach-gesäubertem HTML. **Pflicht: visuelle Hierarchie pro Text-Page**, sonst sehen alle Sektionen gleich aus. - **Erlaubte Markdown-Bausteine:** `## Headline H2` (Sektions-Titel), `### Headline H3` (Unter-Sektion), `**fett**` (Schlüsselwörter und JP-Begriffe wie `**「ちち」 (chichi)**`), `*kursiv*` (Romaji-Hinweise), `- Liste` und `1. Liste`, `> Blockquote` (Merksatz/Beispiel), `` `code` `` (Strukturen wie `` `[Nomen] + です` ``), `---` (Trennlinie zwischen 2 Themenblöcken), `[Linktext](https://…)`. - **Pflicht-Mindestformatierung pro `content_text`-Block:** mind. **1× H2 oder H3** (Sektionstitel) + mind. **2× `**bold**`** für Schlüsselbegriffe + mind. **1× Liste oder Blockquote**, wenn Absatz Aufzählung/Merksatz enthält. Reine Prosa ohne Hervorhebung ist verboten — sieht "alles gleich" aus (User-Feedback 2026-04-25). - **NICHT erlaubt:** roher HTML (`

`, `

`, `