From 879ff6a2953f802b9bf59e3d2da188ea63a03003 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 27 Apr 2024 15:41:19 +0100 Subject: [PATCH] feat: add support for excerpts on blog posts / tutorial articles --- OliverBooth/Data/Blog/BlogPost.cs | 3 +++ .../Configuration/BlogPostConfiguration.cs | 1 + OliverBooth/Data/Blog/IBlogPost.cs | 6 +++++ .../TutorialArticleConfiguration.cs | 1 + OliverBooth/Data/Web/ITutorialArticle.cs | 6 +++++ OliverBooth/Data/Web/TutorialArticle.cs | 5 +++- OliverBooth/Services/BlogPostService.cs | 6 +++++ OliverBooth/Services/ITutorialService.cs | 11 +++++++++ OliverBooth/Services/TutorialService.cs | 24 +++++++++++++++++++ 9 files changed, 62 insertions(+), 1 deletion(-) diff --git a/OliverBooth/Data/Blog/BlogPost.cs b/OliverBooth/Data/Blog/BlogPost.cs index 05ece19..59067b5 100644 --- a/OliverBooth/Data/Blog/BlogPost.cs +++ b/OliverBooth/Data/Blog/BlogPost.cs @@ -16,6 +16,9 @@ internal sealed class BlogPost : IBlogPost /// public bool EnableComments { get; internal set; } + /// + public string? Excerpt { get; internal set; } + /// public Guid Id { get; private set; } = Guid.NewGuid(); diff --git a/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs b/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs index 607f9d3..1ecde2f 100644 --- a/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs +++ b/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs @@ -20,6 +20,7 @@ internal sealed class BlogPostConfiguration : IEntityTypeConfiguration builder.Property(e => e.Updated).IsRequired(false); builder.Property(e => e.Title).HasMaxLength(255).IsRequired(); builder.Property(e => e.Body).IsRequired(); + builder.Property(e => e.Excerpt).HasMaxLength(512).IsRequired(false); builder.Property(e => e.IsRedirect).IsRequired(); builder.Property(e => e.RedirectUrl).HasConversion().HasMaxLength(255).IsRequired(false); builder.Property(e => e.EnableComments).IsRequired(); diff --git a/OliverBooth/Data/Blog/IBlogPost.cs b/OliverBooth/Data/Blog/IBlogPost.cs index d6e4e95..bc1a2e5 100644 --- a/OliverBooth/Data/Blog/IBlogPost.cs +++ b/OliverBooth/Data/Blog/IBlogPost.cs @@ -25,6 +25,12 @@ public interface IBlogPost /// bool EnableComments { get; } + /// + /// Gets the excerpt of this post, if it has one. + /// + /// The excerpt, or if this post has no excerpt. + string? Excerpt { get; } + /// /// Gets the ID of the post. /// diff --git a/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs b/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs index 56924fd..7e859df 100644 --- a/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs +++ b/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs @@ -16,6 +16,7 @@ internal sealed class TutorialArticleConfiguration : IEntityTypeConfiguration e.Id).IsRequired(); builder.Property(e => e.Folder).IsRequired(); + builder.Property(e => e.Excerpt).HasMaxLength(512).IsRequired(false); builder.Property(e => e.Published).IsRequired(); builder.Property(e => e.Updated); builder.Property(e => e.Slug).IsRequired(); diff --git a/OliverBooth/Data/Web/ITutorialArticle.cs b/OliverBooth/Data/Web/ITutorialArticle.cs index c1be295..a25c4ef 100644 --- a/OliverBooth/Data/Web/ITutorialArticle.cs +++ b/OliverBooth/Data/Web/ITutorialArticle.cs @@ -11,6 +11,12 @@ public interface ITutorialArticle /// The body. string Body { get; } + /// + /// Gets the excerpt of this article, if it has one. + /// + /// The excerpt, or if this article has no excerpt. + string? Excerpt { get; } + /// /// Gets the ID of the folder this article is contained within. /// diff --git a/OliverBooth/Data/Web/TutorialArticle.cs b/OliverBooth/Data/Web/TutorialArticle.cs index 1938e75..91132ae 100644 --- a/OliverBooth/Data/Web/TutorialArticle.cs +++ b/OliverBooth/Data/Web/TutorialArticle.cs @@ -8,6 +8,9 @@ internal sealed class TutorialArticle : IEquatable, ITutorialAr /// public string Body { get; private set; } = string.Empty; + /// + public string? Excerpt { get; private set; } + /// public int Folder { get; private set; } @@ -15,7 +18,7 @@ internal sealed class TutorialArticle : IEquatable, ITutorialAr public int Id { get; private set; } /// - public int? NextPart { get; } + public int? NextPart { get; private set; } /// public Uri? PreviewImageUrl { get; private set; } diff --git a/OliverBooth/Services/BlogPostService.cs b/OliverBooth/Services/BlogPostService.cs index 9fcd8cf..049cd6e 100644 --- a/OliverBooth/Services/BlogPostService.cs +++ b/OliverBooth/Services/BlogPostService.cs @@ -90,6 +90,12 @@ internal sealed class BlogPostService : IBlogPostService /// public string RenderExcerpt(IBlogPost post, out bool wasTrimmed) { + if (!string.IsNullOrWhiteSpace(post.Excerpt)) + { + wasTrimmed = false; + return Markdig.Markdown.ToHtml(post.Excerpt, _markdownPipeline); + } + string body = post.Body; int moreIndex = body.IndexOf("", StringComparison.Ordinal); diff --git a/OliverBooth/Services/ITutorialService.cs b/OliverBooth/Services/ITutorialService.cs index ad85bbf..a5bd547 100644 --- a/OliverBooth/Services/ITutorialService.cs +++ b/OliverBooth/Services/ITutorialService.cs @@ -67,6 +67,17 @@ public interface ITutorialService /// The rendered HTML of the article. string RenderArticle(ITutorialArticle article); + /// + /// Renders the excerpt of the specified article. + /// + /// The article whose excerpt to render. + /// + /// When this method returns, contains if the excerpt was trimmed; otherwise, + /// . + /// + /// The rendered HTML of the article's excerpt. + string RenderExcerpt(ITutorialArticle article, out bool wasTrimmed); + /// /// Attempts to find an article by its ID. /// diff --git a/OliverBooth/Services/TutorialService.cs b/OliverBooth/Services/TutorialService.cs index e8d6c9d..c6e1f7d 100644 --- a/OliverBooth/Services/TutorialService.cs +++ b/OliverBooth/Services/TutorialService.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Cysharp.Text; +using Humanizer; using Markdig; using Microsoft.EntityFrameworkCore; using OliverBooth.Data; @@ -108,6 +109,29 @@ internal sealed class TutorialService : ITutorialService return Markdig.Markdown.ToHtml(article.Body, _markdownPipeline); } + /// + public string RenderExcerpt(ITutorialArticle article, out bool wasTrimmed) + { + if (!string.IsNullOrWhiteSpace(article.Excerpt)) + { + wasTrimmed = false; + return Markdig.Markdown.ToHtml(article.Excerpt, _markdownPipeline); + } + + string body = article.Body; + int moreIndex = body.IndexOf("", StringComparison.Ordinal); + + if (moreIndex == -1) + { + string excerpt = body.Truncate(255, "..."); + wasTrimmed = body.Length > 255; + return Markdig.Markdown.ToHtml(excerpt, _markdownPipeline); + } + + wasTrimmed = true; + return Markdig.Markdown.ToHtml(body[..moreIndex], _markdownPipeline); + } + /// public bool TryGetArticle(int id, [NotNullWhen(true)] out ITutorialArticle? article) {