Add plan.md and CLAUDE.md
Architecture decisions with rationale (plan.md) and repo-specific build/run commands plus style rules (CLAUDE.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
8b172e2c3c
commit
2298051bf4
2 files changed
+496
No files matched your search
@@ -0,0 +1,66 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
Website des Elternbeirats der IGMH. Blazor Web App mit statischem
|
||||||
|
Server-Side-Rendering, .NET 10. Läuft als Container auf Unraid hinter dem Nginx
|
||||||
|
Proxy Manager.
|
||||||
|
|
||||||
|
Architekturentscheidungen und deren Begründung: `plan.md`. Dort nachlesen, bevor
|
||||||
|
eine davon in Frage gestellt wird.
|
||||||
|
|
||||||
|
## Wann welches Dokument
|
||||||
|
|
||||||
|
| Thema | Datei |
|
||||||
|
|---|---|
|
||||||
|
| Warum SSR statt WASM, warum keine DB, offene Punkte, Meilensteine | `plan.md` |
|
||||||
|
| Unraid, compose, NPM-Proxy-Host, Registry, Rollback | `docs/deployment.md` |
|
||||||
|
| Strato, DynDNS, Mail-Records | `docs/dns.md` |
|
||||||
|
| Inhalte anlegen und ändern | `docs/inhalte-pflegen.md` |
|
||||||
|
| Impressum, Datenschutz, Fotos | `docs/recht.md` |
|
||||||
|
|
||||||
|
## Kommandos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project Elternbeirat.Web # lokal, Content-Reload per FileSystemWatcher
|
||||||
|
dotnet test # Smoke-Tests: jede Route liefert 200
|
||||||
|
docker compose build && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Es gibt keine Datenbank, keine Migrationen, kein Seeding. Ein `git clone` ist der
|
||||||
|
vollständige Stand.
|
||||||
|
|
||||||
|
## Nicht offensichtlich
|
||||||
|
|
||||||
|
- **Inhalte liegen im Image**, nicht in einem Volume. Jede Textänderung braucht
|
||||||
|
Commit und Rebuild. Das ist Absicht (`plan.md`, AE-3) — nicht „vereinfachen".
|
||||||
|
- **Kein `UseHttpsRedirection()`, kein `UseHsts()`.** NPM terminiert TLS; beides
|
||||||
|
erzeugt hinter dem Proxy eine Redirect-Schleife. `UseForwardedHeaders` mit
|
||||||
|
geleerten `KnownNetworks`/`KnownProxies` ist korrekt so, weil der Container kein
|
||||||
|
Port-Mapping hat und nur über NPM erreichbar ist.
|
||||||
|
- **Das chiseled-Runtime-Image hat keine Shell.** `HEALTHCHECK` mit `curl` oder
|
||||||
|
`sh` schlägt dort fehl.
|
||||||
|
- Neue Seite = Markdown in `Content/seiten/`. Neuer Termin = Eintrag in
|
||||||
|
`Content/termine.yml`. In beiden Fällen wird **kein** `.razor` angefasst.
|
||||||
|
- Slugs ohne Umlaute (`ueber-uns`, nicht `über-uns`).
|
||||||
|
|
||||||
|
## Regeln
|
||||||
|
|
||||||
|
- **Statisches SSR ist der Default.** Kein `@rendermode` ohne konkreten Anlass,
|
||||||
|
und wenn, dann an genau der einen Komponente — nie global.
|
||||||
|
- **Keine externen Ressourcen.** Keine CDN-Skripte, keine Google Fonts, keine
|
||||||
|
Maps- oder Video-Embeds. Schriften werden selbst ausgeliefert. Grund ist
|
||||||
|
Datenschutz, nicht Geschmack (`docs/recht.md`).
|
||||||
|
- **Keine Datenbank, kein EF Core, kein ORM.** Inhalte sind Dateien.
|
||||||
|
- **Inhalte gehören nach `Content/`**, nicht als Markup in Komponenten.
|
||||||
|
- Kein CMS, kein Login, keine Benutzerkonten.
|
||||||
|
- UI-Texte auf Deutsch, ohne Anglizismen. `Termine`, nicht `Events`.
|
||||||
|
- Neue NuGet-Abhängigkeiten vorher begründen. Bestand: `Markdig`, `YamlDotNet`.
|
||||||
|
|
||||||
|
## Stil
|
||||||
|
|
||||||
|
- Nullable aktiviert, `ImplicitUsings` an, File-scoped Namespaces.
|
||||||
|
- Services über DI, als Singleton registriert (Inhalte werden beim Start
|
||||||
|
eingelesen und gecacht).
|
||||||
|
- Öffentliche Typen und Methoden der `Services` bekommen XML-Doc, Razor-Markup
|
||||||
|
nicht.
|
||||||
|
- Fachbegriffe im Code auf Deutsch, wenn sie Domänenbegriffe sind
|
||||||
|
(`Termin`, `Protokoll`, `Beitrag`) — Framework-Begriffe bleiben englisch.
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
# elternbeirat-igmh.de — Neuaufbau
|
||||||
|
|
||||||
|
Ablösung der bisherigen WordPress-Seite durch eine eigene .NET-Anwendung auf
|
||||||
|
eigener Infrastruktur.
|
||||||
|
|
||||||
|
Stand: 2026-09-20 · Verantwortlich: Tom (Thomas Leininger)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ausgangslage
|
||||||
|
|
||||||
|
- Die Domain `elternbeirat-igmh.de` lag bei Maik Palm (ausscheidendes
|
||||||
|
Elternbeirat-Mitglied) im Paket „STRATO Hosting Basic", Auftragsnummer 9157927.
|
||||||
|
- Domaininhaber-Wechsel und Domainumzug sind beidseitig unterschrieben
|
||||||
|
(19.09.2026), die Einreichung bei Strato steht noch aus.
|
||||||
|
- Ziel-Paket: Toms „STRATO Mail Plus" (Auftragsnummer 8844576) — Domain + E-Mail,
|
||||||
|
**kein Webspace**.
|
||||||
|
- **E-Mail bleibt bei Strato.** Nur die Website zieht auf eigene Hardware.
|
||||||
|
- **Die bisherige Website ist aktuell offline.** Der Inhaltsbestand ist damit der
|
||||||
|
zeitkritischste offene Punkt (→ Abschnitt 9).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Ziele und Nicht-Ziele
|
||||||
|
|
||||||
|
**Ziele**
|
||||||
|
|
||||||
|
- Öffentliche Informationsseite des Elternbeirats: wer, wann, welche Protokolle,
|
||||||
|
wie erreichbar.
|
||||||
|
- Betrieb auf eigener Infrastruktur (Unraid), ohne fremden Hoster.
|
||||||
|
- Inhalte versionierbar und ohne Datenbank — ein `git clone` ist das vollständige
|
||||||
|
Backup.
|
||||||
|
- Wartungsarm: keine Plugin-Updates, keine PHP-Sicherheitslücken, kein CMS-Login
|
||||||
|
als Angriffsfläche.
|
||||||
|
|
||||||
|
**Nicht-Ziele (bewusst)**
|
||||||
|
|
||||||
|
- Kein CMS mit Web-Editor in Stufe 1. Falls andere Beiratsmitglieder später selbst
|
||||||
|
redaktionell arbeiten sollen, ist das ein eigener Ausbauschritt (→ Abschnitt 11).
|
||||||
|
- Keine Benutzerkonten, kein Login, kein Mitgliederbereich.
|
||||||
|
- Keine Datenbank.
|
||||||
|
- Keine externen Einbindungen (Fonts, Analytics, Maps, Social Widgets) — aus
|
||||||
|
Datenschutzgründen, siehe Abschnitt 10.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Architekturentscheidungen
|
||||||
|
|
||||||
|
### AE-1: Blazor mit Static Server-Side Rendering, nicht WebAssembly
|
||||||
|
|
||||||
|
**Entscheidung:** Blazor Web App mit Interaktivitätsmodus *None* (reines statisches
|
||||||
|
SSR). Zielframework .NET 10 (LTS).
|
||||||
|
|
||||||
|
**Begründung:** Blazor WASM lädt mehrere MB Runtime vor dem ersten sichtbaren
|
||||||
|
Buchstaben, liefert Suchmaschinen und Link-Vorschauen (WhatsApp, Signal, Messenger
|
||||||
|
— der Hauptverbreitungsweg bei Elternschaften) eine leere Shell und bringt auf
|
||||||
|
einer reinen Informationsseite keinerlei Gegenwert. Static SSR liefert fertiges
|
||||||
|
HTML, braucht kein JavaScript und kostet im Container rund 60 MB RAM.
|
||||||
|
|
||||||
|
**Konsequenz:** Interaktivität ist später pro Komponente nachrüstbar
|
||||||
|
(`@rendermode InteractiveServer` an genau der einen Komponente), ohne die
|
||||||
|
Architektur zu ändern.
|
||||||
|
|
||||||
|
**Verworfene Alternativen:**
|
||||||
|
|
||||||
|
| Alternative | Warum nicht |
|
||||||
|
|---|---|
|
||||||
|
| Blazor WASM | Payload, SEO, Link-Vorschauen, kein Nutzen |
|
||||||
|
| ASP.NET Core MVC/Razor Pages | Funktioniert genauso, aber Razor Components sind das modernere Modell |
|
||||||
|
| Statiq.Web (C#-SSG) → nginx | Ops-technisch am schlanksten (nichts zu patchen), aber jede Textänderung erzwingt einen Build-Lauf. Bleibt als Rückfallebene. |
|
||||||
|
| Astro/Hugo | Ausgereifteres SSG-Ökosystem, aber fremdes Terrain |
|
||||||
|
|
||||||
|
### AE-2: Inhalte als Dateien im Repo, nicht in einer Datenbank
|
||||||
|
|
||||||
|
**Entscheidung:** Seiteninhalte als Markdown mit YAML-Frontmatter, Termine als
|
||||||
|
strukturiertes YAML, Dokumente (Protokolle, Satzung) als PDF unter `wwwroot`.
|
||||||
|
|
||||||
|
**Begründung:** Kein DB-Backup, kein Migrationsschema, keine Konsistenzprobleme
|
||||||
|
zwischen Dateien und Datenbank. Änderungen sind Commits und damit nachvollziehbar
|
||||||
|
und rückrollbar. Für eine Seite mit ~10 Unterseiten und ein paar Terminen pro Jahr
|
||||||
|
ist alles andere Overhead.
|
||||||
|
|
||||||
|
**Konsequenz:** Textänderungen erfordern einen Commit und ein Redeploy. Das ist bei
|
||||||
|
erwarteten fünf Änderungen im Jahr akzeptabel — und der Grund, warum Abschnitt 11
|
||||||
|
den Ausbau zum Web-Editor als eigene Stufe führt.
|
||||||
|
|
||||||
|
### AE-3: Inhalte werden ins Image gebacken, nicht als Volume gemountet
|
||||||
|
|
||||||
|
**Entscheidung:** `Content/` und `wwwroot/` sind Teil des Images.
|
||||||
|
|
||||||
|
**Begründung:** Gemountete Inhalte aus dem Appdata-Share ließen sich zwar direkt
|
||||||
|
am NAS editieren, aber genau dann driften Repo und Live-Stand auseinander — und
|
||||||
|
die Eigenschaft „Backup = `git clone`" aus AE-2 wäre wertlos.
|
||||||
|
|
||||||
|
**Konsequenz:** Kein Schnell-Fix am Live-System. Tippfehler werden korrekt über
|
||||||
|
einen Commit behoben.
|
||||||
|
|
||||||
|
### AE-4: TLS und Zertifikate ausschließlich im Nginx Proxy Manager
|
||||||
|
|
||||||
|
**Entscheidung:** Der Container spricht nur HTTP auf Port 8080 und hat **kein**
|
||||||
|
Port-Mapping nach außen. NPM terminiert TLS und ist der einzige Weg zum Container.
|
||||||
|
|
||||||
|
**Begründung:** Entspricht dem bereits etablierten Muster im Home-Lab
|
||||||
|
(`cloud.anticarnist.de`). Zertifikatsverwaltung bleibt an einer Stelle.
|
||||||
|
|
||||||
|
**Konsequenz (wichtig):** In `Program.cs` **kein** `UseHttpsRedirection()` und
|
||||||
|
**kein** `UseHsts()` — sonst Redirect-Schleife hinter dem Proxy. HSTS setzt NPM.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Projekt anlegen (Rider)
|
||||||
|
|
||||||
|
Template **Blazor Web App** mit diesen Optionen:
|
||||||
|
|
||||||
|
| Option | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Framework | .NET 10.0 |
|
||||||
|
| Authentication | None |
|
||||||
|
| Interactive render mode | **None** |
|
||||||
|
| Include sample pages | aus |
|
||||||
|
| Configure for HTTPS | an (nur für lokale Entwicklung relevant) |
|
||||||
|
| Do not use top-level statements | egal |
|
||||||
|
| Enlist in .NET Aspire orchestration | **aus** |
|
||||||
|
|
||||||
|
Projektname: `Elternbeirat.Web`, Solution: `Elternbeirat`.
|
||||||
|
|
||||||
|
Wichtig ist allein *Interactive render mode = None* — damit erzeugt das Template
|
||||||
|
kein `.Client`-Projekt und kein WebAssembly-Bundle.
|
||||||
|
|
||||||
|
**Angelegt und bestätigt (2026-09-20):** Blazor Web App, `net10.0`, Interactive
|
||||||
|
render mode `None`, Auth `None`, Sample pages aus, Docker-Optionen im Dialog aus
|
||||||
|
(Dockerfile schreiben wir selbst, siehe Abschnitt 7). Git-Repository beim
|
||||||
|
Anlegen mit erzeugt. Solution liegt direkt unter
|
||||||
|
`RiderProjects\Elternbeirat\`, das Projekt in `Elternbeirat.Web\` darunter —
|
||||||
|
**kein** `src/`-Zwischenverzeichnis, anders als ursprünglich in Abschnitt 5
|
||||||
|
skizziert.
|
||||||
|
|
||||||
|
NuGet-Pakete, die dazukommen:
|
||||||
|
|
||||||
|
- `Markdig` — Markdown-Rendering
|
||||||
|
- `YamlDotNet` — Frontmatter und `termine.yml`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Repo-Struktur
|
||||||
|
|
||||||
|
```
|
||||||
|
Elternbeirat/ ← Repo-Root, = Solution-Verzeichnis
|
||||||
|
├── Elternbeirat.sln
|
||||||
|
├── plan.md ← dieses Dokument
|
||||||
|
├── CLAUDE.md ← minimal: Trigger, nicht-offensichtliche Kommandos
|
||||||
|
├── docs/
|
||||||
|
│ ├── deployment.md ← Unraid, NPM, Registry, Rollback
|
||||||
|
│ ├── dns.md ← Strato, DynDNS, Mail-Records
|
||||||
|
│ ├── inhalte-pflegen.md ← Anleitung für den Nicht-Alltagsfall
|
||||||
|
│ ├── inhalte-migration.md ← Übernahme aus dem alten WordPress
|
||||||
|
│ └── recht.md ← Impressum, Datenschutz, Fotos
|
||||||
|
├── Elternbeirat.Web/
|
||||||
|
│ ├── Elternbeirat.Web.csproj
|
||||||
|
│ ├── Components/
|
||||||
|
│ │ ├── App.razor
|
||||||
|
│ │ ├── Routes.razor
|
||||||
|
│ │ ├── Layout/ MainLayout, NavMenu, Footer
|
||||||
|
│ │ └── Pages/ Start, UeberUns, Termine, Protokolle,
|
||||||
|
│ │ News, NewsBeitrag, Kontakt,
|
||||||
|
│ │ Impressum, Datenschutz, Fehler404
|
||||||
|
│ ├── Content/ ← Inhalte, kein Code
|
||||||
|
│ │ ├── seiten/*.md
|
||||||
|
│ │ ├── news/2026-09-20-titel.md
|
||||||
|
│ │ └── termine.yml
|
||||||
|
│ ├── Services/
|
||||||
|
│ │ ├── MarkdownContentService.cs
|
||||||
|
│ │ ├── TermineService.cs
|
||||||
|
│ │ └── IcsWriter.cs
|
||||||
|
│ ├── wwwroot/
|
||||||
|
│ │ ├── css/site.css ← eigenes CSS, keine CDN-Einbindung
|
||||||
|
│ │ ├── img/
|
||||||
|
│ │ └── dokumente/ ← Protokolle, Satzung (PDF)
|
||||||
|
│ └── Program.cs
|
||||||
|
├── Elternbeirat.Web.Tests/ ← Smoke-Tests: jede Route liefert 200
|
||||||
|
├── Dockerfile
|
||||||
|
├── compose.yaml
|
||||||
|
├── .dockerignore
|
||||||
|
└── .gitea/workflows/deploy.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
`CLAUDE.md` bleibt bewusst kurz (Build-/Run-Kommandos, Stilregeln, Verweis auf
|
||||||
|
`docs/`). Die Details liegen in `docs/` und werden nur bei Bedarf gelesen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Inhaltsmodell
|
||||||
|
|
||||||
|
**Seite** (`Content/seiten/ueber-uns.md`):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
titel: Über uns
|
||||||
|
slug: ueber-uns
|
||||||
|
reihenfolge: 20
|
||||||
|
beschreibung: Wer im Elternbeirat der IGMH mitarbeitet und wie wir arbeiten.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Der Elternbeirat
|
||||||
|
|
||||||
|
Fließtext …
|
||||||
|
```
|
||||||
|
|
||||||
|
**Termin** (`Content/termine.yml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- titel: Elternbeiratssitzung
|
||||||
|
beginn: 2026-10-14T19:30:00
|
||||||
|
ende: 2026-10-14T21:00:00
|
||||||
|
ort: IGMH, Raum A103
|
||||||
|
oeffentlich: true
|
||||||
|
notiz: Gäste willkommen
|
||||||
|
```
|
||||||
|
|
||||||
|
**News-Beitrag** (`Content/news/2026-09-20-neue-website.md`) — wie Seite, plus
|
||||||
|
`datum` und `autor`.
|
||||||
|
|
||||||
|
Die Services lesen beim Start alles ein, cachen es im Speicher und stellen es
|
||||||
|
typisiert bereit. In Development zusätzlich ein `FileSystemWatcher`, damit
|
||||||
|
Textänderungen ohne Neustart sichtbar werden.
|
||||||
|
|
||||||
|
**Zusatznutzen ohne Mehraufwand:** ein ICS-Endpoint unter `/termine.ics`, der die
|
||||||
|
öffentlichen Termine ausliefert. Eltern abonnieren den Kalender einmal im Handy
|
||||||
|
und sehen jede Sitzung automatisch. Das ist der eine Punkt, an dem die Eigenbau-
|
||||||
|
Lösung die alte WordPress-Seite spürbar schlägt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Build und Deployment
|
||||||
|
|
||||||
|
### Stufe 1 — Handbetrieb (Start hier)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Für fünf Deployments im Jahr vollkommen ausreichend. CI vorab zu bauen wäre
|
||||||
|
Selbstzweck.
|
||||||
|
|
||||||
|
### Stufe 2 — Gitea Actions (Runner ist vorhanden)
|
||||||
|
|
||||||
|
`.gitea/workflows/deploy.yml` auf Push nach `main`:
|
||||||
|
|
||||||
|
1. `actions/checkout`
|
||||||
|
2. Login an der Gitea-eigenen Container-Registry
|
||||||
|
3. `docker/build-push-action` → `gitea.<domain>/tom/elternbeirat:latest` **und** `:${{ gitea.sha }}`
|
||||||
|
|
||||||
|
Zwei Stolpersteine, die erfahrungsgemäß Zeit kosten:
|
||||||
|
|
||||||
|
- Der `act_runner` im Docker-Modus braucht Zugriff auf einen Docker-Socket oder
|
||||||
|
einen DinD-Service, sonst schlägt `build-push-action` fehl.
|
||||||
|
- Die Gitea-Registry braucht ein Paket-Token mit Schreibrecht (`write:package`),
|
||||||
|
nicht das normale Login-Passwort.
|
||||||
|
|
||||||
|
**Immer auch den SHA-Tag pushen.** `:latest` allein macht Rollback unmöglich.
|
||||||
|
|
||||||
|
### Redeploy auf Unraid
|
||||||
|
|
||||||
|
Watchtower, aber **label-scoped** — sonst aktualisiert er ungefragt den ganzen
|
||||||
|
Home-Lab-Bestand:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# im Watchtower-Container
|
||||||
|
WATCHTOWER_LABEL_ENABLE: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# im eb-web-Service
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativ: manuell im Unraid-Docker-Tab „Update" drücken. Bei dieser
|
||||||
|
Änderungsfrequenz völlig legitim.
|
||||||
|
|
||||||
|
### Dockerfile (Skizze)
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY Elternbeirat.Web/Elternbeirat.Web.csproj Elternbeirat.Web/
|
||||||
|
RUN dotnet restore Elternbeirat.Web/Elternbeirat.Web.csproj
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet publish Elternbeirat.Web/Elternbeirat.Web.csproj -c Release -o /app
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app .
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["dotnet", "Elternbeirat.Web.dll"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Das chiseled-Image läuft ab .NET 8 standardmäßig als non-root (UID 1654) und
|
||||||
|
hört auf Port 8080. **Es enthält keine Shell** — ein `HEALTHCHECK` mit `curl`
|
||||||
|
funktioniert dort nicht. Entweder das normale `aspnet:10.0-noble` verwenden oder
|
||||||
|
die Überwachung NPM bzw. Uptime Kuma überlassen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Hosting auf Unraid
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# compose.yaml
|
||||||
|
services:
|
||||||
|
eb-web:
|
||||||
|
image: gitea.<domain>/tom/elternbeirat:latest
|
||||||
|
container_name: eb-web
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
ASPNETCORE_URLS: http://+:8080
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
networks: [npm]
|
||||||
|
labels:
|
||||||
|
com.centurylinklabs.watchtower.enable: "true"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
npm:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Kein `ports:`-Block. Der Container ist ausschließlich über das NPM-Docker-Netz
|
||||||
|
erreichbar.
|
||||||
|
|
||||||
|
**NPM Proxy Host:**
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Domain Names | `elternbeirat-igmh.de`, `www.elternbeirat-igmh.de` |
|
||||||
|
| Scheme | `http` |
|
||||||
|
| Forward Hostname | `eb-web` |
|
||||||
|
| Forward Port | `8080` |
|
||||||
|
| Block Common Exploits | an |
|
||||||
|
| Websockets Support | aus (wird bei statischem SSR nicht gebraucht) |
|
||||||
|
| SSL | Let's Encrypt, Force SSL, HTTP/2, HSTS |
|
||||||
|
|
||||||
|
**In `Program.cs` nicht vergessen:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||||
|
{
|
||||||
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
|
||||||
|
// NPM ist der einzige Weg zum Container (kein Port-Mapping),
|
||||||
|
// daher ist das Leeren der Allowlists hier vertretbar:
|
||||||
|
KnownNetworks = { }, KnownProxies = { }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Ohne das sieht die App jede Anfrage als HTTP und mit der Proxy-IP statt der
|
||||||
|
Client-IP — relevant für korrekte absolute URLs und für die Logs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Inhalte — zeitkritisch
|
||||||
|
|
||||||
|
Die alte Seite ist offline, Maiks Strato-Vertrag läuft aber noch. Solange er läuft,
|
||||||
|
ist der Webspace erreichbar; nach der Kündigung ist der Bestand endgültig weg.
|
||||||
|
|
||||||
|
**Reihenfolge der Rettungsversuche:**
|
||||||
|
|
||||||
|
1. Prüfen, was beim Termin mit Maik tatsächlich gesichert wurde (Dateien? MySQL-Dump?
|
||||||
|
Beides?). Ein vollständiger Dump wäre der Idealfall — daraus lassen sich Texte,
|
||||||
|
Seitenstruktur, Medien und PDFs sauber extrahieren.
|
||||||
|
2. Falls nur Dateien vorliegen: `wp-content/uploads` enthält Bilder und PDFs, die
|
||||||
|
Texte liegen aber in der Datenbank. Dann Schritt 3.
|
||||||
|
3. Wayback Machine auf Snapshots von `elternbeirat-igmh.de` prüfen.
|
||||||
|
4. Falls nichts davon greift: Maik bitten, vor Vertragsende noch einen Export zu
|
||||||
|
ziehen — oder Inhalte aus den Protokollen und von der Schulseite neu aufbauen.
|
||||||
|
|
||||||
|
**Diese Frage blockiert den Inhaltsteil, nicht den Technikteil.** Das Gerüst lässt
|
||||||
|
sich mit Platzhaltern bauen und später befüllen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Rechtliches
|
||||||
|
|
||||||
|
Kein Rechtsrat — aber die Punkte, an denen Schulseiten regelmäßig auffallen:
|
||||||
|
|
||||||
|
- **Impressum (§ 5 DDG):** Hat der Elternbeirat keine eigene Rechtsform, steht der
|
||||||
|
Betreiber persönlich mit Name und ladungsfähiger Anschrift im Impressum. Das ist
|
||||||
|
eine Entscheidung, keine Formalie — die Privatadresse wird damit öffentlich.
|
||||||
|
Alternative: Anschrift der Schule, aber nur mit deren ausdrücklichem Einverständnis
|
||||||
|
und wenn die Schule Mitbetreiberin ist.
|
||||||
|
- **Datenschutzerklärung:** Mit dem Umzug ist Tom Verantwortlicher im Sinne der DSGVO.
|
||||||
|
Server-Logs mit IP-Adressen benennen, Rechtsgrundlage und Löschfrist festlegen.
|
||||||
|
- **Keine externen Ressourcen.** Google Fonts, Maps, YouTube-Embeds und CDN-Skripte
|
||||||
|
übertragen die IP der Besucher an Dritte. Fonts werden selbst ausgeliefert.
|
||||||
|
- **Fotos von Kindern:** nur mit Einwilligung der Erziehungsberechtigten — bei
|
||||||
|
Schulseiten der mit Abstand häufigste Fehler. Im Zweifel keine Personenfotos.
|
||||||
|
- Hosting am privaten Anschluss bedeutet: Die öffentliche IP des Privatanschlusses
|
||||||
|
steht im DNS einer Schulseite. Bewusste Entscheidung, kein Nebeneffekt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Offene Punkte
|
||||||
|
|
||||||
|
| # | Punkt | Status |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Was wurde vom alten WordPress gesichert? | **offen, zeitkritisch** |
|
||||||
|
| 2 | Kann „STRATO Mail Plus" DynDNS? Laut Strato-FAQ ab „PowerWeb Basic 2013 bzw. STRATO Domain" — ob Mail Plus dazuzählt, ist unklar. Beim Support mit anfragen, solange der Umzugsvorgang läuft. | offen |
|
||||||
|
| 3 | DynDNS-Updater: Fritzbox (kennt als Exposed-Host-Vorschaltgerät die öffentliche IP) oder ddclient-Container auf Unraid? | offen |
|
||||||
|
| 4 | Sollen andere Beiratsmitglieder Inhalte selbst pflegen können? Falls ja: eigener Ausbauschritt (Decap CMS auf Git-Basis oder kleines Admin-UI). | offen |
|
||||||
|
| 5 | Kontaktformular gewünscht? Würde Server-Interaktivität und Spam-Schutz erfordern — `mailto:` ist die aufwandsfreie Alternative. | offen |
|
||||||
|
| 6 | Wer springt ein, wenn Tom nicht verfügbar ist? Eine Seite, die nur einer deployen kann, ist eine Abhängigkeit, die der Beirat kennen sollte. | offen |
|
||||||
|
| 7 | Formulare bei Strato einreichen (unterschrieben, liegt bereit) | offen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Umsetzungsreihenfolge
|
||||||
|
|
||||||
|
| # | Schritt | Abhängig von |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Strato-Formulare einreichen, Domainumzug anstoßen | — |
|
||||||
|
| 2 | Inhaltslage klären (Abschnitt 9) | — |
|
||||||
|
| 3 | Blazor-Projekt in Rider anlegen ✅ (2026-09-20) → nach Gitea pushen | — |
|
||||||
|
| 4 | Content-Pipeline: Markdig, YamlDotNet, Services, ICS-Endpoint | 3 |
|
||||||
|
| 5 | Layout, Navigation, Seiten mit Platzhaltern | 4 |
|
||||||
|
| 6 | Dockerfile, compose.yaml, lokal gegen NPM testen | 5 |
|
||||||
|
| 7 | Echte Inhalte einpflegen | 2, 5 |
|
||||||
|
| 8 | Impressum und Datenschutzerklärung | 7 |
|
||||||
|
| 9 | DNS umstellen, NPM Proxy Host, Let's Encrypt | 1, 6 |
|
||||||
|
| 10 | Gitea Actions + Registry + Watchtower | 6, 9 |
|
||||||
|
|
||||||
|
Schritte 1 und 2 laufen unabhängig vom Code und sollten sofort starten —
|
||||||
|
Schritt 2 ist der einzige, bei dem Warten echten Schaden anrichtet.
|
||||||
Reference in new issue
Block a user