diff --git a/.gitignore b/.gitignore
index f400df3..fcf70a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,8 @@ obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
-.idea/
\ No newline at end of file
+.idea/
+
+# Altbestand der WordPress-Seite (Sichtung/Migration, kann DB-Dumps mit
+# personenbezogenen Daten und grosse Binaerdateien enthalten) -- nie ins Repo.
+/backup/
\ No newline at end of file
diff --git a/Elternbeirat.Web/Components/Pages/Inhaltsseite.razor b/Elternbeirat.Web/Components/Pages/Inhaltsseite.razor
new file mode 100644
index 0000000..e522ce2
--- /dev/null
+++ b/Elternbeirat.Web/Components/Pages/Inhaltsseite.razor
@@ -0,0 +1,41 @@
+@page "/{Slug}"
+@using Elternbeirat.Web.Services
+@inject SeitenService SeitenService
+
+@if (_seite is null)
+{
+ Nicht gefunden
+}
+else
+{
+ @_seite.Titel
+
+ @((MarkupString)_seite.InhaltHtml)
+
+}
+
+@code {
+ [Parameter]
+ public string Slug { get; set; } = "";
+
+ private Seite? _seite;
+
+ protected override void OnParametersSet()
+ {
+ _seite = SeitenService.Finde(Slug);
+
+ // Unbekannter Slug -> 404, damit UseStatusCodePagesWithReExecute die
+ // /not-found-Seite ausliefert (statt einer leeren 200er-Antwort).
+ if (_seite is null)
+ {
+ var kontext = HttpContext;
+ if (kontext is not null)
+ {
+ kontext.Response.StatusCode = StatusCodes.Status404NotFound;
+ }
+ }
+ }
+
+ [CascadingParameter]
+ private HttpContext? HttpContext { get; set; }
+}
diff --git a/Elternbeirat.Web/Content/seiten/vorstandsteam.md b/Elternbeirat.Web/Content/seiten/vorstandsteam.md
new file mode 100644
index 0000000..000d88b
--- /dev/null
+++ b/Elternbeirat.Web/Content/seiten/vorstandsteam.md
@@ -0,0 +1,18 @@
+---
+titel: 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.*
diff --git a/Elternbeirat.Web/Elternbeirat.Web.csproj b/Elternbeirat.Web/Elternbeirat.Web.csproj
index fca27e2..85fec05 100644
--- a/Elternbeirat.Web/Elternbeirat.Web.csproj
+++ b/Elternbeirat.Web/Elternbeirat.Web.csproj
@@ -7,4 +7,14 @@
true
+
+
+
+
+
+
+
+
+
diff --git a/Elternbeirat.Web/Program.cs b/Elternbeirat.Web/Program.cs
index 9bdcf2f..86815da 100644
--- a/Elternbeirat.Web/Program.cs
+++ b/Elternbeirat.Web/Program.cs
@@ -1,4 +1,5 @@
using Elternbeirat.Web.Components;
+using Elternbeirat.Web.Services;
using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args);
@@ -6,6 +7,9 @@ var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents();
+// Inhalte werden beim Start einmalig eingelesen und gecacht -> Singleton.
+builder.Services.AddSingleton();
+
var app = builder.Build();
// NPM terminiert TLS und ist der einzige Weg zum Container (kein Port-Mapping
diff --git a/Elternbeirat.Web/Services/Seite.cs b/Elternbeirat.Web/Services/Seite.cs
new file mode 100644
index 0000000..426ee4a
--- /dev/null
+++ b/Elternbeirat.Web/Services/Seite.cs
@@ -0,0 +1,25 @@
+namespace Elternbeirat.Web.Services;
+
+///
+/// Eine statische Inhaltsseite, eingelesen aus einer Markdown-Datei in
+/// Content/seiten/. Der Dateiname (ohne Endung) ist der Slug.
+///
+public sealed class Seite
+{
+ ///
+ /// URL-Kennung der Seite, abgeleitet aus dem Dateinamen ohne Umlaute
+ /// (z. B. vorstandsteam). Erscheint in der Route /{slug}.
+ ///
+ public required string Slug { get; init; }
+
+ ///
+ /// Anzeigetitel aus dem YAML-Frontmatter (titel:). Fällt auf den
+ /// Slug zurück, wenn kein Titel gesetzt ist.
+ ///
+ public required string Titel { get; init; }
+
+ ///
+ /// Der aus Markdown gerenderte HTML-Rumpf der Seite (ohne Frontmatter).
+ ///
+ public required string InhaltHtml { get; init; }
+}
diff --git a/Elternbeirat.Web/Services/SeitenService.cs b/Elternbeirat.Web/Services/SeitenService.cs
new file mode 100644
index 0000000..957225e
--- /dev/null
+++ b/Elternbeirat.Web/Services/SeitenService.cs
@@ -0,0 +1,86 @@
+using Markdig;
+using Markdig.Extensions.Yaml;
+using Markdig.Syntax;
+
+namespace Elternbeirat.Web.Services;
+
+///
+/// Liest die statischen Inhaltsseiten aus Content/seiten/*.md ein,
+/// rendert sie beim Start einmalig zu HTML und hält sie im Speicher.
+/// Als Singleton registriert — Inhalte liegen im Image und ändern sich zur
+/// Laufzeit nicht (plan.md AE-3).
+///
+public sealed class SeitenService
+{
+ private readonly IReadOnlyDictionary _seitenNachSlug;
+
+ ///
+ /// Liest alle Markdown-Seiten aus dem Verzeichnis Content/seiten
+ /// unterhalb des Anwendungsstamms ein und cached sie.
+ ///
+ public SeitenService(IWebHostEnvironment umgebung)
+ {
+ var pipeline = new MarkdownPipelineBuilder()
+ .UseYamlFrontMatter()
+ .Build();
+
+ var verzeichnis = Path.Combine(umgebung.ContentRootPath, "Content", "seiten");
+ var seiten = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ if (Directory.Exists(verzeichnis))
+ {
+ foreach (var pfad in Directory.EnumerateFiles(verzeichnis, "*.md"))
+ {
+ var seite = Lies(pfad, pipeline);
+ seiten[seite.Slug] = seite;
+ }
+ }
+
+ _seitenNachSlug = seiten;
+ }
+
+ ///
+ /// Liefert die Seite zum angegebenen Slug oder null, wenn es keine
+ /// gibt. Groß-/Kleinschreibung wird ignoriert.
+ ///
+ public Seite? Finde(string slug) =>
+ _seitenNachSlug.GetValueOrDefault(slug);
+
+ private static Seite Lies(string pfad, MarkdownPipeline pipeline)
+ {
+ var slug = Path.GetFileNameWithoutExtension(pfad);
+ var quelle = File.ReadAllText(pfad);
+
+ var dokument = Markdown.Parse(quelle, pipeline);
+ var titel = LiesTitel(dokument, quelle) ?? slug;
+ var html = Markdown.ToHtml(quelle, pipeline);
+
+ return new Seite { Slug = slug, Titel = titel, InhaltHtml = html };
+ }
+
+ ///
+ /// Zieht den titel:-Wert aus dem YAML-Frontmatter. Bewusst simpel
+ /// gehalten (eine Zeile schlüssel: wert) — für strukturiertere
+ /// Metadaten kommt später YamlDotNet.
+ ///
+ private static string? LiesTitel(MarkdownDocument dokument, string quelle)
+ {
+ var block = dokument.Descendants().FirstOrDefault();
+ if (block is null)
+ {
+ return null;
+ }
+
+ var frontmatter = quelle.Substring(block.Span.Start, block.Span.Length);
+ foreach (var zeile in frontmatter.Split('\n'))
+ {
+ var getrimmt = zeile.Trim();
+ if (getrimmt.StartsWith("titel:", StringComparison.OrdinalIgnoreCase))
+ {
+ return getrimmt["titel:".Length..].Trim().Trim('"');
+ }
+ }
+
+ return null;
+ }
+}