Add Markdown content pipeline for static pages
This commit is contained in:
1 parent
b59a96b90c
commit
76f1ad7825
7 files changed
+189
-1
No files matched your search
+5
-1
@@ -3,4 +3,8 @@ obj/
|
|||||||
/packages/
|
/packages/
|
||||||
riderModule.iml
|
riderModule.iml
|
||||||
/_ReSharper.Caches/
|
/_ReSharper.Caches/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Altbestand der WordPress-Seite (Sichtung/Migration, kann DB-Dumps mit
|
||||||
|
# personenbezogenen Daten und grosse Binaerdateien enthalten) -- nie ins Repo.
|
||||||
|
/backup/
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
@page "/{Slug}"
|
||||||
|
@using Elternbeirat.Web.Services
|
||||||
|
@inject SeitenService SeitenService
|
||||||
|
|
||||||
|
@if (_seite is null)
|
||||||
|
{
|
||||||
|
<PageTitle>Nicht gefunden</PageTitle>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<PageTitle>@_seite.Titel</PageTitle>
|
||||||
|
<article>
|
||||||
|
@((MarkupString)_seite.InhaltHtml)
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
|
||||||
|
@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; }
|
||||||
|
}
|
||||||
@@ -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.*
|
||||||
@@ -7,4 +7,14 @@
|
|||||||
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Markdig" Version="0.38.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Inhalte liegen als Dateien im Image (plan.md AE-3), nicht in einem
|
||||||
|
Volume. Ins Ausgabeverzeichnis kopieren, damit der Container sie findet. -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Content\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Elternbeirat.Web.Components;
|
using Elternbeirat.Web.Components;
|
||||||
|
using Elternbeirat.Web.Services;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -6,6 +7,9 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddRazorComponents();
|
builder.Services.AddRazorComponents();
|
||||||
|
|
||||||
|
// Inhalte werden beim Start einmalig eingelesen und gecacht -> Singleton.
|
||||||
|
builder.Services.AddSingleton<SeitenService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// NPM terminiert TLS und ist der einzige Weg zum Container (kein Port-Mapping
|
// NPM terminiert TLS und ist der einzige Weg zum Container (kein Port-Mapping
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace Elternbeirat.Web.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eine statische Inhaltsseite, eingelesen aus einer Markdown-Datei in
|
||||||
|
/// <c>Content/seiten/</c>. Der Dateiname (ohne Endung) ist der Slug.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Seite
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// URL-Kennung der Seite, abgeleitet aus dem Dateinamen ohne Umlaute
|
||||||
|
/// (z. B. <c>vorstandsteam</c>). Erscheint in der Route <c>/{slug}</c>.
|
||||||
|
/// </summary>
|
||||||
|
public required string Slug { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anzeigetitel aus dem YAML-Frontmatter (<c>titel:</c>). Fällt auf den
|
||||||
|
/// Slug zurück, wenn kein Titel gesetzt ist.
|
||||||
|
/// </summary>
|
||||||
|
public required string Titel { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Der aus Markdown gerenderte HTML-Rumpf der Seite (ohne Frontmatter).
|
||||||
|
/// </summary>
|
||||||
|
public required string InhaltHtml { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using Markdig;
|
||||||
|
using Markdig.Extensions.Yaml;
|
||||||
|
using Markdig.Syntax;
|
||||||
|
|
||||||
|
namespace Elternbeirat.Web.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Liest die statischen Inhaltsseiten aus <c>Content/seiten/*.md</c> 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).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SeitenService
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyDictionary<string, Seite> _seitenNachSlug;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Liest alle Markdown-Seiten aus dem Verzeichnis <c>Content/seiten</c>
|
||||||
|
/// unterhalb des Anwendungsstamms ein und cached sie.
|
||||||
|
/// </summary>
|
||||||
|
public SeitenService(IWebHostEnvironment umgebung)
|
||||||
|
{
|
||||||
|
var pipeline = new MarkdownPipelineBuilder()
|
||||||
|
.UseYamlFrontMatter()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var verzeichnis = Path.Combine(umgebung.ContentRootPath, "Content", "seiten");
|
||||||
|
var seiten = new Dictionary<string, Seite>(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Liefert die Seite zum angegebenen Slug oder <c>null</c>, wenn es keine
|
||||||
|
/// gibt. Groß-/Kleinschreibung wird ignoriert.
|
||||||
|
/// </summary>
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zieht den <c>titel:</c>-Wert aus dem YAML-Frontmatter. Bewusst simpel
|
||||||
|
/// gehalten (eine Zeile <c>schlüssel: wert</c>) — für strukturiertere
|
||||||
|
/// Metadaten kommt später YamlDotNet.
|
||||||
|
/// </summary>
|
||||||
|
private static string? LiesTitel(MarkdownDocument dokument, string quelle)
|
||||||
|
{
|
||||||
|
var block = dokument.Descendants<YamlFrontMatterBlock>().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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user