87 lines
2.9 KiB
C#
87 lines
2.9 KiB
C#
using System.Globalization;
|
|
using Markdig;
|
|
|
|
namespace Elternbeirat.Web.Services;
|
|
|
|
/// <summary>
|
|
/// Reads the news posts from <c>Content/posts/*.md</c>, 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.
|
|
/// </summary>
|
|
public sealed class PostService
|
|
{
|
|
private readonly IReadOnlyList<Post> _posts;
|
|
private readonly IReadOnlyDictionary<string, Post> _postsBySlug;
|
|
|
|
/// <summary>
|
|
/// Reads every Markdown post from the <c>Content/posts</c> directory
|
|
/// below the application root and caches it.
|
|
/// </summary>
|
|
public PostService(IWebHostEnvironment environment)
|
|
{
|
|
var pipeline = new MarkdownPipelineBuilder()
|
|
.UseYamlFrontMatter()
|
|
.Build();
|
|
|
|
var directory = Path.Combine(environment.ContentRootPath, "Content", "posts");
|
|
var posts = new List<Post>();
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// All posts, newest first. Used for the overview list.
|
|
/// </summary>
|
|
public IReadOnlyList<Post> All => _posts;
|
|
|
|
/// <summary>
|
|
/// Returns the post for the given slug, or <c>null</c> if there is none.
|
|
/// The lookup is case-insensitive.
|
|
/// </summary>
|
|
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 };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the <c>datum:</c> value as an ISO date (yyyy-MM-dd). Falls back to
|
|
/// <see cref="DateOnly.MinValue"/> when the value is missing or malformed,
|
|
/// so a single bad file does not crash startup — it just sorts last.
|
|
/// </summary>
|
|
private static DateOnly ParseDate(string? value)
|
|
{
|
|
return DateOnly.TryParse(value, CultureInfo.InvariantCulture,
|
|
DateTimeStyles.None, out var date)
|
|
? date
|
|
: DateOnly.MinValue;
|
|
}
|
|
}
|