using System.Globalization;
using Markdig;
namespace Elternbeirat.Web.Services;
///
/// Reads the news posts from Content/posts/*.md, renders them to HTML
/// once at startup and keeps them in memory, sorted newest first. Registered as
/// a singleton because the content lives in the image and does not change at
/// runtime.
///
public sealed class PostService
{
private readonly IReadOnlyList _posts;
private readonly IReadOnlyDictionary _postsBySlug;
///
/// Reads every Markdown post from the Content/posts directory
/// below the application root and caches it.
///
public PostService(IWebHostEnvironment environment)
{
var pipeline = new MarkdownPipelineBuilder()
.UseYamlFrontMatter()
.Build();
var directory = Path.Combine(environment.ContentRootPath, "Content", "posts");
var posts = new List();
if (Directory.Exists(directory))
{
foreach (var path in Directory.EnumerateFiles(directory, "*.md"))
{
posts.Add(Read(path, pipeline));
}
}
// Newest first for the list; a stable slug order breaks date ties.
posts.Sort((a, b) =>
{
var byDate = b.Date.CompareTo(a.Date);
return byDate != 0 ? byDate : string.CompareOrdinal(a.Slug, b.Slug);
});
_posts = posts;
_postsBySlug = posts.ToDictionary(p => p.Slug, StringComparer.OrdinalIgnoreCase);
}
///
/// All posts, newest first. Used for the overview list.
///
public IReadOnlyList All => _posts;
///
/// Returns the post for the given slug, or null if there is none.
/// The lookup is case-insensitive.
///
public Post? Find(string slug) =>
_postsBySlug.GetValueOrDefault(slug);
private static Post 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 date = ParseDate(FrontMatter.Read("date", document, source));
var html = Markdown.ToHtml(source, pipeline);
return new Post { Slug = slug, Title = title, Date = date, ContentHtml = html };
}
///
/// Parses the datum: value as an ISO date (yyyy-MM-dd). Falls back to
/// when the value is missing or malformed,
/// so a single bad file does not crash startup — it just sorts last.
///
private static DateOnly ParseDate(string? value)
{
return DateOnly.TryParse(value, CultureInfo.InvariantCulture,
DateTimeStyles.None, out var date)
? date
: DateOnly.MinValue;
}
}