using Markdig; using Markdig.Extensions.Yaml; using Markdig.Syntax; namespace Elternbeirat.Web.Services; /// /// Reads the static content pages from Content/pages/*.md, 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. /// public sealed class PageService { private readonly IReadOnlyDictionary _pagesBySlug; /// /// Reads every Markdown page from the Content/pages directory below /// the application root and caches it. /// public PageService(IWebHostEnvironment environment) { var pipeline = new MarkdownPipelineBuilder() .UseYamlFrontMatter() .Build(); var directory = Path.Combine(environment.ContentRootPath, "Content", "pages"); var pages = new Dictionary(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; } /// /// Returns the page for the given slug, or null if there is none. /// The lookup is case-insensitive. /// 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 = ReadTitle(document, source) ?? slug; var html = Markdown.ToHtml(source, pipeline); return new Page { Slug = slug, Title = title, ContentHtml = html }; } /// /// Pulls the titel: value out of the YAML front matter. Kept /// deliberately simple (a single key: value line) — YamlDotNet will /// take over once we need structured metadata. The key stays German because /// the Markdown files are edited by German-speaking editors. /// private static string? ReadTitle(MarkdownDocument document, string source) { var block = document.Descendants().FirstOrDefault(); if (block is null) { return null; } var frontMatter = source.Substring(block.Span.Start, block.Span.Length); foreach (var line in frontMatter.Split('\n')) { var trimmed = line.Trim(); if (trimmed.StartsWith("titel:", StringComparison.OrdinalIgnoreCase)) { return trimmed["titel:".Length..].Trim().Trim('"'); } } return null; } }