2026-09-18 01:27:07 +02:00
2026-09-18 01:12:39 +02:00
2026-09-18 01:27:07 +02:00

Anouma

Die Website von Anouma - Next.js (App Router) mit einem eingebauten Payload CMS (PostgreSQL) für Termine, Beiträge, Angebote und Seiteninhalte, plus Online-Termine mit selbst gehostetem LiveKit SFU-Video-Call, Anmeldungen und E-Mail-Erinnerungen.

Projektstruktur

  • app/(frontend)/ - die öffentliche Website (bestehendes ANOUMA-Design, eigener Root-Layout)
  • app/(call)/ - die Video-Call-Oberfläche (/termine/[slug]/call), eigenes minimalistisches dunkles Layout ohne Navbar/Footer
  • app/(payload)/ - der Admin-Bereich unter /admin, Payloads REST/GraphQL-API sowie die Meeting-/Cron-API-Routen (api/meetings/..., api/cron/...)
  • app/global-not-found.tsx - statische 404-Seite (siehe unten, warum sie nötig ist)
  • collections/, globals/, access/, fields/, payload.config.ts - die CMS-Konfiguration
  • lib/payload/ - Local-API-Zugriffe für die öffentliche Website (mit React.cache pro Request memoisiert)
  • lib/meeting/ - Meeting-Passwort, Zeitfenster/Status, signierte Beitritts-Tokens, LiveKit-SFU-Token und Reminder-Logik
  • lib/email/ - SMTP-Versand (nodemailer) und das E-Mail-Template für Reminder
  • lib/texte.ts - die ursprünglichen Anouma-Texte, nur noch als Seed-Quelle verwendet
  • scripts/seed.ts - überträgt die vorhandenen Inhalte ins CMS
  • server.ts - eigener Node-Server (statt next start), passend zur bestehenden Payload-Konfiguration

Zwei bzw. drei Root-Layouts ((frontend), (payload), (call)) bedeuten: Next.js kann daraus keine einzelne 404-Seite komponieren, deshalb gibt es app/global-not-found.tsx (siehe experimental.globalNotFound in next.config.ts).

Online-Termine & Video-Call

Kontaktanfragen und CMS-Nachrichten

Kontakt- und Terminanfragen werden unter /admin/anfragen als Konversationen gespeichert. Antworten aus dem CMS werden per SMTP verschickt. Setze dafür INBOUND_REPLY_TO auf die Mailbox, auf die Besucher antworten sollen. Ein Inbound-Mail-Provider muss eingehende Antworten anschließend als JSON an POST /api/email/inbound weiterleiten und dabei x-inbound-secret mit dem Wert aus INBOUND_EMAIL_SECRET senden. Erwartet werden mindestens from, subject, text und eine eindeutige messageId; der Thread-Schlüssel im Betreff ([ANOUMA-…]) ordnet die Antwort der richtigen Anfrage zu.

  • Ein Termin wird per Häkchen „Online-Termin“ zu einem Video-Call-Termin. Payload generiert dabei automatisch ein zufälliges Meeting-Passwort (nie aus der Meeting-ID abgeleitet); die Admin kann jederzeit ein neues erzeugen lassen.
  • Öffentliche Beitrittsseite: /termine/[slug]/beitreten (Name + Passwort, beide Pflicht) → bei Erfolg /termine/[slug]/call.
  • Ist die aufrufende Person im selben Browser als Admin eingeloggt, wird sie automatisch als Host erkannt (kein Passwort nötig, größeres Beitritts-Zeitfenster, exklusive Steuerung: Bildschirmfreigabe, Teilnehmer entfernen).
  • Die Zeitfenster (wie früh Host/Teilnehmer beitreten dürfen, wie lange das Meeting nach Ende offen bleibt) sind zentral im CMS unter Video-Termin-Einstellungen konfigurierbar.
  • Server-seitige Validierung: /api/meetings/[slug]/join prüft Termin, Zeitfenster und Passwort und stellt danach erst ein kurzlebiges, signiertes Sitzungs-Token aus (MEETING_SESSION_SECRET). Ein zweiter Endpoint tauscht dieses App-Token gegen ein kurzlebiges LiveKit-Token aus.
  • LiveKit läuft als SFU auf dem eigenen Server: Audio, Video und Bildschirm werden nicht zwischen allen Browsern vermasht, sondern einmal zum SFU gesendet und gezielt an die anderen Teilnehmer verteilt. Der Server zeichnet nichts auf oder speichert Medien.
  • Teilnehmer-Anmeldungen laufen über die Collection Anmeldungen (event-registrations), öffentlich erreichbar über /api/meetings/[slug]/register.

E-Mail-Erinnerungen

Erinnerungs-Mails (60/30 Minuten vorher, pro Termin im CMS einzeln an-/abschaltbar) werden nicht automatisch im Hintergrund verschickt, sondern müssen von einem externen Cron-Job ausgelöst werden, z. B. alle 5 Minuten:

curl -H "Authorization: Bearer $CRON_SECRET" https://anouma.org/api/cron/event-reminders

Der Versand ist idempotent (reminder60Sent/reminder30Sent je Anmeldung, hostReminder60Sent/hostReminder30Sent je Termin) - ein häufiger laufender Cron verschickt also nie doppelt.

Setup (Produktion / Deployment)

Produktion läuft komplett über GitHub Actions CI/CD - der Server baut nichts selbst, er zieht ein fertiges, getestetes Image aus GitHub Container Registry. Einmalig auf dem Server:

git clone https://github.com/whitehai11/anouma.git
cd anouma
chmod +x setup.sh
./setup.sh

setup.sh installiert bei Bedarf Docker, legt /opt/anouma an, generiert alle Secrets automatisch (nie erneut, wenn schon vorhanden), richtet einen eingeschränkten Deployment-User samt SSH-Key für GitHub Actions ein und zeigt am Ende genau die Werte an, die als GitHub Repository Secrets hinterlegt werden müssen. Das Docker-Image landet unter ghcr.io/whitehai11/anouma. ./setup.sh --non-interactive läuft ohne Rückfragen.

Jedes weitere Deployment läuft danach ausschließlich über einen Git-Tag:

git tag v1.2.0 && git push origin v1.2.0

GitHub Actions testet, baut das Image, pusht es nach GHCR und deployt es per SSH auf den Server - inklusive Backup, Healthcheck und automatischem Rollback bei Fehlschlag. Es gibt kein separates Update-Script.

Für alle Details (Architektur, GitHub Secrets, CI/Release-Workflows, Reverse-Proxy- und LiveKit-Einrichtung, Environment-Variablen, Backups, Rollback, Security) siehe DEPLOYMENT.md.

Lokale Entwicklung (ohne Docker für die App)

npm install
cp .env.example .env
# .env ausfüllen: DATABASE_URI auf 127.0.0.1 statt "postgres" setzen, PAYLOAD_SECRET (z. B. mit `openssl rand -base64 48`)

Datenbank

Nur Postgres per Docker, die App läuft direkt auf dem Host:

docker compose up -d postgres

(oder eine gehostete Postgres-Instanz, z. B. Neon/Supabase - einfach DATABASE_URI in .env entsprechend setzen).

Entwicklung

npm run dev

Vorhandene Inhalte ins CMS übertragen

npm run seed

Überträgt die 7 Angebote sowie die Texte für Startseite, Über mich, Angebote-Einleitung, Aktuelles-Einleitung, Kontakt und Termin buchen aus lib/texte.ts ins CMS. Kann gefahrlos mehrfach ausgeführt werden. Kontaktdaten (E-Mail/Telefon/Region) werden zunächst als Platzhalter gesetzt - bitte im Admin unter „Kontakt“ durch die echten Angaben ersetzen.

Produktions-Build ohne Docker

npm run build
npm run migrate   # wendet Datenbank-Migrationen an
npm run start

Für einen vollständig dockerisierten Produktionsbetrieb (App + Datenbank, Healthchecks, Reverse-Proxy-Integration) siehe oben bzw. DEPLOYMENT.md.

Weitere Skripte

Skript Zweck
npm run generate:types payload-types.ts aus der Config neu erzeugen
npm run generate:importmap Admin-Importmap neu erzeugen (nach neuen Feldtypen)
npm run migrate:create Neue Datenbank-Migration aus Config-Änderungen bauen
npm run lint ESLint
npm test Test-Suite (aktuell ein Platzhalter - noch keine Tests vorhanden)
npm run seo:check Optionale SEO-Prüfung (doppelte Slugs, fehlende Alt-Texte) - nicht produktionskritisch

Design

Siehe app/(frontend)/globals.css für das Farbsystem und AGENTS.md für die Next.js-Version-16-Hinweise, die für jede Code-Änderung in app/ gelten.

S
Description
No description provided
Readme
781 KiB
Languages
TypeScript 92%
Shell 4.8%
CSS 2.8%
Dockerfile 0.3%