Serve pages and home from PocketBase, drop the file-based content layer

This commit is contained in:
tleininger committed 2026-09-23 13:30:48 +02:00
1 parent da87716d97
commit 4136af6417
15 files changed
+117 -285

No files matched your search

@@ -1,5 +1,4 @@
@page "/{Slug}"
@inject PageService PageService
@if (_page is null)
{
@@ -9,6 +8,6 @@ else
{
<PageTitle>@_page.Title</PageTitle>
<article>
@((MarkupString)_page.ContentHtml)
@Body
</article>
}
@@ -1,4 +1,6 @@
using Elternbeirat.Web.Services;
using Elternbeirat.Contracts;
using Elternbeirat.PocketBase;
using Elternbeirat.Web.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Http;
@@ -6,23 +8,49 @@ namespace Elternbeirat.Web.Features.Pages;
public partial class ContentPage
{
[LoggerMessage(Level = LogLevel.Warning, Message = "Could not load page '{slug}' from PocketBase.")]
private static partial void LogLoadFailed(ILogger logger, string slug, Exception exception);
[Parameter]
public string Slug { get; set; } = "";
[Inject]
private PocketBaseClient PocketBase { get; set; } = default!;
[Inject]
private ILogger<ContentPage> Logger { get; set; } = default!;
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private Page? _page;
protected override void OnParametersSet()
{
_page = PageService.Find(Slug);
// The page body rendered from Markdown to HTML, ready to emit as a
// MarkupString in the markup.
private MarkupString Body => (MarkupString)Markdown.ToHtml(_page?.Body);
// Unknown slug -> 404, so UseStatusCodePagesWithReExecute serves the
// /not-found page instead of an empty 200 response.
protected override async Task OnParametersSetAsync()
{
try
{
var pages = await PocketBase.GetPagesAsync();
_page = pages.FirstOrDefault(
p => string.Equals(p.Slug, Slug, StringComparison.OrdinalIgnoreCase));
}
catch (HttpRequestException ex)
{
// Source unreachable -> treat as "not found" rather than a 500. The
// 404 branch below serves the friendly /not-found page.
LogLoadFailed(Logger, Slug, ex);
_page = null;
}
// Unknown slug (or unreachable source) -> 404, so
// UseStatusCodePagesWithReExecute serves the /not-found page instead of an
// empty 200 response.
if (_page is null && HttpContext is not null)
{
HttpContext.Response.StatusCode = StatusCodes.Status404NotFound;
}
}
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
}
-25
View File
@@ -1,25 +0,0 @@
namespace Elternbeirat.Web.Services;
/// <summary>
/// A static content page, read from a Markdown file in <c>Content/pages/</c>.
/// The file name (without extension) is the slug.
/// </summary>
public sealed class Page
{
/// <summary>
/// URL identifier of the page, taken from the file name without umlauts
/// (e.g. <c>vorstandsteam</c>). It appears in the route <c>/{slug}</c>.
/// </summary>
public required string Slug { get; init; }
/// <summary>
/// Display title from the YAML front matter (<c>titel:</c>). Falls back to
/// the slug when no title is set.
/// </summary>
public required string Title { get; init; }
/// <summary>
/// The page body rendered from Markdown to HTML (front matter excluded).
/// </summary>
public required string ContentHtml { get; init; }
}
@@ -1,57 +0,0 @@
using Markdig;
namespace Elternbeirat.Web.Services;
/// <summary>
/// Reads the static content pages from <c>Content/pages/*.md</c>, renders them
/// to HTML once at startup and keeps them in memory. Registered as a singleton
/// because the content lives in the image and does not change at runtime.
/// </summary>
public sealed class PageService
{
private readonly IReadOnlyDictionary<string, Page> _pagesBySlug;
/// <summary>
/// Reads every Markdown page from the <c>Content/pages</c> directory below
/// the application root and caches it.
/// </summary>
public PageService(IWebHostEnvironment environment)
{
var pipeline = new MarkdownPipelineBuilder()
.UseYamlFrontMatter()
.Build();
var directory = Path.Combine(environment.ContentRootPath, "Content", "pages");
var pages = new Dictionary<string, Page>(StringComparer.OrdinalIgnoreCase);
if (Directory.Exists(directory))
{
foreach (var path in Directory.EnumerateFiles(directory, "*.md"))
{
var page = Read(path, pipeline);
pages[page.Slug] = page;
}
}
_pagesBySlug = pages;
}
/// <summary>
/// Returns the page for the given slug, or <c>null</c> if there is none.
/// The lookup is case-insensitive.
/// </summary>
public Page? Find(string slug) =>
_pagesBySlug.GetValueOrDefault(slug);
private static Page Read(string path, MarkdownPipeline pipeline)
{
var slug = Path.GetFileNameWithoutExtension(path);
var source = File.ReadAllText(path);
var document = Markdown.Parse(source, pipeline);
var title = FrontMatter.Read("title", document, source) ?? slug;
var html = Markdown.ToHtml(source, pipeline);
return new Page { Slug = slug, Title = title, ContentHtml = html };
}
}