Fix/content page heading #36

Merged
Tom merged 13 commits from fix/content-page-heading into main 2026-09-30 23:18:44 +02:00
7 changed files with 87 additions and 7 deletions
Showing only changes of commit 4fa17429b5 - Show all commits

No files matched your search

@@ -255,6 +255,22 @@ public sealed class PocketBaseFixture : IAsyncLifetime
title = "Downloads", body = "# Downloads", location = "header", title = "Downloads", body = "# Downloads", location = "header",
order = 5, slug = "downloads", embed = Array.Empty<string>(), @public = true, order = 5, slug = "downloads", embed = Array.Empty<string>(), @public = true,
}); });
// Page records for the posts and events lists. Routes /posts and /events are
// served by PostList and EventList, which read these records for their heading
// and intro (title + body) and render the list themselves, just like FaqList.
// The body carries no "# heading": the title already renders as the h1.
await CreateRecordAsync(http, "pages", new
{
title = "Beiträge", body = "Neuigkeiten aus dem Elternbeirat.",
location = "header",
order = 6, slug = "posts", embed = Array.Empty<string>(), @public = true,
});
await CreateRecordAsync(http, "pages", new
{
title = "Termine", body = "Sitzungen und Veranstaltungen auf einen Blick.",
location = "header",
order = 7, slug = "events", embed = Array.Empty<string>(), @public = true,
});
// A plain content page reached only by its slug (not shown in the header), // A plain content page reached only by its slug (not shown in the header),
// so the catch-all "/{slug}" ContentPage route stays covered. It carries a // so the catch-all "/{slug}" ContentPage route stays covered. It carries a
// valid location because the field is required; footer keeps it low-key. // valid location because the field is required; footer keeps it low-key.
+18
View File
@@ -140,6 +140,24 @@ public sealed class RouteSmokeTests : IDisposable
html.ShouldContain("Förderverein"); // the page body, rendered from Markdown html.ShouldContain("Förderverein"); // the page body, rendered from Markdown
} }
[Fact]
public async Task Posts_and_events_render_their_page_heading_and_body()
{
// /posts and /events are served by PostList and EventList, which -- like
// FaqList -- read their own "posts"/"events" page record for heading and
// intro instead of hard-coding them. Assert both the title (as the h1) and
// the body text from the fixture records reach the markup.
var client = _factory.CreateClient();
var posts = await client.GetStringAsync(new Uri("/posts", UriKind.Relative));
posts.ShouldContain("Beiträge"); // heading from the page title
posts.ShouldContain("Neuigkeiten aus dem Elternbeirat."); // the page body
var events = await client.GetStringAsync(new Uri("/events", UriKind.Relative));
events.ShouldContain("Termine"); // heading from the page title
events.ShouldContain("Sitzungen und Veranstaltungen auf einen Blick."); // the page body
}
[Fact] [Fact]
public async Task Health_returns_200_when_PocketBase_is_reachable() public async Task Health_returns_200_when_PocketBase_is_reachable()
{ {
@@ -1,8 +1,19 @@
@page "/events" @page "/events"
<PageTitle>Termine</PageTitle> @{
// Heading and intro come from the "events" page record; fall back to a sensible
// default only if that record is missing, so the page still renders.
var title = _page?.Title ?? "Termine";
}
<h1>Termine</h1> <PageTitle>@title</PageTitle>
<h1>@title</h1>
@if (_page is not null && !string.IsNullOrWhiteSpace(_page.Body))
{
<article>@Body</article>
}
@if (_unavailable) @if (_unavailable)
{ {
@@ -23,6 +23,11 @@ public partial class EventList
[CascadingParameter] [CascadingParameter]
private HttpContext? HttpContext { get; set; } private HttpContext? HttpContext { get; set; }
// The "events" page record supplies the heading and intro text, so an editor
// owns them in PocketBase like every other page, instead of them being
// hard-coded here. Null until loaded, or when no such page exists.
private Page? _page;
private IReadOnlyList<Event> _upcomingEvents = []; private IReadOnlyList<Event> _upcomingEvents = [];
private IReadOnlyList<Event> _pastEvents = []; private IReadOnlyList<Event> _pastEvents = [];
@@ -31,12 +36,19 @@ public partial class EventList
// 503 this sets on the response. // 503 this sets on the response.
private bool _unavailable; private bool _unavailable;
// The page body rendered from Markdown to HTML, ready to emit as a MarkupString.
private MarkupString Body => (MarkupString)Markdown.ToHtml(_page?.Body);
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
IReadOnlyList<Event> events; IReadOnlyList<Event> events;
try try
{ {
events = await PocketBase.GetEventsAsync(HttpContext?.RequestAborted ?? CancellationToken.None); var token = HttpContext?.RequestAborted ?? CancellationToken.None;
var pages = await PocketBase.GetPagesAsync(token);
_page = pages.FirstOrDefault(
page => string.Equals(page.Slug, "events", StringComparison.OrdinalIgnoreCase));
events = await PocketBase.GetEventsAsync(token);
} }
catch (PocketBaseUnavailableException exception) catch (PocketBaseUnavailableException exception)
{ {
+13 -2
View File
@@ -1,8 +1,19 @@
@page "/posts" @page "/posts"
<PageTitle>Beiträge</PageTitle> @{
// Heading and intro come from the "posts" page record; fall back to a sensible
// default only if that record is missing, so the page still renders.
var title = _page?.Title ?? "Beiträge";
}
<h1>Beiträge</h1> <PageTitle>@title</PageTitle>
<h1>@title</h1>
@if (_page is not null && !string.IsNullOrWhiteSpace(_page.Body))
{
<article>@Body</article>
}
@if (_unavailable) @if (_unavailable)
{ {
@@ -23,6 +23,11 @@ public partial class PostList
[CascadingParameter] [CascadingParameter]
private HttpContext? HttpContext { get; set; } private HttpContext? HttpContext { get; set; }
// The "posts" page record supplies the heading and intro text, so an editor owns
// them in PocketBase like every other page, instead of them being hard-coded here.
// Null until loaded, or when no such page exists.
private Page? _page;
private IReadOnlyList<Post> _posts = []; private IReadOnlyList<Post> _posts = [];
// Set when the content source could not be reached or read. The markup then // Set when the content source could not be reached or read. The markup then
@@ -30,11 +35,18 @@ public partial class PostList
// 503 this sets on the response. // 503 this sets on the response.
private bool _unavailable; private bool _unavailable;
// The page body rendered from Markdown to HTML, ready to emit as a MarkupString.
private MarkupString Body => (MarkupString)Markdown.ToHtml(_page?.Body);
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
_posts = await PocketBase.GetPostsAsync(HttpContext?.RequestAborted ?? CancellationToken.None); var token = HttpContext?.RequestAborted ?? CancellationToken.None;
var pages = await PocketBase.GetPagesAsync(token);
_page = pages.FirstOrDefault(
page => string.Equals(page.Slug, "posts", StringComparison.OrdinalIgnoreCase));
_posts = await PocketBase.GetPostsAsync(token);
} }
catch (PocketBaseUnavailableException exception) catch (PocketBaseUnavailableException exception)
{ {
+1 -1
View File
@@ -21,7 +21,7 @@ migrate((app) => {
const schema = JSON.parse(toString($os.readFile("/pb_migrations/collections_schema.json"))) const schema = JSON.parse(toString($os.readFile("/pb_migrations/collections_schema.json")))
const records = { const records = {
pages: [{"body":"# Vorstandsteam\n\nDer Elternbeirat der IGMH vertritt die Interessen aller Eltern gegenüber der Schulleitung und dem Kollegium. Wir treffen uns regelmäßig, bringen Anliegen aus der Elternschaft ein und begleiten das Schulleben aktiv.\n\n## Vorstand\n\n- **Vorsitz** — Anke Berger\n- **Stellvertretung** — Murat Yılmaz\n- **Kasse** — Sabine Krause\n- **Schriftführung** — Thomas Heinrich\n\n## So erreichen Sie uns\n\nAm schnellsten per Mail über unsere [Kontaktseite](/contact). Zu unseren öffentlichen Sitzungen sind alle Eltern herzlich eingeladen — die Termine finden Sie unter [Termine](/events).","embed":[],"location":"header","order":2,"public":true,"slug":"board","title":"Vorstandsteam"},{"body":"# Förderverein\n\nDer Förderverein der IGMH unterstützt Projekte, für die im regulären Schulhaushalt kein Geld vorgesehen ist: von der Ausstattung der Bibliothek über Pausenspielgeräte bis zu Zuschüssen für Klassenfahrten, damit kein Kind aus finanziellen Gründen zurückbleibt.\n\n## Mitglied werden\n\nDer Jahresbeitrag beträgt 24 € — jeder Euro fließt direkt in die Arbeit an der Schule. Den Aufnahmeantrag finden Sie unter [Downloads](/downloads).\n\n## Spenden\n\nSpenden sind jederzeit willkommen und werden auf Wunsch mit einer Zuwendungsbestätigung quittiert. Sprechen Sie uns über die [Kontaktseite](/contact) an.","embed":[],"location":"header","order":3,"public":true,"slug":"patrons","title":"Förderverein"},{"body":"Auf dieser Seite haben wir die Fragen zusammengetragen, die uns am häufigsten erreichen.\n\nIhre Frage ist nicht dabei? Schreiben Sie uns über die [Kontaktseite](/contact) – wir helfen gern weiter.","embed":[],"location":"header","order":6,"public":true,"slug":"faqs","title":"FAQs"},{"body":"# Downloads\n\nFormulare und Dokumente zum Herunterladen:\n\n- Aufnahmeantrag Förderverein\n- Einzugsermächtigung\n- Erstattungsformular Klassenkasse\n\nFehlt ein Dokument? Melden Sie sich über die [Kontaktseite](/contact).","embed":[],"location":"header","order":7,"public":true,"slug":"downloads","title":"Downloads"},{"body":"# Kontakt\n\nSie erreichen den Elternbeirat der IGMH per E-Mail:\n\n**kontakt@elternbeirat-igmh.info**\n\nWir antworten in der Regel innerhalb weniger Tage. Bei dringenden schulischen Angelegenheiten wenden Sie sich bitte direkt an das Sekretariat der Schule.","embed":[],"location":"footer","order":1,"public":true,"slug":"contact","title":"Kontakt"},{"body":"# Impressum\n\nAngaben gemäß § 5 DDG\n\n**Elternbeirat der IGMH**\nVertreten durch den Vorstand\n\nKontakt: siehe [Kontaktseite](/contact)\n\nVerantwortlich für den Inhalt nach § 18 Abs. 2 MStV: der Vorstand des Elternbeirats.","embed":[],"location":"footer","order":2,"public":true,"slug":"imprint","title":"Impressum"},{"body":"# Datenschutz\n\nDer Schutz Ihrer Daten ist uns wichtig. Diese Website kommt ohne externe Dienste aus: keine Analysewerkzeuge, keine eingebundenen Karten oder Videos, keine Schriften von fremden Servern.\n\n## Server-Logs\n\nBeim Aufruf werden technisch notwendige Zugriffsdaten (IP-Adresse, Zeitpunkt, aufgerufene Seite) kurzzeitig verarbeitet, um den Betrieb sicherzustellen. Eine Zusammenführung mit anderen Daten findet nicht statt.\n\n## Ihre Rechte\n\nFragen zum Datenschutz richten Sie bitte an die [Kontaktseite](/contact).","embed":[],"location":"footer","order":3,"public":true,"slug":"privacy","title":"Datenschutz"},{"body":"# Willkommen beim Elternbeirat der IGMH","embed":["posts","events"],"location":"header","order":1,"public":true,"slug":"home","title":"Start"},{"body":"# Beiträge","embed":[],"location":"header","order":4,"public":true,"slug":"posts","title":"Beiträge"},{"body":"# Termine","embed":[],"location":"header","order":5,"public":true,"slug":"events","title":"Termine"}] pages: [{"body":"# Vorstandsteam\n\nDer Elternbeirat der IGMH vertritt die Interessen aller Eltern gegenüber der Schulleitung und dem Kollegium. Wir treffen uns regelmäßig, bringen Anliegen aus der Elternschaft ein und begleiten das Schulleben aktiv.\n\n## Vorstand\n\n- **Vorsitz** — Anke Berger\n- **Stellvertretung** — Murat Yılmaz\n- **Kasse** — Sabine Krause\n- **Schriftführung** — Thomas Heinrich\n\n## So erreichen Sie uns\n\nAm schnellsten per Mail über unsere [Kontaktseite](/contact). Zu unseren öffentlichen Sitzungen sind alle Eltern herzlich eingeladen — die Termine finden Sie unter [Termine](/events).","embed":[],"location":"header","order":2,"public":true,"slug":"board","title":"Vorstandsteam"},{"body":"# Förderverein\n\nDer Förderverein der IGMH unterstützt Projekte, für die im regulären Schulhaushalt kein Geld vorgesehen ist: von der Ausstattung der Bibliothek über Pausenspielgeräte bis zu Zuschüssen für Klassenfahrten, damit kein Kind aus finanziellen Gründen zurückbleibt.\n\n## Mitglied werden\n\nDer Jahresbeitrag beträgt 24 € — jeder Euro fließt direkt in die Arbeit an der Schule. Den Aufnahmeantrag finden Sie unter [Downloads](/downloads).\n\n## Spenden\n\nSpenden sind jederzeit willkommen und werden auf Wunsch mit einer Zuwendungsbestätigung quittiert. Sprechen Sie uns über die [Kontaktseite](/contact) an.","embed":[],"location":"header","order":3,"public":true,"slug":"patrons","title":"Förderverein"},{"body":"Auf dieser Seite haben wir die Fragen zusammengetragen, die uns am häufigsten erreichen.\n\nIhre Frage ist nicht dabei? Schreiben Sie uns über die [Kontaktseite](/contact) – wir helfen gern weiter.","embed":[],"location":"header","order":6,"public":true,"slug":"faqs","title":"FAQs"},{"body":"# Downloads\n\nFormulare und Dokumente zum Herunterladen:\n\n- Aufnahmeantrag Förderverein\n- Einzugsermächtigung\n- Erstattungsformular Klassenkasse\n\nFehlt ein Dokument? Melden Sie sich über die [Kontaktseite](/contact).","embed":[],"location":"header","order":7,"public":true,"slug":"downloads","title":"Downloads"},{"body":"# Kontakt\n\nSie erreichen den Elternbeirat der IGMH per E-Mail:\n\n**kontakt@elternbeirat-igmh.info**\n\nWir antworten in der Regel innerhalb weniger Tage. Bei dringenden schulischen Angelegenheiten wenden Sie sich bitte direkt an das Sekretariat der Schule.","embed":[],"location":"footer","order":1,"public":true,"slug":"contact","title":"Kontakt"},{"body":"# Impressum\n\nAngaben gemäß § 5 DDG\n\n**Elternbeirat der IGMH**\nVertreten durch den Vorstand\n\nKontakt: siehe [Kontaktseite](/contact)\n\nVerantwortlich für den Inhalt nach § 18 Abs. 2 MStV: der Vorstand des Elternbeirats.","embed":[],"location":"footer","order":2,"public":true,"slug":"imprint","title":"Impressum"},{"body":"# Datenschutz\n\nDer Schutz Ihrer Daten ist uns wichtig. Diese Website kommt ohne externe Dienste aus: keine Analysewerkzeuge, keine eingebundenen Karten oder Videos, keine Schriften von fremden Servern.\n\n## Server-Logs\n\nBeim Aufruf werden technisch notwendige Zugriffsdaten (IP-Adresse, Zeitpunkt, aufgerufene Seite) kurzzeitig verarbeitet, um den Betrieb sicherzustellen. Eine Zusammenführung mit anderen Daten findet nicht statt.\n\n## Ihre Rechte\n\nFragen zum Datenschutz richten Sie bitte an die [Kontaktseite](/contact).","embed":[],"location":"footer","order":3,"public":true,"slug":"privacy","title":"Datenschutz"},{"body":"# Willkommen beim Elternbeirat der IGMH","embed":["posts","events"],"location":"header","order":1,"public":true,"slug":"home","title":"Start"},{"body":"Neuigkeiten aus dem Elternbeirat: Berichte aus den Sitzungen, Ankündigungen und Wissenswertes rund um die Elternarbeit an der IGMH. Die neuesten Beiträge stehen oben.","embed":[],"location":"header","order":4,"public":true,"slug":"posts","title":"Beiträge"},{"body":"Sitzungen des Elternbeirats, Schulveranstaltungen und weitere Termine, die für Eltern wichtig sind.","embed":[],"location":"header","order":5,"public":true,"slug":"events","title":"Termine"}]
, ,
posts: [{"body":"## Neuer Vorstand gewählt\n\nAuf der Mitgliederversammlung wurde der Vorstand des Elternbeirats für das Schuljahr **2026/27** neu gewählt. Wir danken dem bisherigen Team für sein Engagement und freuen uns auf die gemeinsame Arbeit.\n\nDie neuen Ansprechpartnerinnen und Ansprechpartner finden Sie auf der Seite [Vorstandsteam](/board).","date":"2026-09-24 18:00:00.000Z","public":true,"slug":"new-board-2026","title":"Neuer Vorstand für das Schuljahr 2026/27 gewählt"},{"body":"## Herbstbasar am 22. November\n\nAm **22. November** findet wieder unser beliebter Herbstbasar statt. Es gibt selbst gebackene Kuchen, Bastelstände und ein Bühnenprogramm der Klassen.\n\nWer einen Stand betreuen oder etwas beisteuern möchte, meldet sich bitte bis zum 10. November über das [Kontaktformular](/contact) an. Wir freuen uns über jede helfende Hand!","date":"2026-09-18 09:30:00.000Z","public":true,"slug":"herbstbasar-2026","title":"Herbstbasar am 22. November – jetzt anmelden"},{"body":"## Mehr Auswahl beim Mittagessen\n\nAb den Herbstferien erweitert der Caterer das Angebot in der Mensa: Es gibt künftig **täglich ein zusätzliches vegetarisches Gericht** sowie eine größere Salatauswahl.\n\nRückmeldungen zum Essen nehmen wir gerne über den Elternbeirat auf – sprechen Sie uns an.","date":"2026-09-10 12:15:00.000Z","public":true,"slug":"mensa-mehr-auswahl","title":"Mehr Auswahl beim Mittagessen ab den Herbstferien"},{"body":"## Besuchen Sie unseren Stand\n\nAm kommenden **Elternsprechtag** ist der Elternbeirat mit einem Info-Stand im Foyer vertreten. Kommen Sie vorbei – wir beantworten Fragen rund um die Elternarbeit und freuen uns über neue Mitstreiterinnen und Mitstreiter.\n\nAlle Termine finden Sie unter [Termine](/events).","date":"2026-09-02 08:00:00.000Z","public":true,"slug":"elternsprechtag-stand","title":"Elternbeirat am Elternsprechtag – besuchen Sie unseren Stand"},{"body":"## Neue Spielgeräte für den Pausenhof\n\nDank der Einnahmen aus dem letzten Basar und der Unterstützung des [Fördervereins](/patrons) konnten neue Spielgeräte für den Pausenhof angeschafft werden.\n\nDie Kinder haben die Klettergerüste bereits begeistert in Beschlag genommen.","date":"2026-08-28 16:45:00.000Z","public":true,"slug":"neue-spielgeraete","title":"Neue Spielgeräte für den Pausenhof"}] posts: [{"body":"## Neuer Vorstand gewählt\n\nAuf der Mitgliederversammlung wurde der Vorstand des Elternbeirats für das Schuljahr **2026/27** neu gewählt. Wir danken dem bisherigen Team für sein Engagement und freuen uns auf die gemeinsame Arbeit.\n\nDie neuen Ansprechpartnerinnen und Ansprechpartner finden Sie auf der Seite [Vorstandsteam](/board).","date":"2026-09-24 18:00:00.000Z","public":true,"slug":"new-board-2026","title":"Neuer Vorstand für das Schuljahr 2026/27 gewählt"},{"body":"## Herbstbasar am 22. November\n\nAm **22. November** findet wieder unser beliebter Herbstbasar statt. Es gibt selbst gebackene Kuchen, Bastelstände und ein Bühnenprogramm der Klassen.\n\nWer einen Stand betreuen oder etwas beisteuern möchte, meldet sich bitte bis zum 10. November über das [Kontaktformular](/contact) an. Wir freuen uns über jede helfende Hand!","date":"2026-09-18 09:30:00.000Z","public":true,"slug":"herbstbasar-2026","title":"Herbstbasar am 22. November – jetzt anmelden"},{"body":"## Mehr Auswahl beim Mittagessen\n\nAb den Herbstferien erweitert der Caterer das Angebot in der Mensa: Es gibt künftig **täglich ein zusätzliches vegetarisches Gericht** sowie eine größere Salatauswahl.\n\nRückmeldungen zum Essen nehmen wir gerne über den Elternbeirat auf – sprechen Sie uns an.","date":"2026-09-10 12:15:00.000Z","public":true,"slug":"mensa-mehr-auswahl","title":"Mehr Auswahl beim Mittagessen ab den Herbstferien"},{"body":"## Besuchen Sie unseren Stand\n\nAm kommenden **Elternsprechtag** ist der Elternbeirat mit einem Info-Stand im Foyer vertreten. Kommen Sie vorbei – wir beantworten Fragen rund um die Elternarbeit und freuen uns über neue Mitstreiterinnen und Mitstreiter.\n\nAlle Termine finden Sie unter [Termine](/events).","date":"2026-09-02 08:00:00.000Z","public":true,"slug":"elternsprechtag-stand","title":"Elternbeirat am Elternsprechtag – besuchen Sie unseren Stand"},{"body":"## Neue Spielgeräte für den Pausenhof\n\nDank der Einnahmen aus dem letzten Basar und der Unterstützung des [Fördervereins](/patrons) konnten neue Spielgeräte für den Pausenhof angeschafft werden.\n\nDie Kinder haben die Klettergerüste bereits begeistert in Beschlag genommen.","date":"2026-08-28 16:45:00.000Z","public":true,"slug":"neue-spielgeraete","title":"Neue Spielgeräte für den Pausenhof"}]
, ,