Nach dem Kanban-Board als erstem Praxisbeispiel für den AHA-Stack folgt jetzt ein komplett anderes Projekt: Rechenkiste – eine Mathe-Übungsapp für Grundschulkinder.
Live: rechenkiste.casoon.dev
Warum dieses Beispiel wichtig ist
Rechenkiste ist mehr als eine Lern-App. Es ist ein Proof of Concept für eine Entwicklung, die das Frontend-Ökosystem grundlegend verändert: Server-Side Rendering von Komponenten als HTML-Fragmente – konkret umgesetzt mit Astro und seiner neuen Container API.
Die Frage, die dahinter steht: Brauchen wir das JavaScript-Framework noch im Browser – oder reicht es auf dem Server?
React, Vue und Co. können längst serverseitig rendern. Next.js, Nuxt, Remix – alle setzen auf SSR. Aber sie hydratisieren: Der Server rendert HTML, dann lädt der Browser das Framework und macht die Seite “interaktiv”. Das Framework läuft auf beiden Seiten.
Der Unterschied hier: Kein Framework im Browser. Der Server rendert HTML-Fragmente, HTMX tauscht sie aus, fertig. Keine Hydration, kein Virtual DOM, kein State-Management im Client.
Was das konkret bedeutet:
- Kein JavaScript-Bundle für UI-Logik – nur HTMX (14 KB) und Alpine.js für lokale Interaktion
- Keine Hydration – was der Server rendert, ist sofort nutzbar
- Kein doppelter State – die Wahrheit liegt auf dem Server, nicht im Browser
- Keine manipulierbare Logik – Validierung passiert serverseitig, der Client kann nichts umgehen
React Server Components gehen in eine ähnliche Richtung, aber mit mehr Komplexität. Astros Container API ist simpler: Komponente rein, HTML raus.
Rechenkiste zeigt, dass dieser Ansatz auch für Anwendungen mit komplexer Interaktion funktioniert – nicht nur für statische Content-Seiten.
Was Rechenkiste kann
Die App richtet sich an Kinder der Klassen 1–4 und bietet:
- Arithmetik – Addition, Subtraktion, Multiplikation, Division
- Zahlenverständnis – Vergleichen, Ordnen, Zahlenstrahl
- Geometrie – Formen, Symmetrie, Koordinaten
- Brüche – Visuelle Darstellungen
- Uhrzeit – Analoguhren lesen
- Textaufgaben – Sachaufgaben aus dem Alltag
- Maßeinheiten – Länge, Gewicht, Volumen
Aufgaben werden dynamisch generiert, die Schwierigkeit passt sich an, falsch beantwortete Fragen werden wiederholt. Alles ohne Page-Reload – dank HTMX.
Der Tech-Stack
| Technologie | Aufgabe |
|---|---|
| Astro | Server-Side Rendering, Zero-JS Default |
| HTMX | Dynamische Interaktionen via HTML-Attribute |
| Alpine.js | Reaktive UI-Komponenten (Timer, Drag-Drop) |
| Tailwind CSS | Styling |
| @casoon/fragment-renderer | Server-seitige Astro-Komponenten für HTMX |
| Cloudflare Workers | Hosting |
Das Besondere: Die App läuft komplett auf Cloudflare Workers – inklusive serverseitigem Rendering von Astro-Komponenten.
Warum @casoon/fragment-renderer?
Für dieses Projekt ist ein neues npm-Package entstanden: @casoon/fragment-renderer.
Das Problem
HTMX funktioniert so: Der Client schickt eine Anfrage, der Server antwortet mit einem HTML-Fragment, HTMX tauscht einen Teil der Seite aus. Kein JSON, kein Client-seitiges Rendering – der Server liefert fertiges HTML.
Mit Astro-Komponenten ist das nicht trivial. Astro rendert normalerweise ganze Seiten. Die Container API existiert zwar, ist aber experimentell und auf Cloudflare Workers nicht ohne Weiteres nutzbar. Für jede Aufgabe, jedes Feedback, jede Ergebnisanzeige eine eigene API-Route schreiben? Das skaliert nicht.
Die Lösung
@casoon/fragment-renderer abstrahiert das Rendering von Astro-Komponenten zu HTML-Fragmenten:
import { createAstroRuntime } from "@casoon/fragment-renderer";
import { ahaStackPreset } from "@casoon/fragment-renderer/presets/aha-stack";
const runtime = createAstroRuntime({
...ahaStackPreset({ locale: "de", htmxHeaders: true }),
components: [
{ id: "feedback-correct", loader: () => import("./FeedbackCorrect.astro") },
{ id: "feedback-incorrect", loader: () => import("./FeedbackIncorrect.astro") },
{ id: "task-arithmetic", loader: () => import("./TaskArithmetic.astro") },
],
});
// Astro-Komponente als HTML-Fragment rendern
const html = await runtime.renderToString({
componentId: "feedback-correct",
props: { locale: "de", nextUrl: "/next-task" },
});
Was das bringt:
- Component Registry – Komponenten per ID registrieren, dynamisch laden
- HTMX-Headers –
HX-Trigger,HX-Retargetautomatisch setzen - Presets – Vorkonfigurierte Setups für AHA-Stack, E-Mail, CMS
- Adapter – Node.js, Cloudflare Workers, Vercel Edge
Der Ablauf in Rechenkiste
- Kind klickt “Nächste Aufgabe”
- HTMX sendet Request an
/api/test/next - Server generiert Aufgabe, wählt passende Komponente (TaskArithmetic, TaskGeometry, …)
fragment-rendererrendert die Komponente zu HTML- HTMX tauscht den Aufgaben-Bereich aus
Kein Page-Reload. Kein JavaScript-Framework. Nur HTML, das hin und her geschickt wird.
Der Fragment-Renderer im Einsatz
So sieht der Task-Renderer in Rechenkiste aus:
// src/server/services/task-renderer.ts
import { createAstroRuntime } from "@casoon/fragment-renderer";
import { ahaStackPreset } from "@casoon/fragment-renderer/presets/aha-stack";
function createRuntime(locale: Locale) {
return createAstroRuntime({
...ahaStackPreset({ locale, htmxHeaders: true }),
components: [
{ id: "task-arithmetic", loader: () => Promise.resolve({ default: TaskArithmetic }) },
{ id: "task-geometry", loader: () => Promise.resolve({ default: TaskGeometry }) },
{ id: "feedback-correct", loader: () => Promise.resolve({ default: FeedbackCorrect }) },
{ id: "feedback-incorrect", loader: () => Promise.resolve({ default: FeedbackIncorrect }) },
],
});
}
export async function renderTaskFragment(task: TaskInstance, locale: Locale): Promise<string> {
const runtime = createRuntime(locale);
// Komponente basierend auf Aufgabentyp wählen
const componentId = task.inputType === "multiple-choice"
? "task-multiple-choice"
: task.category === "geometry"
? "task-geometry"
: "task-arithmetic";
return runtime.renderToString({
componentId,
props: { task, locale },
});
}
Die Komponenten selbst sind normale Astro-Komponenten – mit Tailwind-Styling, SVG-Grafiken für Uhren und Brüche, Alpine.js für Drag-Drop.
Das Aufgabensystem
Aufgaben implementieren ein einheitliches Interface:
interface TaskInstance {
id: string;
question: string;
category: string;
inputType: "text" | "multiple-choice" | "drag-drop";
validate(answer: string): ValidationResult;
getHint(): string;
getCorrectAnswer(): string | number;
}
Jede Kategorie (Arithmetik, Geometrie, Brüche, …) liefert:
- Aufgabengenerierung mit konfigurierbarer Schwierigkeit
- Selbst-Validierung mit hilfreichen Hinweisen
- SVG-Visualisierungen wo sinnvoll (Uhren, Brüche, Formen)
Die Generierung ist klassenbasiert – eine Erstklässlerin bekommt 3 + 4, ein Viertklässler 347 × 28.
Server-State statt Client-State
Ein wesentlicher Unterschied zu klassischen Single-Page-Applications: Die gesamte Geschäftslogik läuft auf dem Server. Der Client bekommt nur fertiges HTML – keine Aufgabendaten, keine Validierungslogik.
Was auf dem Server passiert
1. Session-Erstellung (/api/test/start)
Wenn ein Kind eine Übung startet, passiert auf dem Server:
// Session mit allen Aufgaben generieren
const session = createSession(grade, taskCount, locale, options);
// Speichern in Astro Session (Cloudflare KV – global verteilter Key-Value-Store)
await saveSession(context, session);
Die Session enthält:
- Alle generierten Aufgaben (vorberechnet!)
- Die korrekten Antworten
- Den aktuellen Fortschritt
- Falsch beantwortete Aufgaben für Wiederholung
- Schwierigkeitsanpassung
2. Aufgaben laden (/api/test/task)
Jede Aufgabenanfrage:
- Lädt die Session aus KV
- Holt die aktuelle Aufgabe
- Rendert sie zu HTML
- Schickt nur die Frage, nicht die Lösung
const session = await loadSession(context);
const currentTask = getCurrentTask(session);
const taskHtml = await renderTaskFragment(currentTask, session.id, locale);
3. Antwort prüfen (/api/test/answer)
Die Validierung passiert komplett serverseitig:
const result = submitAnswer(session, answer); // Validiert gegen gespeicherte Lösung
nextTask(session); // Wechselt zur nächsten Aufgabe
await saveSession(context, session); // Aktualisiert Session
return renderFeedbackResponse(result, ...); // Rendert Feedback-HTML
Was NICHT zum Client geht
- Korrekte Antworten bleiben auf dem Server – erst nach Absenden sichtbar
- Validierung läuft serverseitig – der Browser prüft nichts
- Aufgabenliste ist dem Client unbekannt – nur die aktuelle Aufgabe wird gerendert
- Session-State liegt in KV – nichts im LocalStorage oder Memory
Das ist ein Sicherheitsvorteil: Ein Kind kann nicht in den DevTools die Antwort nachschauen oder den Fortschritt manipulieren.
Session-Datenstruktur
interface TestSession {
id: string;
grade: Grade; // Schwierigkeitsstufe 1-5
totalTasks: number;
currentIndex: number;
tasks: SerializedTask[]; // Alle Aufgaben (inkl. Lösungen!)
results: TaskResultRecord[]; // Bisherige Antworten
locale: Locale;
options: SessionOptions;
// Adaptive Schwierigkeit
currentDifficulty: Grade;
consecutiveCorrect: number;
consecutiveIncorrect: number;
// Wiederholung falscher Aufgaben
incorrectTaskIds: string[];
retryMode: boolean;
// Performance-Messung
fragmentLoads: number;
pageLoads: number;
}
Die Session wird bei jeder Interaktion aus Cloudflare KV geladen, aktualisiert und zurückgeschrieben. Das klingt langsam, ist es aber nicht: KV ist für genau diese Zugriffsmuster optimiert.
Adaptive Schwierigkeit
Die Schwierigkeitsanpassung zeigt, warum Server-State sinnvoll ist:
function adjustDifficulty(session: TestSession): void {
// 3x richtig hintereinander → schwerer
if (session.consecutiveCorrect >= 3 && session.currentDifficulty < 5) {
session.currentDifficulty++;
// Kommende Aufgaben neu generieren
const newTasks = taskGenerator.generateMany(session.currentDifficulty, ...);
// In Session einfügen
}
// 3x falsch → leichter
if (session.consecutiveIncorrect >= 3 && session.currentDifficulty > 1) {
session.currentDifficulty--;
// ...
}
}
Das wäre mit Client-State komplizierter: Der Server müsste trotzdem wissen, welche Aufgaben generiert wurden, um Antworten zu validieren. Mit Server-State ist alles an einem Ort.
Vergleich: SPA vs. AHA-Stack
| Aspekt | Klassische SPA | Rechenkiste (AHA-Stack) |
|---|---|---|
| Aufgabengenerierung | Client (oder API-Call pro Aufgabe) | Server (alle bei Session-Start) |
| Lösungen | Im Client-State oder per API | Nur auf Server in KV |
| Validierung | Client-seitig (manipulierbar) | Server-seitig (sicher) |
| Session | LocalStorage/Memory | Cloudflare KV |
| JS-Bundle | Framework + Logik | Nur HTMX + Alpine.js |
Der Trade-off: Mehr Server-Roundtrips, dafür weniger Client-Komplexität und bessere Kontrolle über den State.
Übertragbar: WaWi, ERP, Shop-Systeme
Rechenkiste ist eine Lern-App – aber das Architekturmuster passt auf viele Business-Anwendungen. Überall dort, wo sensible Logik nicht im Browser laufen sollte.
Warenwirtschaft (WaWi)
Eine klassische WaWi-Oberfläche: Artikellisten, Bestandsübersichten, Bestellformulare. Mit dem AHA-Stack:
- Artikelsuche: HTMX-Request → Server filtert, rendert Tabelle → Fragment ersetzt Liste
- Bestandsbuchung: Formular absenden → Server validiert, bucht, rendert Bestätigung
- Preisberechnung: Rabatte, Staffelpreise, Kundenkonditionen – alles serverseitig, nichts im Browser manipulierbar
// Beispiel: Bestandsbuchung
export async function renderBookingResult(
articleId: string,
quantity: number,
userId: string
): Promise<string> {
// Validierung auf dem Server
const article = await getArticle(articleId);
const user = await getUser(userId);
if (!user.canBook(article)) {
return runtime.renderToString({
componentId: "error-no-permission",
props: { message: "Keine Berechtigung für diese Buchung" }
});
}
// Buchung durchführen
const result = await bookStock(article, quantity, user);
return runtime.renderToString({
componentId: "booking-success",
props: { article, quantity, newStock: result.newStock }
});
}
Vorteil: Die Berechtigungslogik ist nicht im JavaScript-Bundle sichtbar. Ein Mitarbeiter kann nicht durch Manipulation im Browser Buchungen durchführen, für die er keine Rechte hat.
ERP-Systeme
ERP-Anwendungen haben oft komplexe Workflows: Genehmigungsprozesse, Statusübergänge, Berechnungen. Genau das, was nicht im Client liegen sollte.
- Genehmigungsworkflows: Status-Buttons rendern basierend auf Benutzerrolle und aktuellem Status – der Client sieht nur, was er darf
- Kalkulationen: Stücklisten, Arbeitspläne, Kostenrechnung – der Server rechnet, der Client zeigt an
- Audit-Trail: Jede Aktion geht durch den Server, lückenlose Protokollierung
// Workflow-Status rendern
export async function renderWorkflowActions(
documentId: string,
userId: string
): Promise<string> {
const document = await getDocument(documentId);
const user = await getUser(userId);
const allowedTransitions = workflow.getAllowedTransitions(document.status, user.role);
return runtime.renderToString({
componentId: "workflow-actions",
props: {
document,
actions: allowedTransitions // Nur erlaubte Aktionen werden gerendert
}
});
}
Shop-Systeme
E-Commerce ist ein Paradebeispiel für “Logik gehört auf den Server”:
- Preisberechnung: Kundengruppen-Rabatte, Staffelpreise, Gutschein-Validierung – alles serverseitig
- Verfügbarkeitsprüfung: Lagerbestand in Echtzeit, keine veralteten Client-Daten
- Warenkorb: Session-basiert auf dem Server, nicht im LocalStorage manipulierbar
| Funktion | Client-seitig (SPA) | Server-seitig (AHA-Stack) |
|---|---|---|
| Preisanzeige | API-Call, Client rendert | Server rendert fertige Preise |
| Rabattberechnung | Logik im Bundle sichtbar | Logik bleibt auf Server |
| Gutschein-Validierung | Client prüft Format, Server prüft Gültigkeit | Server prüft alles |
| Warenkorb-Summe | Client rechnet | Server rechnet, Client zeigt an |
| Checkout | Multi-Step mit Client-State | Multi-Step mit Server-Session |
Warum das funktioniert
Der AHA-Stack ist kein Rückschritt zu “alten” Server-rendered Apps. Der Unterschied zu klassischem PHP/Rails:
- Komponenten-Architektur: Wiederverwendbare Astro-Komponenten, nicht Template-Spaghetti
- Partial Updates: HTMX tauscht nur Teile aus, kein Full-Page-Reload
- Edge-Deployment: Cloudflare Workers = Server-Rendering mit CDN-Latenz
- TypeScript End-to-End: Typsicherheit von der Datenbank bis zum HTML
Für interne Business-Anwendungen – WaWi, ERP, Backoffice – ist das oft die bessere Wahl als eine React-SPA. Weniger Komplexität, bessere Security, einfacheres Deployment.
Der Counter: F und P
Im Footer der App steht ein kleiner Counter: F:12 P:2
- F = Fragment-Loads – Wie oft HTMX ein HTML-Fragment nachgeladen hat
- P = Page-Loads – Wie oft der Browser eine komplette Seite geladen hat
Das Verhältnis zeigt den Vorteil des AHA-Stacks: Nach dem initialen Laden (P) passiert fast alles über Fragmente (F). Jede Aufgabe, jedes Feedback, jede Ergebnisanzeige ist ein Fragment – kein voller Page-Load nötig.
Bei einer typischen Session mit 10 Aufgaben: F:25 P:2. Der Browser hat die Seite zweimal geladen (Start, Ergebnis), aber 25 Fragmente ausgetauscht. Das ist schneller, verbraucht weniger Bandbreite und fühlt sich flüssiger an.
Der Counter ist selbst ein Fragment – er wird per HTMX aktualisiert, wenn sich etwas ändert:
<span
id="load-counter"
hx-get="/api/test/counter"
hx-trigger="load, taskLoaded from:body, feedbackShown from:body"
hx-swap="innerHTML">
</span>
Projektstruktur
src/
├── components/
│ └── fragments/ # HTMX-Fragmente
│ ├── TaskArithmetic.astro
│ ├── TaskGeometry.astro
│ ├── TaskMultipleChoice.astro
│ ├── TaskDragDrop.astro
│ ├── FeedbackCorrect.astro
│ ├── FeedbackIncorrect.astro
│ └── ResultDisplay.astro
├── i18n/
│ └── translations.ts # DE, EN, UK
├── pages/
│ ├── index.astro # Startseite
│ ├── auswahl.astro # Auswahl Klasse/Kategorie
│ ├── test.astro # Übungssession
│ ├── ergebnis.astro # Ergebnisse
│ └── technik.astro # Technik-Dokumentation
└── server/
├── domain/
│ ├── session.ts # Session-Management
│ └── task-system/ # Aufgabenlogik
│ ├── tasks/ # Aufgaben-Implementierungen
│ ├── generator.ts
│ └── registry.ts
└── services/
├── fragment-service.ts
└── task-renderer.ts
Mehrsprachigkeit
Die App unterstützt drei Sprachen:
- Deutsch –
/ - Englisch –
/en/ - Ukrainisch –
/uk/
Ukrainisch ist dabei, weil es für den praktischen Einsatz interessant wäre: Viele ukrainische Kinder in deutschen Schulen könnten Mathematik üben, auch wenn die deutsche Sprache noch eine Hürde ist. Rechenkiste ist dafür nicht gedacht – aber die Idee zeigt, wohin so etwas gehen könnte.
Deployment
Die App läuft auf Cloudflare Workers:
pnpm build
pnpm deploy
Das war’s. Astro mit Cloudflare-Adapter, wrangler.toml für die Konfiguration, fertig.
Proof of Concept, kein Ersatz
Rechenkiste ist ein Proof of Concept – eine Demonstration, was mit dem AHA-Stack und @casoon/fragment-renderer möglich ist. Die App funktioniert, kann gern genutzt werden, ist aber nicht als vollwertige Lernplattform gedacht.
Für ernsthaftes Üben in der Schule empfehle ich Anton. Das kommt bei meinen Kindern erfolgreich zum Einsatz: durchdachte Didaktik, Fortschrittsverfolgung, Gamification, die funktioniert. Das kann Rechenkiste nicht – und will es auch nicht.
Der Wert von Rechenkiste liegt im Code: Er zeigt, wie man eine interaktive Anwendung baut, die schnell lädt, ohne JavaScript-Framework auskommt und trotzdem modern ist.
Alternativen: Nicht nur Astro kann das
Der Ansatz “Server rendert HTML-Fragmente, Browser tauscht aus” ist nicht neu. Andere Ökosysteme haben das früher etabliert:
| Technologie | Sprache | Ansatz |
|---|---|---|
| Laravel Livewire | PHP | Komponenten → HTML-Fragmente, automatisches AJAX |
| Rails Hotwire/Turbo | Ruby | Views → HTML-Fragmente, Turbo Frames |
| Phoenix LiveView | Elixir | Templates → HTML über WebSocket |
| Django + HTMX | Python | Templates → HTML-Fragmente |
| Go + Templ + HTMX | Go | Templates → HTML-Fragmente |
Die ernsthaftesten Konkurrenten:
- Laravel Livewire – Das PHP-Pendant. Riesige Community, exzellente DX, Battle-tested. Wenn PHP im Stack ist, die ausgereifteste Lösung.
- Rails Hotwire – Turbo + Stimulus. Basecamp und Hey laufen damit. Sehr durchdacht, aber Ruby-Ökosystem.
- Phoenix LiveView – Technisch am beeindruckendsten. WebSocket statt HTTP, Server hält UI-State im Prozess. Extrem schnell, aber Elixir ist Nische.
Was Astro + fragment-renderer unterscheidet:
- JavaScript/TypeScript – Größerer Talentpool als Elixir, Ruby oder PHP
- Edge-Runtime – Cloudflare Workers, Vercel Edge, Deno Deploy – das können die anderen nicht
- Kein Vendor-Lock-in – Standard-Astro-Komponenten, HTMX ist framework-agnostisch
Der AHA-Stack ist quasi “Livewire/Hotwire für das JavaScript-Ökosystem”. Die Idee ist nicht neu – die Implementierung für TypeScript + Edge schon.