Remove the leftover file content layer and align the docs

This commit is contained in:
tleininger committed 2026-09-23 15:30:53 +02:00
1 parent 3fd3796d73
commit 47d9841532
21 files changed
+154 -327

No files matched your search

+42 -22
View File
@@ -2,12 +2,13 @@
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
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
> with content as files. The move to editor-based content maintenance
> (`tasks/001-redaktion-ohne-entwickler.md`) deliberately revisits AE-2 and AE-3 —
> when that lands, update this file.
> **Note on scope.** The site started as a static information site with content
> as files (stage 1). Editorial maintenance without a developer then replaced
> that file layer with PocketBase, which revised **AE-2** and **AE-3** below —
> each now records both the original decision and why it changed.
## 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
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
YAML (`Content/events.yml`), documents as PDF under `wwwroot`.
**Decision:** All visible content lives in **PocketBase** (collections `pages`,
`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
files and database. Changes are commits and are therefore traceable and reversible.
For a site with ~10 subpages and a few events per year, anything else is overhead.
**Original decision (stage 1):** Content as files in the repo — Markdown with YAML
frontmatter, events as `Content/events.yml`. The rationale then was: no DB backup,
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
changes a year that is acceptable — and the reason the editor expansion
(`tasks/001`) is treated as a separate stage.
**Why it changed:** The site needs to be maintainable by the board **without a
developer and without a redeploy**. Files meant every text fix was a commit and a
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
NAS, but that is exactly when the repo and the live state drift apart — and the
"backup = `git clone`" property from AE-2 would be worthless.
**Decision:** The editable content lives in PocketBase's `pb_data`, a persistent
volume separate from the app image. Only the static assets under `wwwroot/` (CSS,
fonts, PDFs) are baked into the image.
**Consequence:** No quick fix on the live system. Typos are corrected properly via a
commit. Note: the content is read once at startup and cached — there is **no**
`FileSystemWatcher` (a change needs a restart / `dotnet watch`).
**Original decision (stage 1):** `Content/` and `wwwroot/` were both baked into the
image, deliberately **not** a mounted volume, so the live state could never drift
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
+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
means editing records in the PocketBase admin UI at
<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
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
`Content/pages/datenschutz.md`; filling them in with released, real data is tracked
in `tasks/003-echte-inhalte-vor-go-live.md`.
actual imprint and privacy texts are `pages` records in PocketBase (slugs
`imprint` and `privacy`), edited in the admin UI — see `redaktion.md`. Filling
them in with released, real data before go-live is tracked as a Gitea issue
(milestone „Elternbeirat-Website").
## 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
Änderung im Admin ist sofort live.
> **Übergang (Stand #8).** Der Inhalt ist bereits vollständig in PocketBase; die
> Blazor-App wird gerade darauf umgestellt, ihn von dort zu lesen (Issue #8).
> Solange das läuft, kann die ausgelieferte Seite noch aus den alten Dateien
> unter `Content/` stammen. Sobald #8 durch ist, ist PocketBase die einzige
> Quelle und dieser Abschnitt der einzige Pflegeweg.
> **Stand.** Die Blazor-App liest ihren gesamten Inhalt aus PocketBase (Issue #8);
> die alte Datei-Schicht unter `Content/` gibt es nicht mehr. PocketBase ist damit
> die einzige Quelle und dieser Abschnitt der einzige Pflegeweg.
> **Sprachkonvention.** Feldnamen und Slugs sind **englisch** (`title`, `slug`,
> `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,
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`)
Eine Seite hat `title` (Überschrift für Menschen, Umlaute erlaubt), `slug` (die
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`,
`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
`location` (`header` oder `footer`) und `order` (Reihenfolge, ab 1). Die App
baut Kopf- und Fußnavigation daraus; nichts wird im Markup angefasst.