feat: add support for excerpts on blog posts / tutorial articles

This commit is contained in:
Oliver Booth 2024-04-27 15:41:19 +01:00
parent cd6bbec1a5
commit 879ff6a295
Signed by: oliverbooth
GPG Key ID: E60B570D1B7557B5
9 changed files with 62 additions and 1 deletions

View File

@ -16,6 +16,9 @@ internal sealed class BlogPost : IBlogPost
/// <inheritdoc />
public bool EnableComments { get; internal set; }
/// <inheritdoc />
public string? Excerpt { get; internal set; }
/// <inheritdoc />
public Guid Id { get; private set; } = Guid.NewGuid();

View File

@ -20,6 +20,7 @@ internal sealed class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost>
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<UriToStringConverter>().HasMaxLength(255).IsRequired(false);
builder.Property(e => e.EnableComments).IsRequired();

View File

@ -25,6 +25,12 @@ public interface IBlogPost
/// </value>
bool EnableComments { get; }
/// <summary>
/// Gets the excerpt of this post, if it has one.
/// </summary>
/// <value>The excerpt, or <see langword="null" /> if this post has no excerpt.</value>
string? Excerpt { get; }
/// <summary>
/// Gets the ID of the post.
/// </summary>

View File

@ -16,6 +16,7 @@ internal sealed class TutorialArticleConfiguration : IEntityTypeConfiguration<Tu
builder.Property(e => 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();

View File

@ -11,6 +11,12 @@ public interface ITutorialArticle
/// <value>The body.</value>
string Body { get; }
/// <summary>
/// Gets the excerpt of this article, if it has one.
/// </summary>
/// <value>The excerpt, or <see langword="null" /> if this article has no excerpt.</value>
string? Excerpt { get; }
/// <summary>
/// Gets the ID of the folder this article is contained within.
/// </summary>

View File

@ -8,6 +8,9 @@ internal sealed class TutorialArticle : IEquatable<TutorialArticle>, ITutorialAr
/// <inheritdoc />
public string Body { get; private set; } = string.Empty;
/// <inheritdoc />
public string? Excerpt { get; private set; }
/// <inheritdoc />
public int Folder { get; private set; }
@ -15,7 +18,7 @@ internal sealed class TutorialArticle : IEquatable<TutorialArticle>, ITutorialAr
public int Id { get; private set; }
/// <inheritdoc />
public int? NextPart { get; }
public int? NextPart { get; private set; }
/// <inheritdoc />
public Uri? PreviewImageUrl { get; private set; }

View File

@ -90,6 +90,12 @@ internal sealed class BlogPostService : IBlogPostService
/// <inheritdoc />
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("<!--more-->", StringComparison.Ordinal);

View File

@ -67,6 +67,17 @@ public interface ITutorialService
/// <returns>The rendered HTML of the article.</returns>
string RenderArticle(ITutorialArticle article);
/// <summary>
/// Renders the excerpt of the specified article.
/// </summary>
/// <param name="article">The article whose excerpt to render.</param>
/// <param name="wasTrimmed">
/// When this method returns, contains <see langword="true" /> if the excerpt was trimmed; otherwise,
/// <see langword="false" />.
/// </param>
/// <returns>The rendered HTML of the article's excerpt.</returns>
string RenderExcerpt(ITutorialArticle article, out bool wasTrimmed);
/// <summary>
/// Attempts to find an article by its ID.
/// </summary>

View File

@ -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);
}
/// <inheritdoc />
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("<!--more-->", 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);
}
/// <inheritdoc />
public bool TryGetArticle(int id, [NotNullWhen(true)] out ITutorialArticle? article)
{