Read all content from PocketBase, per request #30

Merged
Tom merged 7 commits from feature/pocketbase-consume into main 2026-09-23 16:02:46 +02:00
21 changed files with 154 additions and 327 deletions
Showing only changes of commit 47d9841532 - Show all commits

No files matched your search

+39 -24
View File
@@ -2,7 +2,8 @@
Website des Elternbeirats der IGMH. Blazor Web App mit statischem Website des Elternbeirats der IGMH. Blazor Web App mit statischem
Server-Side-Rendering, .NET 10. Läuft als Container auf Unraid hinter dem Nginx Server-Side-Rendering, .NET 10. Läuft als Container auf Unraid hinter dem Nginx
Proxy Manager. Proxy Manager. Die Inhalte liegen in **PocketBase** (zweiter Container); die App
liest sie pro Request über einen typisierten `PocketBaseClient`.
Architekturentscheidungen und deren Begründung: `docs/architektur.md`. Dort Architekturentscheidungen und deren Begründung: `docs/architektur.md`. Dort
nachlesen, bevor eine davon in Frage gestellt wird. nachlesen, bevor eine davon in Frage gestellt wird.
@@ -11,7 +12,7 @@ nachlesen, bevor eine davon in Frage gestellt wird.
| Thema | Datei | | Thema | Datei |
|---|---| |---|---|
| Warum SSR statt WASM, warum keine DB (AE-1 bis AE-4) | `docs/architektur.md` | | Warum SSR statt WASM, warum PocketBase als Datenschicht (AE-1 bis AE-4) | `docs/architektur.md` |
| Stack lokal starten, Tests, compose-Overlays | `docs/entwicklung.md` | | Stack lokal starten, Tests, compose-Overlays | `docs/entwicklung.md` |
| Unraid, compose, NPM-Proxy-Host, Registry, Rollback | `docs/deployment.md` | | Unraid, compose, NPM-Proxy-Host, Registry, Rollback | `docs/deployment.md` |
| Inhalte anlegen und ändern (PocketBase-Admin) | `docs/redaktion.md` | | Inhalte anlegen und ändern (PocketBase-Admin) | `docs/redaktion.md` |
@@ -26,28 +27,36 @@ Sub-Issues als Checkliste im Body, alle am Milestone „Elternbeirat-Website".
## Kommandos ## Kommandos
```bash ```bash
dotnet run --project Elternbeirat.Web # lokal; Inhalte werden beim Start gelesen (kein Auto-Reload) # Lokaler Stack (App + PocketBase) über das Dev-Overlay, siehe docs/entwicklung.md:
dotnet test # Smoke-Tests: jede Route liefert 200 docker compose -f compose.yaml -f compose.dev.yaml up --build # App :5000, PocketBase-Admin :8090/_/
docker compose build && docker compose up -d dotnet test # Smoke-Tests (starten eigene PocketBase) + Unit-Tests
``` ```
Es gibt keine Datenbank, keine Migrationen, kein Seeding. Ein `git clone` ist der Die App liest ihren Inhalt pro Request aus PocketBase; sie hält selbst keinen
vollständige Stand. Inhalt und liest nichts beim Start ein. Kein EF Core, keine Migrationen, kein
Seeding im Code. Ein `git clone` ist **nicht** der vollständige Stand — der
Inhalt liegt in PocketBase (`pb_data`), das separat gesichert wird (siehe
`docs/deployment.md`).
## Nicht offensichtlich ## Nicht offensichtlich
- **Inhalte liegen im Image**, nicht in einem Volume. Jede Textänderung braucht - **Inhalte liegen in PocketBase** (`pb_data`-Volume), nicht im Image. Eine
Commit und Rebuild. Das ist Absicht (`docs/architektur.md`, AE-3) — nicht Textänderung ist ein Eintrag im PocketBase-Admin und sofort live — kein Commit,
„vereinfachen". kein Rebuild (`docs/architektur.md`, AE-2/AE-3). Nur die statischen Assets unter
`wwwroot/` (CSS, Fonts, PDFs) sind ins Image gebacken.
- **Kein `UseHttpsRedirection()`, kein `UseHsts()`.** NPM terminiert TLS; beides - **Kein `UseHttpsRedirection()`, kein `UseHsts()`.** NPM terminiert TLS; beides
erzeugt hinter dem Proxy eine Redirect-Schleife. `UseForwardedHeaders` mit erzeugt hinter dem Proxy eine Redirect-Schleife. `UseForwardedHeaders` mit
geleerten `KnownNetworks`/`KnownProxies` ist korrekt so, weil der Container kein geleerten `KnownNetworks`/`KnownProxies` ist korrekt so, weil der Container kein
Port-Mapping hat und nur über NPM erreichbar ist. Port-Mapping hat und nur über NPM erreichbar ist.
- **Das chiseled-Runtime-Image hat keine Shell.** `HEALTHCHECK` mit `curl` oder - **Das chiseled-Runtime-Image hat keine Shell.** `HEALTHCHECK` mit `curl` oder
`sh` schlägt dort fehl. `sh` schlägt dort fehl.
- Neue Seite = Markdown in `Content/pages/`. Neuer Beitrag = Markdown in - Neue Seite / neuer Beitrag / neuer Termin / neue Frage = **Record in der
`Content/posts/`. Neuer Termin = Eintrag in `Content/events.yml`. In allen passenden PocketBase-Collection** (`pages` / `posts` / `events` / `faqs`), nicht
Fällen wird **kein** `.razor` angefasst. eine Datei und **kein** `.razor`. `pages` mit `slug` ist sofort unter `/<slug>`
erreichbar; `location`+`order` steuern das Menü, `embed` bettet Listen ein
(`docs/redaktion.md`). Eine **neue Art von Liste** (eigene Route/Logik) ist
dagegen Code: neue Collection + `PocketBaseClient`-Methode + Komponente
(`docs/entwicklung.md`).
- Slugs ohne Umlaute (`ueber-uns`, nicht `über-uns`). - Slugs ohne Umlaute (`ueber-uns`, nicht `über-uns`).
## Regeln ## Regeln
@@ -57,22 +66,28 @@ vollständige Stand.
- **Keine externen Ressourcen.** Keine CDN-Skripte, keine Google Fonts, keine - **Keine externen Ressourcen.** Keine CDN-Skripte, keine Google Fonts, keine
Maps- oder Video-Embeds. Schriften werden selbst ausgeliefert. Grund ist Maps- oder Video-Embeds. Schriften werden selbst ausgeliefert. Grund ist
Datenschutz, nicht Geschmack (`docs/recht.md`). Datenschutz, nicht Geschmack (`docs/recht.md`).
- **Keine Datenbank, kein EF Core, kein ORM.** Inhalte sind Dateien. - **Kein EF Core, kein ORM, keine Migrationen.** Inhalte kommen aus PocketBase
- **Inhalte gehören nach `Content/`**, nicht als Markup in Komponenten. über den `PocketBaseClient` (REST), nicht aus einer relationalen DB im Code.
- Kein CMS, kein Login, keine Benutzerkonten. - **Inhalte gehören nach PocketBase**, nicht als Markup in Komponenten und nicht
in Dateien im Repo.
- Kein eigenes CMS, kein Login und keine Benutzerkonten **in der Website** —
gepflegt wird ausschließlich im PocketBase-Admin (Redakteure sind Superuser).
- UI-Texte auf Deutsch, ohne Anglizismen. `Termine`, nicht `Events`. - UI-Texte auf Deutsch, ohne Anglizismen. `Termine`, nicht `Events`.
- Neue NuGet-Abhängigkeiten vorher begründen. Bestand: `Markdig`, `YamlDotNet`. - Neue NuGet-Abhängigkeiten vorher begründen. Genutzt wird nur `Markdig`
(Markdown → HTML). `YamlDotNet` stammt aus der abgelösten Datei-Schicht, wird
nicht mehr verwendet und soll aus dem `csproj` entfernt werden.
## Stil ## Stil
- Nullable aktiviert, `ImplicitUsings` an, File-scoped Namespaces. - Nullable aktiviert, `ImplicitUsings` an, File-scoped Namespaces.
- Services über DI, als Singleton registriert (Inhalte werden beim Start - Datenzugriff über den typisierten `PocketBaseClient` (registriert via
eingelesen und gecacht). `AddHttpClient`), der pro Request liest — kein Start-Cache, kein Singleton mit
- Öffentliche Typen und Methoden der `Services` bekommen XML-Doc (auf Englisch, Inhalten. Komponenten liegen feature-basiert unter `Features/<Bereich>/`.
leicht verständlich), Razor-Markup nicht. - Öffentliche Typen und Methoden im `PocketBaseClient` und in `Contracts`
bekommen XML-Doc (auf Englisch, leicht verständlich), Razor-Markup nicht.
- **Code durchgängig auf Englisch** — Typen, Member, Variablen, Kommentare, - **Code durchgängig auf Englisch** — Typen, Member, Variablen, Kommentare,
Skripte und Doku. Das gilt **auch für Domänenbegriffe**: im Code `Event`, nicht Skripte und Doku. Das gilt **auch für Domänenbegriffe**: im Code `Event`, nicht
`Termin`; `Post`, nicht `Beitrag`. Deutsch bleibt ausschließlich, was ein `Termin`; `Post`, nicht `Beitrag`. Deutsch bleibt ausschließlich, was ein
Besucher liest oder ein Redakteur pflegt: UI-Texte, Markdown-Inhalte, Besucher liest oder ein Redakteur pflegt: UI-Texte sowie die **Werte** der
Frontmatter-Schlüssel wie `titel:` sowie Slugs/Dateinamen unter `Content/` PocketBase-Records (z. B. `title: Vorstandsteam`, der Markdown-`body`). Die
(z. B. `vorstandsteam`, `/termine`). Feldnamen und Slugs bleiben dagegen englisch (`title`, `slug`, `/board`).
-1
View File
@@ -9,7 +9,6 @@
<ItemGroup> <ItemGroup>
<PackageVersion Include="Markdig" Version="0.38.0" /> <PackageVersion Include="Markdig" Version="0.38.0" />
<PackageVersion Include="YamlDotNet" Version="16.2.1" />
</ItemGroup> </ItemGroup>
<!-- Test-only packages. --> <!-- Test-only packages. -->
-26
View File
@@ -1,26 +0,0 @@
# Calendar entries of the Elternbeirat.
#
# One entry per event. Required keys: title, start.
# start/end:
# date only -> "2026-03-15" (all-day)
# date with time -> "2026-03-15 19:30"
# location and note are optional.
- title: Elternbeiratssitzung
start: "2026-10-08 19:30"
location: Lehrerzimmer
note: Themen bitte vorab per E-Mail einreichen.
- title: Herbstbasar
start: "2026-11-14 10:00"
end: "2026-11-14 16:00"
location: Aula
- title: Weihnachtsferien
start: "2026-12-21"
end: "2027-01-06"
- title: Elternsprechtag
start: "2026-09-05 15:00"
end: "2026-09-05 18:00"
location: Klassenräume
@@ -1,22 +0,0 @@
---
title: Datenschutz
---
# Datenschutzerklärung
*TODO: Rechtlich verbindliche Datenschutzerklärung ergänzen. Erst nach Freigabe
finalisieren – siehe `docs/recht.md`.*
Diese Website wird bewusst ohne externe Ressourcen betrieben: keine CDN-Skripte,
keine Google Fonts, keine Karten- oder Video-Einbettungen. Schriften werden von
unserem eigenen Server ausgeliefert. Dadurch werden beim Besuch keine Daten an
Dritte übertragen.
## Verantwortliche Stelle
*TODO: siehe [Impressum](/impressum).*
## Server-Logdaten
*TODO: beschreiben, welche Zugriffsdaten der Server (bzw. der vorgelagerte
Proxy) protokolliert und wie lange.*
@@ -1,17 +0,0 @@
---
title: Downloads
---
# Downloads
Hier stellen wir Unterlagen zur Elternarbeit als Datei bereit: Protokolle der
Sitzungen, Elterninformationen und die Geschäftsordnung.
## Unterlagen
*TODO: Download-Liste ergänzen. Dateien liegen unter `wwwroot/downloads/` und
werden hier verlinkt, z. B.:*
- *Protokoll der Vollversammlung (PDF)*
- *Geschäftsordnung des Elternbeirats (PDF)*
- *Elterninformation Elternvertreter (PDF)*
@@ -1,20 +0,0 @@
---
title: FAQ Elternarbeit
---
# Elternarbeit
Unterstützung für neu gewählte und erfahrene Elternvertreterinnen und
Elternvertreter.
## Welche Aufgaben habe ich als Elternvertreter?
*TODO: Aufgaben und Rechte der Klassenelternvertretung beschreiben.*
## Wie läuft die Zusammenarbeit mit dem Elternbeirat?
*TODO: Sitzungsrhythmus und Ansprechpartner ergänzen.*
## Wo finde ich Vorlagen und Unterlagen?
Unterlagen und Protokolle finden Sie im Bereich [Downloads](/downloads).
@@ -1,22 +0,0 @@
---
title: FAQ Elterneuro
---
# Elterneuro
Der Elterneuro ist ein freiwilliger Beitrag der Eltern, mit dem der Elternbeirat
Projekte an der Schule unterstützt.
## Wofür wird der Elterneuro verwendet?
*TODO: konkrete Beispiele für die Verwendung ergänzen.*
## Wie hoch ist der Beitrag?
Der Elterneuro ist freiwillig.
*TODO: übliche Beitragshöhe und Zahlungsweg ergänzen.*
## An wen kann ich mich bei Fragen wenden?
Bei Fragen erreichen Sie uns über die [Kontaktseite](/kontakt).
@@ -1,21 +0,0 @@
---
title: FAQ Mensa
---
# Mensa
Alles rund um das Mittagessen an der IGMH: Anmeldung, Guthaben und Fristen.
## Wie melde ich mein Kind an?
Die Essensbestellung läuft über das System i-NET Menue.
*TODO: Ablauf der Erstanmeldung und Zugangsdaten beschreiben.*
## Wie lade ich Guthaben auf?
*TODO: Aufladeweg und Zahlungsarten ergänzen.*
## Bis wann kann ich bestellen oder stornieren?
*TODO: Fristen für Bestellung und Stornierung ergänzen.*
@@ -1,21 +0,0 @@
---
title: FAQ Schließfach
---
# Schließfächer
Informationen zur Anmietung eines Schließfachs an der IGMH.
## Welche Größen gibt es und was kosten sie?
*TODO: Verfügbare Größen und Preise ergänzen.*
## Wie miete ich ein Schließfach an?
Die Verwaltung läuft über das Serviceportal von AstraDirect.
*TODO: Ablauf der Anmietung und Link zum Portal ergänzen.*
## Wie tausche oder kündige ich mein Fach?
*TODO: Vorgehen für Fachtausch und Kündigung ergänzen.*
-15
View File
@@ -1,15 +0,0 @@
---
title: FAQ
---
# Häufige Fragen
Hier finden Eltern Antworten auf wiederkehrende Fragen rund um den Schulalltag.
Die Themen sind nach Bereichen aufgeteilt:
- [Mensa](/faq-mensa) – Anmeldung, Guthaben, Fristen
- [Schließfächer](/faq-schliessfach) – Größen, Preise, Verwaltung
- [Elterneuro](/faq-elterneuro) – freiwilliger Beitrag und Verwendung
- [Elternarbeit](/faq-elternarbeit) – Tipps für Elternvertreterinnen und Elternvertreter
*TODO: Reihenfolge und weitere Themen ergänzen, sobald die Unterseiten stehen.*
@@ -1,19 +0,0 @@
---
title: Förderverein
---
# Förderverein „Freunde der IGMH"
Der Förderverein „Freunde der IGMH" unterstützt die Schule bei Anschaffungen und
Projekten, die aus dem regulären Budget nicht finanziert werden können. Mitglieder
sind Eltern, Lehrkräfte, Ehemalige und Förderer der Schule.
## Was der Verein fördert
- Ausstattung für Unterricht und Arbeitsgemeinschaften
- Musische, sportliche und kulturelle Projekte
- Anschaffungen, die allen Schülerinnen und Schülern zugutekommen
## Mitglied werden
*TODO: Beitrittsformular bzw. Ansprechpartner und Beitragshöhe ergänzen.*
@@ -1,24 +0,0 @@
---
title: Impressum
---
# Impressum
*TODO: Rechtlich verbindliches Impressum ergänzen. Erst nach Freigabe mit echten
Daten füllen – siehe `docs/recht.md`.*
## Angaben gemäß § 5 DDG
*TODO: Name und Anschrift des Diensteanbieters (Elternbeirat / Schule).*
## Vertreten durch
*TODO: gesetzlicher Vertreter.*
## Kontakt
*TODO: E-Mail-Adresse (siehe [Kontakt](/kontakt)).*
## Verantwortlich i. S. d. § 18 Abs. 2 MStV
*TODO: Name und Anschrift der verantwortlichen Person.*
-16
View File
@@ -1,16 +0,0 @@
---
title: Kontakt
---
# Kontakt
Sie erreichen den Elternbeirat der IGMH per E-Mail:
[TODO-adresse@example.org](mailto:TODO-adresse@example.org)
*TODO: Echte Kontakt-E-Mail-Adresse eintragen.*
## Anschrift
Elternbeirat der IGMH
*TODO: Anschrift der Schule ergänzen.*
@@ -1,18 +0,0 @@
---
title: Vorstandsteam
---
# Das Vorstandsteam
Der Elternbeirat der IGMH wird von einem ehrenamtlichen Vorstand geleitet.
Er vertritt die Elternschaft gegenüber Schule und Schulträger und koordiniert
die Arbeit der Klassenelternvertreter.
## Aufgaben des Vorstands
- Vertretung der Eltern in der Schulkonferenz
- Zusammenarbeit mit Schulleitung und Kollegium
- Organisation der Vollversammlungen und Sitzungen
- Ansprechpartner für Fragen rund um Mensa, Schließfächer und Förderverein
*Die namentliche Vorstellung der Vorstandsmitglieder folgt.*
@@ -1,11 +0,0 @@
---
title: Neue Sporthalle feierlich eröffnet
date: 2025-10-05
---
# Neue Sporthalle feierlich eröffnet
Die neue Sporthalle der IGMH ist eröffnet. Schülerinnen, Schüler und Lehrkräfte
haben damit deutlich mehr Platz für Sportunterricht und Arbeitsgemeinschaften.
*TODO: Bericht und Fotos ergänzen.*
@@ -1,12 +0,0 @@
---
title: Neuer Vorstand des Elternbeirats gewählt
date: 2025-02-26
---
# Neuer Vorstand des Elternbeirats gewählt
Bei der Vollversammlung hat der Elternbeirat der IGMH einen neuen Vorstand
gewählt. Das Team bedankt sich für das entgegengebrachte Vertrauen und freut
sich auf die gemeinsame Arbeit im neuen Schuljahr.
*TODO: Namen und Ämter des neuen Vorstands ergänzen (nach Freigabe).*
-7
View File
@@ -19,7 +19,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Markdig" /> <PackageReference Include="Markdig" />
<PackageReference Include="YamlDotNet" />
</ItemGroup> </ItemGroup>
<!-- The typed PocketBaseClient the app reads its content from. Referencing <!-- The typed PocketBaseClient the app reads its content from. Referencing
@@ -28,10 +27,4 @@
<ProjectReference Include="..\Elternbeirat.PocketBase\Elternbeirat.PocketBase.csproj" /> <ProjectReference Include="..\Elternbeirat.PocketBase\Elternbeirat.PocketBase.csproj" />
</ItemGroup> </ItemGroup>
<!-- Content ships as files inside the image, not in a volume. Copy it to
the output directory so the container can find it. -->
<ItemGroup>
<Content Include="Content\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project> </Project>
+42 -22
View File
@@ -2,12 +2,13 @@
Why the site is built the way it is. Read this before questioning one of these Why the site is built the way it is. Read this before questioning one of these
decisions — each records the reasoning, not just the choice. Operational how-to decisions — each records the reasoning, not just the choice. Operational how-to
lives in `docs/deployment.md`; open work lives in `tasks/`. lives in `docs/deployment.md`; open work lives in the Gitea issues (milestone
„Elternbeirat-Website").
> **Note on scope.** These decisions describe stage 1: a static information site > **Note on scope.** The site started as a static information site with content
> with content as files. The move to editor-based content maintenance > as files (stage 1). Editorial maintenance without a developer then replaced
> (`tasks/001-redaktion-ohne-entwickler.md`) deliberately revisits AE-2 and AE-3 — > that file layer with PocketBase, which revised **AE-2** and **AE-3** below —
> when that lands, update this file. > each now records both the original decision and why it changed.
## AE-1: Blazor with static server-side rendering, not WebAssembly ## AE-1: Blazor with static server-side rendering, not WebAssembly
@@ -24,30 +25,49 @@ costs around 60 MB of RAM in the container.
(`@rendermode InteractiveServer` on exactly that one component), without changing the (`@rendermode InteractiveServer` on exactly that one component), without changing the
architecture. Never global. architecture. Never global.
## AE-2: Content as files in the repo, not in a database ## AE-2: Content in PocketBase, read per request (revised)
**Decision:** Page content as Markdown with YAML frontmatter, events as structured **Decision:** All visible content lives in **PocketBase** (collections `pages`,
YAML (`Content/events.yml`), documents as PDF under `wwwroot`. `posts`, `events`, `faqs`); the app reads it over the REST API per request via a
typed `PocketBaseClient`. Documents remain PDFs under `wwwroot`.
**Rationale:** No DB backup, no migration schema, no consistency problems between **Original decision (stage 1):** Content as files in the repo — Markdown with YAML
files and database. Changes are commits and are therefore traceable and reversible. frontmatter, events as `Content/events.yml`. The rationale then was: no DB backup,
For a site with ~10 subpages and a few events per year, anything else is overhead. no migration schema, no file/DB drift, and "backup = `git clone`". For a handful of
pages that was the leanest option.
**Consequence:** Text changes require a commit and a redeploy. With an expected five **Why it changed:** The site needs to be maintainable by the board **without a
changes a year that is acceptable — and the reason the editor expansion developer and without a redeploy**. Files meant every text fix was a commit and a
(`tasks/001`) is treated as a separate stage. rebuild. PocketBase keeps the "small, self-hostable, one binary" spirit while
letting an editor change content live in its admin UI. It has **no** migration
schema of the EF/ORM kind — the collections are its data — so the objection that
weighed against "a database" in stage 1 does not apply here.
## AE-3: Content is baked into the image, not mounted as a volume **Consequence:** Editing is now a task in the PocketBase admin, not a commit (see
`redaktion.md`). The app holds no content of its own; if PocketBase is unreachable a
page degrades to empty rather than failing. "Backup = `git clone`" no longer covers
the content — the backup is now `pb_data` (see AE-3 and `deployment.md`).
**Decision:** `Content/` and `wwwroot/` are part of the image. ## AE-3: Content lives in PocketBase's `pb_data`, not baked into the image (revised)
**Rationale:** Mounted content from the appdata share could be edited directly on the **Decision:** The editable content lives in PocketBase's `pb_data`, a persistent
NAS, but that is exactly when the repo and the live state drift apart — and the volume separate from the app image. Only the static assets under `wwwroot/` (CSS,
"backup = `git clone`" property from AE-2 would be worthless. fonts, PDFs) are baked into the image.
**Consequence:** No quick fix on the live system. Typos are corrected properly via a **Original decision (stage 1):** `Content/` and `wwwroot/` were both baked into the
commit. Note: the content is read once at startup and cached — there is **no** image, deliberately **not** a mounted volume, so the live state could never drift
`FileSystemWatcher` (a change needs a restart / `dotnet watch`). from the repo and "backup = `git clone`" held.
**Why it changed:** Editing content live (AE-2) is only possible if that content
survives a redeploy — so it must be a volume, not part of the image. The drift
concern is answered differently now: the content simply has one home (PocketBase),
and the repo no longer claims to hold it. The app image stays stateless and can be
rebuilt and redeployed at any time without touching the data.
**Consequence:** The backup is a copy of `pb_data` (plus a restore test), not a
`git clone` (set up in issue #9, see `deployment.md`). The app reads content per
request over HTTP, so there is nothing cached at startup and no `FileSystemWatcher`
— an editorial change is live immediately, no restart needed.
## AE-4: TLS and certificates exclusively in the Nginx Proxy Manager ## AE-4: TLS and certificates exclusively in the Nginx Proxy Manager
+29
View File
@@ -100,3 +100,32 @@ The app reads its content from PocketBase over the compose network
inside the compose network, independent of the container name). Editing content inside the compose network, independent of the container name). Editing content
means editing records in the PocketBase admin UI at means editing records in the PocketBase admin UI at
<http://localhost:8090/_/>, not editing files. See `redaktion.md`. <http://localhost:8090/_/>, not editing files. See `redaktion.md`.
---
## Adding a new content type
Editors can add **records** to the existing collections and add plain text
`pages`, but a **new kind of list** — its own collection rendered on its own
route with its own sorting or grouping — is a development task, not editorial.
It takes three steps, all small because the plumbing is shared:
1. **Collection + DTO.** Create the collection in the PocketBase admin (give it a
`public` bool and open List/View rules, like the others), then add a matching
record type in `Elternbeirat.Contracts` with `[JsonPropertyName(...)]` on each
field — mirror `Page`/`Post`.
2. **Client method.** Add one method to `PocketBaseClient`. The shared
`GetRecordsAsync<T>(collection, sort, ct)` already does the `public=true`
filter, the sort and the deserialization, so a new type is a one-liner:
`public Task<IReadOnlyList<Minutes>> GetMinutesAsync(CancellationToken ct = default)`
`=> GetRecordsAsync<Minutes>("minutes", "-date", ct);`
3. **Feature component.** Add a component under `Features/<Name>/` with its own
`@page "/…"` route that injects `PocketBaseClient`, calls the new method and
renders the result. Literal routes win over the `/{Slug}` catch-all, so pick a
slug that no `pages` record needs. Follow an existing list (`PostList`,
`EventList`, `FaqList`) for the load-in-`OnInitializedAsync`, log-and-degrade
pattern, and add a route smoke test plus a fixture seed.
Text pages need none of this — they are all served generically by
`ContentPage` (`@page "/{Slug}"`), which is why a new `pages` record is live
without a rebuild.
+4 -3
View File
@@ -1,9 +1,10 @@
# Legal — imprint, privacy, photos # Legal — imprint, privacy, photos
Not legal advice — but the points where school sites regularly get flagged. The Not legal advice — but the points where school sites regularly get flagged. The
actual imprint and privacy texts live in `Content/pages/impressum.md` and actual imprint and privacy texts are `pages` records in PocketBase (slugs
`Content/pages/datenschutz.md`; filling them in with released, real data is tracked `imprint` and `privacy`), edited in the admin UI — see `redaktion.md`. Filling
in `tasks/003-echte-inhalte-vor-go-live.md`. them in with released, real data before go-live is tracked as a Gitea issue
(milestone „Elternbeirat-Website").
## Imprint (§ 5 DDG) ## Imprint (§ 5 DDG)
+40 -6
View File
@@ -5,11 +5,9 @@ Wie die sichtbaren Inhalte der Website gepflegt werden. Der Inhalt liegt in
über die Weboberfläche bearbeitet. Keine Dateien, kein Commit, kein Rebuild: eine über die Weboberfläche bearbeitet. Keine Dateien, kein Commit, kein Rebuild: eine
Änderung im Admin ist sofort live. Änderung im Admin ist sofort live.
> **Übergang (Stand #8).** Der Inhalt ist bereits vollständig in PocketBase; die > **Stand.** Die Blazor-App liest ihren gesamten Inhalt aus PocketBase (Issue #8);
> Blazor-App wird gerade darauf umgestellt, ihn von dort zu lesen (Issue #8). > die alte Datei-Schicht unter `Content/` gibt es nicht mehr. PocketBase ist damit
> Solange das läuft, kann die ausgelieferte Seite noch aus den alten Dateien > die einzige Quelle und dieser Abschnitt der einzige Pflegeweg.
> unter `Content/` stammen. Sobald #8 durch ist, ist PocketBase die einzige
> Quelle und dieser Abschnitt der einzige Pflegeweg.
> **Sprachkonvention.** Feldnamen und Slugs sind **englisch** (`title`, `slug`, > **Sprachkonvention.** Feldnamen und Slugs sind **englisch** (`title`, `slug`,
> `board`, `posts`). Der Text, den ein Besucher liest, bleibt **deutsch** — also > `board`, `posts`). Der Text, den ein Besucher liest, bleibt **deutsch** — also
@@ -49,16 +47,52 @@ Jede Collection hat als **letztes Feld `public`** (ja/nein). Nur Records mit
`public = true` erscheinen auf der Website — so lässt sich ein Entwurf anlegen, `public = true` erscheinen auf der Website — so lässt sich ein Entwurf anlegen,
ohne dass er schon sichtbar ist. ohne dass er schon sichtbar ist.
### Was redaktionell geht — und was Code braucht
Redaktion arbeitet mit **Daten**, nicht mit Struktur. Konkret:
| Was du willst | Geht redaktionell? | Wie |
|---|---|---|
| Beitrag / Termin / Frage hinzufügen | **Ja** | Neuer Record in `posts` / `events` / `faqs` — erscheint sofort in der jeweiligen Liste |
| Neue Textseite | **Ja** | Neuer Record in `pages` (siehe unten) |
| Eine Seite mit einer Liste anteasern | **Ja** | Feld `embed` der Seite (`posts`/`events`/`faqs`) |
| **Neue Art von Liste** (eigene Seite mit eigener Sortierung/Gruppierung und eigener Adresse, z. B. „Protokolle") | **Nein** | Braucht Entwicklung: neue Collection **plus** Code |
Das heißt: **Bestehende Listen lassen sich beliebig erweitern**, aber eine *neue*
Listenart entsteht nicht durch Anlegen einer Collection allein. Die drei Listen
(`posts`, `events`, `faqs`) sind fest verdrahtet, weil jede eine eigene
Darstellung hat — Datum und Sortierung, Kalender/`.ics`, Themen-Gruppierung. Eine
weitere solche Ansicht anzulegen ist ein **Entwickler-Task**: eine neue Collection
in PocketBase, eine Lesemethode im `PocketBaseClient` und eine eigene Komponente
mit ihrer Route (siehe `docs/entwicklung.md`). Das ist Absicht: Inhalte sind
Daten, Struktur und Darstellung sind Code.
--- ---
## Seiten (`pages`) ## Seiten (`pages`)
Eine Seite hat `title` (Überschrift für Menschen, Umlaute erlaubt), `slug` (die Eine Seite hat `title` (Überschrift für Menschen, Umlaute erlaubt), `slug` (die
URL, klein und **ohne Umlaute**: `board`, nicht `über-uns`) und `body` (der URL, klein und **ohne Umlaute**: `board`, nicht `über-uns`) und `body` (der
Text, als Editor-Feld). Text, als **Markdown**).
**Eine neue Seite anlegen** — Record in `pages`, dann ist die Adresse `/<slug>`
sofort live, ohne Code und ohne Rebuild:
1. `title` setzen (Pflicht).
2. `slug` setzen (Pflicht), englisch und ohne Umlaute — z. B. `slug = schulweg`
ergibt `/schulweg`.
3. `body` als Markdown schreiben (optional; darf leer bleiben).
4. **`public` anhaken.** Ohne Haken ist die Seite ein Entwurf und erscheint auf
der Website als „nicht gefunden" (404) — das ist der häufigste Stolperstein.
5. Optional `location` (`header`/`footer`) **und** `order` setzen, damit die Seite
ins Menü kommt; optional `embed` für eingebettete Blöcke (siehe unten).
- **Slug bleibt englisch und ohne Umlaute.** `imprint`, `privacy`, `contact`, - **Slug bleibt englisch und ohne Umlaute.** `imprint`, `privacy`, `contact`,
`board`, `patrons`. Der `title` darf deutsch mit Umlauten sein (`Förderverein`). `board`, `patrons`. Der `title` darf deutsch mit Umlauten sein (`Förderverein`).
- **Reservierte Slugs.** `posts`, `events`, `faqs` und `/` (die Startseite `home`)
gehören den festen Listen-/Sonderansichten. Eine `pages`-Seite mit einem dieser
Slugs wird von der jeweiligen Ansicht verdeckt und **nicht** angezeigt; in der
Produktion dienen solche Records nur als Menü-Platzhalter.
- **Menü:** Ob eine Seite ins Menü kommt, steht an der Seite selbst — die Felder - **Menü:** Ob eine Seite ins Menü kommt, steht an der Seite selbst — die Felder
`location` (`header` oder `footer`) und `order` (Reihenfolge, ab 1). Die App `location` (`header` oder `footer`) und `order` (Reihenfolge, ab 1). Die App
baut Kopf- und Fußnavigation daraus; nichts wird im Markup angefasst. baut Kopf- und Fußnavigation daraus; nichts wird im Markup angefasst.