Compare commits
6 Commits
b119861eee
...
96e63a3088
Author | SHA1 | Date |
---|---|---|
Oliver Booth | 96e63a3088 | |
Oliver Booth | 1919b1d5c8 | |
Oliver Booth | a1dd6ef6ff | |
Oliver Booth | 985acf7bc3 | |
Oliver Booth | 879ff6a295 | |
Oliver Booth | cd6bbec1a5 |
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
@ -24,5 +25,6 @@ internal sealed class TutorialArticleConfiguration : IEntityTypeConfiguration<Tu
|
|||
builder.Property(e => e.NextPart);
|
||||
builder.Property(e => e.PreviousPart);
|
||||
builder.Property(e => e.Visibility).HasConversion<EnumToStringConverter<Visibility>>();
|
||||
builder.Property(e => e.EnableComments).IsRequired();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,20 @@ public interface ITutorialArticle
|
|||
/// <value>The body.</value>
|
||||
string Body { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether comments are enabled for the article.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true" /> if comments are enabled for the article; otherwise, <see langword="false" />.
|
||||
/// </value>
|
||||
bool EnableComments { 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>
|
||||
|
|
|
@ -8,6 +8,12 @@ internal sealed class TutorialArticle : IEquatable<TutorialArticle>, ITutorialAr
|
|||
/// <inheritdoc />
|
||||
public string Body { get; private set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool EnableComments { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? Excerpt { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Folder { get; private set; }
|
||||
|
||||
|
@ -15,7 +21,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; }
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
using System.Web;
|
||||
using Cysharp.Text;
|
||||
using OliverBooth.Data.Blog;
|
||||
using OliverBooth.Data.Web;
|
||||
using OliverBooth.Services;
|
||||
|
||||
namespace OliverBooth.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for generating HTML tags
|
||||
/// </summary>
|
||||
public static class HtmlUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates <c><meta></c> embed tags by pulling data from the specified blog post.
|
||||
/// </summary>
|
||||
/// <param name="post">The blog post whose metadata should be retrieved.</param>
|
||||
/// <param name="blogPostService">The <see cref="IBlogPostService" /> injected by the page.</param>
|
||||
/// <returns>A string containing a collection of <c><meta></c> embed tags.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <para><paramref name="post" /> is <see langword="null" />.</para>
|
||||
/// -or-
|
||||
/// <para><paramref name="blogPostService" /> is <see langword="null" />.</para>
|
||||
/// </exception>
|
||||
public static string CreateMetaTagsFromPost(IBlogPost post, IBlogPostService blogPostService)
|
||||
{
|
||||
if (post is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(post));
|
||||
}
|
||||
|
||||
if (blogPostService is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(blogPostService));
|
||||
}
|
||||
|
||||
|
||||
string excerpt = blogPostService.RenderExcerpt(post, out _);
|
||||
var tags = new Dictionary<string, string>
|
||||
{
|
||||
["title"] = post.Title,
|
||||
["description"] = excerpt,
|
||||
["author"] = post.Author.DisplayName
|
||||
};
|
||||
return CreateMetaTags(tags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <c><meta></c> embed tags by pulling data from the specified article.
|
||||
/// </summary>
|
||||
/// <param name="article">The article whose metadata should be retrieved.</param>
|
||||
/// <param name="tutorialService">The <see cref="ITutorialService" /> injected by the page.</param>
|
||||
/// <returns>A string containing a collection of <c><meta></c> embed tags.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <para><paramref name="article" /> is <see langword="null" />.</para>
|
||||
/// -or-
|
||||
/// <para><paramref name="tutorialService" /> is <see langword="null" />.</para>
|
||||
/// </exception>
|
||||
public static string CreateMetaTagsFromTutorialArticle(ITutorialArticle article, ITutorialService tutorialService)
|
||||
{
|
||||
if (article is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(article));
|
||||
}
|
||||
|
||||
if (tutorialService is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tutorialService));
|
||||
}
|
||||
|
||||
|
||||
string excerpt = tutorialService.RenderExcerpt(article, out _);
|
||||
var tags = new Dictionary<string, string>
|
||||
{
|
||||
["title"] = article.Title,
|
||||
["description"] = excerpt,
|
||||
["author"] = "Oliver Booth" // TODO add article author support?
|
||||
};
|
||||
return CreateMetaTags(tags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <c><meta></c> embed tags by pulling data from the specified dictionary.
|
||||
/// </summary>
|
||||
/// <param name="tags">
|
||||
/// A dictionary containing the tag values. This dictionary should be in the form:
|
||||
///
|
||||
/// <list type="table">
|
||||
/// <listheader>
|
||||
/// <term>Key</term>
|
||||
/// <description>Description</description>
|
||||
/// </listheader>
|
||||
///
|
||||
/// <item>
|
||||
/// <term>description</term>
|
||||
/// <description>
|
||||
/// The value to apply to the <c>description</c>, <c>og:description</c>, and <c>twitter:description</c>, tags.
|
||||
/// </description>
|
||||
/// </item>
|
||||
///
|
||||
/// <item>
|
||||
/// <term>author</term>
|
||||
/// <description>The value to apply to the <c>og:site_name</c>, and <c>twitter:creator</c>, tags.</description>
|
||||
/// </item>
|
||||
///
|
||||
/// <item>
|
||||
/// <term>title</term>
|
||||
/// <description>
|
||||
/// The value to apply to the <c>title</c>, <c>og:title</c>, and <c>twitter:title</c>, tags.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
///
|
||||
/// Any other values contained with the dictionary are ignored.
|
||||
/// </param>
|
||||
/// <returns>A string containing a collection of <c><meta></c> embed tags.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="tags" /> is <see langword="null" />.</exception>
|
||||
public static string CreateMetaTags(IReadOnlyDictionary<string, string> tags)
|
||||
{
|
||||
if (tags is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tags));
|
||||
}
|
||||
|
||||
using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder();
|
||||
builder.AppendLine("""<meta property="og:type" content="article">""");
|
||||
|
||||
if (tags.TryGetValue("description", out string? description))
|
||||
{
|
||||
description = HttpUtility.HtmlEncode(description);
|
||||
builder.AppendLine($"""<meta name="description" content="{description}">""");
|
||||
builder.AppendLine($"""<meta property="og:description" content="{description}">""");
|
||||
builder.AppendLine($"""<meta property="twitter:description" content="{description}">""");
|
||||
}
|
||||
|
||||
if (tags.TryGetValue("author", out string? author))
|
||||
{
|
||||
author = HttpUtility.HtmlEncode(author);
|
||||
builder.AppendLine($"""<meta property="og:site_name" content="{author}">""");
|
||||
builder.AppendLine($"""<meta property="twitter:creator" content="{author}">""");
|
||||
}
|
||||
|
||||
if (tags.TryGetValue("title", out string? title))
|
||||
{
|
||||
title = HttpUtility.HtmlEncode(title);
|
||||
builder.AppendLine($"""<meta name="title" content="{title}">""");
|
||||
builder.AppendLine($"""<meta property="og:title" content="{title}">""");
|
||||
builder.AppendLine($"""<meta property="twitter:title" content="{title}">""");
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
|
@ -132,31 +132,25 @@
|
|||
|
||||
@if (post.EnableComments)
|
||||
{
|
||||
<div id="disqus_thread"></div>
|
||||
<script>
|
||||
var disqus_config = function () {
|
||||
this.page.url = "@post.GetDisqusUrl()";
|
||||
this.page.identifier = "@post.GetDisqusIdentifier()";
|
||||
this.page.title = "@post.Title";
|
||||
this.page.postId = "@post.GetDisqusPostId()";
|
||||
};
|
||||
|
||||
(function() {
|
||||
const d = document, s = d.createElement("script");
|
||||
s.async = true;
|
||||
s.type = "text/javascript";
|
||||
s.src = "https://oliverbooth-dev.disqus.com/embed.js";
|
||||
s.setAttribute("data-timestamp", (+ new Date()).toString());
|
||||
(d.head || d.body).appendChild(s);
|
||||
})();
|
||||
<div class="giscus"></div>
|
||||
@section Scripts
|
||||
{
|
||||
<script src="https://giscus.app/client.js"
|
||||
data-repo="oliverbooth/oliverbooth.dev"
|
||||
data-repo-id="MDEwOlJlcG9zaXRvcnkyNDUxODEyNDI="
|
||||
data-category="Comments"
|
||||
data-category-id="DIC_kwDODp0rOs4Ce_Nj"
|
||||
data-mapping="pathname"
|
||||
data-strict="0"
|
||||
data-reactions-enabled="1"
|
||||
data-emit-metadata="0"
|
||||
data-input-position="bottom"
|
||||
data-theme="preferred_color_scheme"
|
||||
data-lang="en"
|
||||
crossorigin="anonymous"
|
||||
async>
|
||||
</script>
|
||||
<script id="dsq-count-scr" src="https://oliverbooth-dev.disqus.com/count.js" async></script>
|
||||
<noscript>
|
||||
Please enable JavaScript to view the
|
||||
<a href="https://disqus.com/?ref_noscript" rel="nofollow">
|
||||
comments powered by Disqus.
|
||||
</a>
|
||||
</noscript>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -8,21 +8,8 @@
|
|||
@{
|
||||
ViewData["Title"] = "Blog";
|
||||
MastodonStatus latestStatus = MastodonService.GetLatestStatus();
|
||||
bool doAprilFools = DateOnly.FromDateTime(DateTime.UtcNow) == new DateOnly(2024, 04, 01) || Environment.GetEnvironmentVariable("DO_AF") == "1";
|
||||
}
|
||||
|
||||
@if (doAprilFools)
|
||||
{
|
||||
<h1>UNDER CONSTRUCTION</h1>
|
||||
<div style="text-align: center">
|
||||
<img src="~/img/construction_90x85.gif">
|
||||
<img src="~/img/underconstruction_323x118.gif">
|
||||
<img src="~/img/construction_90x85.gif">
|
||||
<p>Coming soon WATCH THIS SPACE</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card text-center mastodon-update-card">
|
||||
<div class="card-body">
|
||||
@Html.Raw(latestStatus.Content)
|
||||
|
@ -92,4 +79,3 @@ else
|
|||
{{/each}}
|
||||
</div>
|
||||
</script>
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<main class="container">
|
||||
<h1 class="display-4">@ViewData["Title"]</h1>
|
||||
<p class="lead">Last Updated: 26 May 2023</p>
|
||||
<p class="lead">Last Updated: 27 April 2024</p>
|
||||
<div class="alert alert-primary">
|
||||
This Privacy Policy differs from the policy that applies to my applications published to Google Play. For my
|
||||
applications' privacy policy, please <a asp-page="/Privacy/GooglePlay">click here</a>.
|
||||
|
@ -32,10 +32,13 @@
|
|||
</p>
|
||||
<p>I do not use any cookies or similar tracking technologies that can identify individual users.</p>
|
||||
<p>
|
||||
Please note that my website includes a Disqus integration for commenting on blog posts. Disqus is a third-party
|
||||
Please note that my website includes a GitHub integration for commenting on blog posts. GitHub is a third-party
|
||||
service, and their use of cookies and collection of personal information are governed by their own privacy
|
||||
policies. I have no control over the information collected by Disqus, and I encourage you to review their
|
||||
privacy policy to understand how your information may be used by them.
|
||||
policies. I have no control over the information collected by GitHub, and I encourage you to review
|
||||
<a href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement">
|
||||
their privacy policy
|
||||
</a>
|
||||
to understand how your information may be used by them.
|
||||
</p>
|
||||
|
||||
<h2>Use of Information</h2>
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
@using OliverBooth.Data.Blog
|
||||
@using OliverBooth.Data.Web
|
||||
@using OliverBooth.Extensions
|
||||
@using OliverBooth.Services
|
||||
@inject IBlogPostService BlogPostService
|
||||
@inject ITutorialService TutorialService
|
||||
@{
|
||||
HttpRequest request = Context.Request;
|
||||
var url = new Uri($"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}");
|
||||
bool doAprilFools = DateOnly.FromDateTime(DateTime.UtcNow) == new DateOnly(2024, 04, 01) || Environment.GetEnvironmentVariable("DO_AF") == "1";
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
|
@ -28,15 +30,11 @@
|
|||
}
|
||||
@if (ViewData["Post"] is IBlogPost post)
|
||||
{
|
||||
string excerpt = BlogPostService.RenderExcerpt(post, out bool trimmed);
|
||||
<meta name="title" content="@post.Title">
|
||||
<meta name="description" content="@excerpt">
|
||||
<meta property="og:title" content="@post.Title">
|
||||
<meta property="og:description" content="@excerpt">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="twitter:title" content="@post.Title">
|
||||
<meta property="twitter:creator" content="@post.Author.DisplayName">
|
||||
<meta property="twitter:description" content="@excerpt">
|
||||
@Html.Raw(HtmlUtility.CreateMetaTagsFromPost(post, BlogPostService))
|
||||
}
|
||||
else if (ViewData["Post"] is ITutorialArticle article)
|
||||
{
|
||||
@Html.Raw(HtmlUtility.CreateMetaTagsFromTutorialArticle(article, TutorialService))
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -62,28 +60,12 @@
|
|||
<link rel="stylesheet" href="~/css/prism.vs.min.css" asp-append-version="true">
|
||||
<link rel="stylesheet" href="~/css/app.min.css" asp-append-version="true">
|
||||
<link rel="stylesheet" href="~/css/ribbon.min.css" asp-append-version="true">
|
||||
@if (doAprilFools)
|
||||
{
|
||||
<link rel="stylesheet" href="~/css/af-app.min.css" asp-append-version="true">
|
||||
}
|
||||
</head>
|
||||
<body>
|
||||
<header class="container" style="margin-top: 20px;">
|
||||
<div id="site-title" class="text-center">
|
||||
<h1>
|
||||
@if (doAprilFools)
|
||||
{
|
||||
<marquee>
|
||||
<a href="/">
|
||||
<img src="~/img/ob-af-256x256.png" alt="Oliver Booth" height="128">
|
||||
<img src="~/img/af-oliverbooth-1236x293.png" alt="Oliver Booth" height="128">
|
||||
</a>
|
||||
</marquee>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="/"><img src="~/img/ob-256x256.png" alt="Oliver Booth" height="128"> Oliver Booth</a>
|
||||
}
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
@ -113,13 +95,6 @@
|
|||
<div style="margin:50px 0;"></div>
|
||||
|
||||
<div class="container">
|
||||
@if (doAprilFools)
|
||||
{
|
||||
<h1 style="text-decoration: underline; color: #0f0 !important; margin: 20px 0;">
|
||||
<img src="~/img/af-homepage_500x383.jpg" alt="WELCOME TO MY HOMEPAGE!!!!111SHIFT+1">
|
||||
</h1>
|
||||
}
|
||||
|
||||
<main role="main" class="pb-3">
|
||||
@RenderBody()
|
||||
</main>
|
||||
|
@ -137,7 +112,7 @@
|
|||
</ul>
|
||||
|
||||
<ul class="footer-nav" style="margin-top: 20px;">
|
||||
<li>@(doAprilFools ? "(C) 2003" : Html.Raw($"© {DateTime.UtcNow.Year}"))</li>
|
||||
<li>© @DateTime.UtcNow.Year</li>
|
||||
<li><a asp-page="/privacy/index">Privacy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -90,3 +90,32 @@
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
@if (article.EnableComments)
|
||||
{
|
||||
<div class="giscus"></div>
|
||||
@section Scripts
|
||||
{
|
||||
<script src="https://giscus.app/client.js"
|
||||
data-repo="oliverbooth/oliverbooth.dev"
|
||||
data-repo-id="MDEwOlJlcG9zaXRvcnkyNDUxODEyNDI="
|
||||
data-category="Comments"
|
||||
data-category-id="DIC_kwDODp0rOs4Ce_Nj"
|
||||
data-mapping="pathname"
|
||||
data-strict="0"
|
||||
data-reactions-enabled="1"
|
||||
data-emit-metadata="0"
|
||||
data-input-position="bottom"
|
||||
data-theme="preferred_color_scheme"
|
||||
data-lang="en"
|
||||
crossorigin="anonymous"
|
||||
async>
|
||||
</script>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-center text-muted">Comments are not enabled for this post.</p>
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue