
Bildungsanwendung
Ein Anatomieatlas für Studierende der Medizin und der Gesundheitswissenschaften. Es gibt keine Datenbank: Die Inhalte liegen im Repository, werden zur Build-Zeit gegen ein Schema geprüft, und alle Seiten werden statisch erzeugt. Suche, Quiz und Offline-Nutzung laufen im Browser.
Anatomieinhalte bestehen aus zwei verschiedenen Materialien: auf der einen Seite rein strukturelle Daten — Ursprung, Ansatz, Nerv und Funktion eines Muskels — auf der anderen freie Erzählung, klinische Notizen, Differenzialdiagnose. Das Erste in einer Fließtextdatei zu halten, macht die Daten unsuchbar und inkonsistent; das Zweite in eine Datenbanktabelle zu pressen, macht das Schreiben unmöglich. Der eigentliche Punkt ist jedoch die Korrektheit: In Bildungsinhalten ist ein leer gelassenes Feld oder eine Referenz auf eine nicht existierende Struktur ein stiller Fehler für die lernende Person. Die Seite öffnet sich, die Tabelle erscheint, nur eine Zeile fehlt. Solche Fehler mussten vor der Veröffentlichung auffallen.
Ein Nutzertyp, aber zwei unterschiedliche Nutzungsmomente. Beim Lernen: lange Sitzungen, ständiges Springen von einer Struktur zur nächsten — vom Muskel zum Nerv, vom Nerv zur Region — und der Wunsch, die Suche über die Tastatur zu öffnen. Beim Wiederholen: vor der Prüfung, in der Pause, am Telefon, oft mit schlechter Verbindung. Der zweite Modus macht Offline-Betrieb und schnellen Start zwingend.
Einzelner Entwickler. Inhaltsschema und Prüfpipeline, Datenmodell, Struktur der statischen Erzeugung, Suche, Quiz-Ablauf, Offline-Schicht und Tests.
Im Zentrum steht eine Entscheidung: keine Datenbank. Strukturdaten liegen als JSON im Repository, erzählende Inhalte als MDX; auf der Detailseite werden beide über einen Loader zusammengeführt. Beziehungen entstehen über Slugs statt über Identifikatoren — der Nerv eines Muskels heißt nicht "nerv-42", sondern trägt den lesbaren Namen dieses Nervs, was Adressen und Inhaltsdateien für Menschen bearbeitbar hält. Alle Detailseiten entstehen über `generateStaticParams` zur Build-Zeit; zur Laufzeit gibt es keinen Lesepfad zu einem Server. Der Standard ist Server Component; `"use client"` steht nur dort, wo es echte Interaktion gibt. Im Browser laufen drei Dinge: unscharfe Suche mit Fuse.js — der Index wird auf dem Client aufgebaut, das ⌘K-Modal am Desktop und die Vollbildebene am Telefon teilen denselben Körper — der in Zustand gehaltene persönliche Zustand und der Service-Worker-Cache. In Zustand liegen keine Inhalte, nur was der Person gehört: Lesezeichen, Fortschritt, Quiz-Zustand.
Diese Inhalte ändern sich nicht hundertmal am Tag, sondern einige Male pro Semester und nur nach Durchsicht. Für solche Inhalte bringt eine Datenbank Laufzeitabfragen, Verbindungsverwaltung und eine eigene Verwaltungsoberfläche mit sich — und die Aktualität, die sie kauft, wird nicht gebraucht. Stattdessen liegen die Inhalte unter Versionskontrolle: Jede Änderung ist ein prüfbarer Diff und lässt sich zurücknehmen.
KompromissEine Inhaltsänderung erfordert Zugriff auf das Repository und eine neue Bereitstellung; eine Fachperson kann keine Korrektur allein vornehmen. Selbst ein Tippfehler wartet auf einen Build.
Das Prüfskript liest alle Datendateien gegen Zod-Schemata und endet bei einem einzigen ungültigen Datensatz mit einem Fehlercode. Die TypeScript-Typen stammen aus denselben Schemata, das Schema ist also die einzige Quelle: Typ und Laufzeitprüfung können nicht auseinanderlaufen. Bei Bildungsinhalten ist dieses Tor der einzige Mechanismus, der "die Seite öffnet sich, aber die Information fehlt" vor der Veröffentlichung abfängt.
KompromissWeil das Schema streng ist, kann ein unfertiges Stück Inhalt nicht ins Repository; wer an einem Entwurf arbeiten will, muss entweder dem Schema ein leeres Feld erlauben oder den Inhalt außerhalb des Repositories halten. Das Schema zu lockern senkt jedoch unmittelbar den Wert des Tores.
Der durchsuchte Datenbestand ist klein und fest; einen zur Build-Zeit ohnehin vorhandenen Index an den Client zu schicken, ist schneller und einfacher als eine Anfrage pro Tastendruck. Unscharfe Übereinstimmung war nötig, damit ein lateinischer Begriff und seine türkische Entsprechung mit derselben Anfrage gefunden werden. Desktop-Modal und mobile Ebene teilen sich Körper und Index.
KompromissDer Index wird auf den Client geladen; mit wachsendem Inhalt wächst dieser Aufwand, und irgendwann wird ein Wechsel zur serverseitigen Suche nötig. Außerdem werden keine Daten darüber erhoben, wonach gesucht wird — gut für den Datenschutz, ein blinder Fleck für die Priorisierung von Inhalten.
Lesezeichen, Fortschritt und Quiz-Zustand gehören der Person, und es gibt keinen Grund, sie auf einem Server zu speichern — es gibt kein Konto und keine Synchronisation. Inhalte in den Client-Store zu kopieren, ist dagegen verboten: Inhalt ist die eine Quelle vom Server, und eine zweite Kopie läuft mit der Zeit auseinander.
KompromissWechselt jemand das Gerät, wandert der Fortschritt nicht mit; werden die Browserdaten gelöscht, geht er verloren. Das ist der Preis dafür, ohne Konto zu arbeiten, und er wurde akzeptiert.
Next.js App Router wird hier nicht als Server-Framework, sondern als statischer Seitengenerator genutzt: Hunderte Detailseiten entstehen über `generateStaticParams` zur Build-Zeit, und weil Server Component der Standard ist, bleibt das an den Client gesendete JavaScript minimal. Zod erfüllt zwei Aufgaben — Prüftor und Typquelle; dass beides aus einer Quelle kommt, garantiert, dass eine Schemaänderung auch den Typ ändert. MDX erlaubt es, klinische Erzählung als Fließtext zu schreiben, und lässt das Einbetten von Komponenten offen. Fuse.js ist klein genug für den Client. Serwist baut die Offline-Schicht auf, ohne einen Service Worker von Hand zu schreiben. Auf der visuellen Seite liefert interaktives SVG Zoom und Verschiebung, ohne in die Unschärfe eines Rasterbildes zu fallen.
Die Anwendung hat keinen Schreibpfad: kein Formular, keine Sitzung, kein Konto, keine Nutzerdatenbank. Entsprechend werden auch keine personenbezogenen Daten gespeichert — Fortschritt und Lesezeichen verlassen das Gerät nicht. Die bei der Inhaltserstellung genutzten Modellaufrufe stehen nur in Skripten; zur Laufzeit geht keine Anfrage an einen externen Dienst. Das reduziert die Angriffsfläche auf die Korrektheit der Inhalte — und die schützt das Build-Tor.
Für dieses Projekt liegt keine gespeicherte, mit Datum versehene Lighthouse-Ausgabe vor, deshalb steht hier kein Seitengeschwindigkeits-Score. Strukturell gilt: Auf dem Lesepfad gibt es keine Serverabfrage, die Seiten werden als Build-Ergebnis ausgeliefert. Da Suchindex und Quiz-Zustand auf dem Client liegen, hängt die Interaktionslatenz nicht von einer Netzwerkrunde ab. Der Service Worker hält zuletzt gesehene Seiten im Cache; bricht die Verbindung ab, zeigt die Anwendung die eigene Offline-Seite statt eines leeren Bildschirms.
Das Prüftor habe ich von Anfang an eingebaut, und es war die Entscheidung, die sich in diesem Projekt am meisten ausgezahlt hat; dasselbe Muster habe ich später in Rahmina als Inhaltsfreigabe-Tor wiederholt — dort lautete die Regel nicht "passt es zum Schema", sondern "wurde es geprüft". Genau das fehlt hier: Das Schema prüft, dass ein Feld gefüllt ist, nicht, dass der Inhalt richtig ist. Heute würde ich jedem Inhaltsblock einen Prüfstatus geben und das Release-Tor auf "der Build scheitert, wenn ungeprüfter Inhalt existiert" umstellen. Der zweite Punkt ist die Suche: Den Index an den Client zu geben, ist beim heutigen Umfang richtig, aber nichts misst, wann diese Entscheidung kippt. Heute würde ich die Indexgröße mit einem Schwellwert begrenzen und den Build bei Überschreitung warnen lassen — wann eine Entscheidung ungültig wird, gehört genauso in den Code wie die Entscheidung selbst.
Sagen Sie mir, was Sie bauen wollen; ich sage Ihnen vorab, wie lange es dauert und wo man anfängt.