diff --git a/OliverBooth/Data/Blog/IBlogAuthor.cs b/OliverBooth.Common/Data/Blog/IBlogAuthor.cs
similarity index 95%
rename from OliverBooth/Data/Blog/IBlogAuthor.cs
rename to OliverBooth.Common/Data/Blog/IBlogAuthor.cs
index 3f1c3e1..279da26 100644
--- a/OliverBooth/Data/Blog/IBlogAuthor.cs
+++ b/OliverBooth.Common/Data/Blog/IBlogAuthor.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Blog;
+namespace OliverBooth.Common.Data.Blog;
///
/// Represents the author of a blog post.
diff --git a/OliverBooth/Data/Blog/IBlogPost.cs b/OliverBooth.Common/Data/Blog/IBlogPost.cs
similarity index 98%
rename from OliverBooth/Data/Blog/IBlogPost.cs
rename to OliverBooth.Common/Data/Blog/IBlogPost.cs
index bc1a2e5..093220c 100644
--- a/OliverBooth/Data/Blog/IBlogPost.cs
+++ b/OliverBooth.Common/Data/Blog/IBlogPost.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Blog;
+namespace OliverBooth.Common.Data.Blog;
///
/// Represents a blog post.
diff --git a/OliverBooth/Data/Blog/ILegacyComment.cs b/OliverBooth.Common/Data/Blog/ILegacyComment.cs
similarity index 97%
rename from OliverBooth/Data/Blog/ILegacyComment.cs
rename to OliverBooth.Common/Data/Blog/ILegacyComment.cs
index 2f43dc2..2a5cc99 100644
--- a/OliverBooth/Data/Blog/ILegacyComment.cs
+++ b/OliverBooth.Common/Data/Blog/ILegacyComment.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Blog;
+namespace OliverBooth.Common.Data.Blog;
///
/// Represents a comment that was posted on a legacy comment framework.
diff --git a/OliverBooth/Data/Blog/IUser.cs b/OliverBooth.Common/Data/Blog/IUser.cs
similarity index 97%
rename from OliverBooth/Data/Blog/IUser.cs
rename to OliverBooth.Common/Data/Blog/IUser.cs
index 912db14..80af1b8 100644
--- a/OliverBooth/Data/Blog/IUser.cs
+++ b/OliverBooth.Common/Data/Blog/IUser.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Blog;
+namespace OliverBooth.Common.Data.Blog;
///
/// Represents a user which can log in to the blog.
diff --git a/OliverBooth/Data/Mastodon/AttachmentType.cs b/OliverBooth.Common/Data/Mastodon/AttachmentType.cs
similarity index 66%
rename from OliverBooth/Data/Mastodon/AttachmentType.cs
rename to OliverBooth.Common/Data/Mastodon/AttachmentType.cs
index 6d0b5e1..701db4e 100644
--- a/OliverBooth/Data/Mastodon/AttachmentType.cs
+++ b/OliverBooth.Common/Data/Mastodon/AttachmentType.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Mastodon;
+namespace OliverBooth.Common.Data.Mastodon;
public enum AttachmentType
{
diff --git a/OliverBooth.Common/Data/Mastodon/IMastodonStatus.cs b/OliverBooth.Common/Data/Mastodon/IMastodonStatus.cs
new file mode 100644
index 0000000..f7bdc95
--- /dev/null
+++ b/OliverBooth.Common/Data/Mastodon/IMastodonStatus.cs
@@ -0,0 +1,31 @@
+namespace OliverBooth.Common.Data.Mastodon;
+
+///
+/// Represents a status on Mastodon.
+///
+public interface IMastodonStatus
+{
+ ///
+ /// Gets the content of the status.
+ ///
+ /// The content.
+ string Content { get; }
+
+ ///
+ /// Gets the date and time at which this status was posted.
+ ///
+ /// The post timestamp.
+ DateTimeOffset CreatedAt { get; }
+
+ ///
+ /// Gets the media attachments for this status.
+ ///
+ /// The media attachments.
+ IReadOnlyList MediaAttachments { get; }
+
+ ///
+ /// Gets the original URI of the status.
+ ///
+ /// The original URI.
+ Uri OriginalUri { get; }
+}
diff --git a/OliverBooth/Data/Mastodon/MediaAttachment.cs b/OliverBooth.Common/Data/Mastodon/MediaAttachment.cs
similarity index 92%
rename from OliverBooth/Data/Mastodon/MediaAttachment.cs
rename to OliverBooth.Common/Data/Mastodon/MediaAttachment.cs
index ddc3307..117be81 100644
--- a/OliverBooth/Data/Mastodon/MediaAttachment.cs
+++ b/OliverBooth.Common/Data/Mastodon/MediaAttachment.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Mastodon;
+namespace OliverBooth.Common.Data.Mastodon;
public sealed class MediaAttachment
{
diff --git a/OliverBooth/Data/Visibility.cs b/OliverBooth.Common/Data/Visibility.cs
similarity index 94%
rename from OliverBooth/Data/Visibility.cs
rename to OliverBooth.Common/Data/Visibility.cs
index 17b3e87..5bd0e92 100644
--- a/OliverBooth/Data/Visibility.cs
+++ b/OliverBooth.Common/Data/Visibility.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data;
+namespace OliverBooth.Common.Data;
///
/// An enumeration of the possible visibilities of a blog post.
diff --git a/OliverBooth/Data/Web/BookState.cs b/OliverBooth.Common/Data/Web/BookState.cs
similarity index 90%
rename from OliverBooth/Data/Web/BookState.cs
rename to OliverBooth.Common/Data/Web/BookState.cs
index 637415a..29b8312 100644
--- a/OliverBooth/Data/Web/BookState.cs
+++ b/OliverBooth.Common/Data/Web/BookState.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Web;
+namespace OliverBooth.Common.Data.Web;
///
/// Represents the state of a book.
diff --git a/OliverBooth/Data/Web/IBlacklistEntry.cs b/OliverBooth.Common/Data/Web/IBlacklistEntry.cs
similarity index 93%
rename from OliverBooth/Data/Web/IBlacklistEntry.cs
rename to OliverBooth.Common/Data/Web/IBlacklistEntry.cs
index 00516fb..6c55acc 100644
--- a/OliverBooth/Data/Web/IBlacklistEntry.cs
+++ b/OliverBooth.Common/Data/Web/IBlacklistEntry.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Web;
+namespace OliverBooth.Common.Data.Web;
///
/// Represents an entry in the blacklist.
diff --git a/OliverBooth/Data/Web/IBook.cs b/OliverBooth.Common/Data/Web/IBook.cs
similarity index 95%
rename from OliverBooth/Data/Web/IBook.cs
rename to OliverBooth.Common/Data/Web/IBook.cs
index 5c71d85..77f5856 100644
--- a/OliverBooth/Data/Web/IBook.cs
+++ b/OliverBooth.Common/Data/Web/IBook.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Web;
+namespace OliverBooth.Common.Data.Web;
///
/// Represents a book.
diff --git a/OliverBooth/Data/Web/ICodeSnippet.cs b/OliverBooth.Common/Data/Web/ICodeSnippet.cs
similarity index 93%
rename from OliverBooth/Data/Web/ICodeSnippet.cs
rename to OliverBooth.Common/Data/Web/ICodeSnippet.cs
index ba1d324..29ec491 100644
--- a/OliverBooth/Data/Web/ICodeSnippet.cs
+++ b/OliverBooth.Common/Data/Web/ICodeSnippet.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Web;
+namespace OliverBooth.Common.Data.Web;
///
/// Represents a code snippet.
diff --git a/OliverBooth/Data/Web/IProgrammingLanguage.cs b/OliverBooth.Common/Data/Web/IProgrammingLanguage.cs
similarity index 92%
rename from OliverBooth/Data/Web/IProgrammingLanguage.cs
rename to OliverBooth.Common/Data/Web/IProgrammingLanguage.cs
index 039f31a..464b943 100644
--- a/OliverBooth/Data/Web/IProgrammingLanguage.cs
+++ b/OliverBooth.Common/Data/Web/IProgrammingLanguage.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Web;
+namespace OliverBooth.Common.Data.Web;
///
/// Represents a programming language.
diff --git a/OliverBooth/Data/Web/IProject.cs b/OliverBooth.Common/Data/Web/IProject.cs
similarity index 98%
rename from OliverBooth/Data/Web/IProject.cs
rename to OliverBooth.Common/Data/Web/IProject.cs
index f2cc0d8..0b41719 100644
--- a/OliverBooth/Data/Web/IProject.cs
+++ b/OliverBooth.Common/Data/Web/IProject.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Web;
+namespace OliverBooth.Common.Data.Web;
///
/// Represents a project.
diff --git a/OliverBooth/Data/Web/ITemplate.cs b/OliverBooth.Common/Data/Web/ITemplate.cs
similarity index 92%
rename from OliverBooth/Data/Web/ITemplate.cs
rename to OliverBooth.Common/Data/Web/ITemplate.cs
index 50ad893..b3bd1e5 100644
--- a/OliverBooth/Data/Web/ITemplate.cs
+++ b/OliverBooth.Common/Data/Web/ITemplate.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Web;
+namespace OliverBooth.Common.Data.Web;
///
/// Represents a template.
diff --git a/OliverBooth/Data/Web/ITutorialArticle.cs b/OliverBooth.Common/Data/Web/ITutorialArticle.cs
similarity index 98%
rename from OliverBooth/Data/Web/ITutorialArticle.cs
rename to OliverBooth.Common/Data/Web/ITutorialArticle.cs
index a53b289..7d27e63 100644
--- a/OliverBooth/Data/Web/ITutorialArticle.cs
+++ b/OliverBooth.Common/Data/Web/ITutorialArticle.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Web;
+namespace OliverBooth.Common.Data.Web;
///
/// Represents a tutorial article.
diff --git a/OliverBooth/Data/Web/ITutorialFolder.cs b/OliverBooth.Common/Data/Web/ITutorialFolder.cs
similarity index 96%
rename from OliverBooth/Data/Web/ITutorialFolder.cs
rename to OliverBooth.Common/Data/Web/ITutorialFolder.cs
index 65a3cd2..0544d85 100644
--- a/OliverBooth/Data/Web/ITutorialFolder.cs
+++ b/OliverBooth.Common/Data/Web/ITutorialFolder.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Data.Web;
+namespace OliverBooth.Common.Data.Web;
///
/// Represents a folder for tutorial articles.
diff --git a/OliverBooth/Data/Web/ProjectStatus.cs b/OliverBooth.Common/Data/Web/ProjectStatus.cs
similarity index 92%
rename from OliverBooth/Data/Web/ProjectStatus.cs
rename to OliverBooth.Common/Data/Web/ProjectStatus.cs
index a3cbf49..a7e4cb1 100644
--- a/OliverBooth/Data/Web/ProjectStatus.cs
+++ b/OliverBooth.Common/Data/Web/ProjectStatus.cs
@@ -1,6 +1,6 @@
using System.ComponentModel;
-namespace OliverBooth.Data.Web;
+namespace OliverBooth.Common.Data.Web;
///
/// Represents the status of a project.
diff --git a/OliverBooth.Common/OliverBooth.Common.csproj b/OliverBooth.Common/OliverBooth.Common.csproj
new file mode 100644
index 0000000..5f634e5
--- /dev/null
+++ b/OliverBooth.Common/OliverBooth.Common.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/OliverBooth/Services/IBlogPostService.cs b/OliverBooth.Common/Services/IBlogPostService.cs
similarity index 88%
rename from OliverBooth/Services/IBlogPostService.cs
rename to OliverBooth.Common/Services/IBlogPostService.cs
index 5947f19..5517539 100644
--- a/OliverBooth/Services/IBlogPostService.cs
+++ b/OliverBooth.Common/Services/IBlogPostService.cs
@@ -1,7 +1,8 @@
using System.Diagnostics.CodeAnalysis;
-using OliverBooth.Data.Blog;
+using OliverBooth.Common.Data;
+using OliverBooth.Common.Data.Blog;
-namespace OliverBooth.Services;
+namespace OliverBooth.Common.Services;
///
/// Represents a service for managing blog posts.
@@ -22,8 +23,9 @@ public interface IBlogPostService
///
/// Returns the total number of blog posts.
///
+ /// The post visibility filter.
/// The total number of blog posts.
- int GetBlogPostCount();
+ int GetBlogPostCount(Visibility visibility = Visibility.None);
///
/// Returns a collection of blog posts from the specified page, optionally limiting the number of posts
@@ -62,6 +64,15 @@ public interface IBlogPostService
/// The next blog post from the specified blog post.
IBlogPost? GetNextPost(IBlogPost blogPost);
+ ///
+ /// Returns the number of pages needed to render all blog posts, using the specified as an
+ /// indicator of how many posts are allowed per page.
+ ///
+ /// The page size. Defaults to 10.
+ /// The post visibility filter.
+ /// The page count.
+ int GetPageCount(int pageSize = 10, Visibility visibility = Visibility.None);
+
///
/// Returns the previous blog post from the specified blog post.
///
diff --git a/OliverBooth/Services/IBlogUserService.cs b/OliverBooth.Common/Services/IBlogUserService.cs
similarity index 90%
rename from OliverBooth/Services/IBlogUserService.cs
rename to OliverBooth.Common/Services/IBlogUserService.cs
index 0a117ae..83577e3 100644
--- a/OliverBooth/Services/IBlogUserService.cs
+++ b/OliverBooth.Common/Services/IBlogUserService.cs
@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
-using OliverBooth.Data.Blog;
+using OliverBooth.Common.Data.Blog;
-namespace OliverBooth.Services;
+namespace OliverBooth.Common.Services;
///
/// Represents a service for managing users.
diff --git a/OliverBooth/Services/ICodeSnippetService.cs b/OliverBooth.Common/Services/ICodeSnippetService.cs
similarity index 95%
rename from OliverBooth/Services/ICodeSnippetService.cs
rename to OliverBooth.Common/Services/ICodeSnippetService.cs
index b81cb88..d065ce0 100644
--- a/OliverBooth/Services/ICodeSnippetService.cs
+++ b/OliverBooth.Common/Services/ICodeSnippetService.cs
@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
-using OliverBooth.Data.Web;
+using OliverBooth.Common.Data.Web;
-namespace OliverBooth.Services;
+namespace OliverBooth.Common.Services;
///
/// Represents a service which can fetch multi-language code snippets.
diff --git a/OliverBooth/Services/IContactService.cs b/OliverBooth.Common/Services/IContactService.cs
similarity index 77%
rename from OliverBooth/Services/IContactService.cs
rename to OliverBooth.Common/Services/IContactService.cs
index 800a3e7..38b1e9c 100644
--- a/OliverBooth/Services/IContactService.cs
+++ b/OliverBooth.Common/Services/IContactService.cs
@@ -1,6 +1,6 @@
-using OliverBooth.Data.Web;
+using OliverBooth.Common.Data.Web;
-namespace OliverBooth.Services;
+namespace OliverBooth.Common.Services;
///
/// Represents a service for managing contact information.
diff --git a/OliverBooth/Services/IMastodonService.cs b/OliverBooth.Common/Services/IMastodonService.cs
similarity index 59%
rename from OliverBooth/Services/IMastodonService.cs
rename to OliverBooth.Common/Services/IMastodonService.cs
index 2126d46..dc38a51 100644
--- a/OliverBooth/Services/IMastodonService.cs
+++ b/OliverBooth.Common/Services/IMastodonService.cs
@@ -1,6 +1,6 @@
-using OliverBooth.Data.Mastodon;
+using OliverBooth.Common.Data.Mastodon;
-namespace OliverBooth.Services;
+namespace OliverBooth.Common.Services;
public interface IMastodonService
{
@@ -8,5 +8,5 @@ public interface IMastodonService
/// Gets the latest status posted to Mastodon.
///
/// The latest status.
- MastodonStatus GetLatestStatus();
+ IMastodonStatus GetLatestStatus();
}
\ No newline at end of file
diff --git a/OliverBooth.Common/Services/IProgrammingLanguageService.cs b/OliverBooth.Common/Services/IProgrammingLanguageService.cs
new file mode 100644
index 0000000..dc0acd0
--- /dev/null
+++ b/OliverBooth.Common/Services/IProgrammingLanguageService.cs
@@ -0,0 +1,14 @@
+namespace OliverBooth.Common.Services;
+
+///
+/// Represents a service which can perform programming language lookup.
+///
+public interface IProgrammingLanguageService
+{
+ ///
+ /// Returns the human-readable name of a language.
+ ///
+ /// The alias of the language.
+ /// The human-readable name, or if the name could not be found.
+ string GetLanguageName(string alias);
+}
diff --git a/OliverBooth/Services/IProjectService.cs b/OliverBooth.Common/Services/IProjectService.cs
similarity index 97%
rename from OliverBooth/Services/IProjectService.cs
rename to OliverBooth.Common/Services/IProjectService.cs
index 16ae771..66cdbaa 100644
--- a/OliverBooth/Services/IProjectService.cs
+++ b/OliverBooth.Common/Services/IProjectService.cs
@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
-using OliverBooth.Data.Web;
+using OliverBooth.Common.Data.Web;
-namespace OliverBooth.Services;
+namespace OliverBooth.Common.Services;
///
/// Represents a service for interacting with projects.
diff --git a/OliverBooth/Services/IReadingListService.cs b/OliverBooth.Common/Services/IReadingListService.cs
similarity index 85%
rename from OliverBooth/Services/IReadingListService.cs
rename to OliverBooth.Common/Services/IReadingListService.cs
index 78cc4f2..6f59791 100644
--- a/OliverBooth/Services/IReadingListService.cs
+++ b/OliverBooth.Common/Services/IReadingListService.cs
@@ -1,6 +1,6 @@
-using OliverBooth.Data.Web;
+using OliverBooth.Common.Data.Web;
-namespace OliverBooth.Services;
+namespace OliverBooth.Common.Services;
///
/// Represents a service which fetches books from the reading list.
diff --git a/OliverBooth/Services/ITutorialService.cs b/OliverBooth.Common/Services/ITutorialService.cs
similarity index 97%
rename from OliverBooth/Services/ITutorialService.cs
rename to OliverBooth.Common/Services/ITutorialService.cs
index 1366172..11ddc62 100644
--- a/OliverBooth/Services/ITutorialService.cs
+++ b/OliverBooth.Common/Services/ITutorialService.cs
@@ -1,9 +1,9 @@
using System.Diagnostics.CodeAnalysis;
-using OliverBooth.Data;
-using OliverBooth.Data.Blog;
-using OliverBooth.Data.Web;
+using OliverBooth.Common.Data;
+using OliverBooth.Common.Data.Blog;
+using OliverBooth.Common.Data.Web;
-namespace OliverBooth.Services;
+namespace OliverBooth.Common.Services;
///
/// Represents a service which can retrieve tutorial articles.
diff --git a/OliverBooth/Markdown/Callout/CalloutBlock.cs b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutBlock.cs
similarity index 95%
rename from OliverBooth/Markdown/Callout/CalloutBlock.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutBlock.cs
index 5c01675..fdf484a 100644
--- a/OliverBooth/Markdown/Callout/CalloutBlock.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutBlock.cs
@@ -1,7 +1,7 @@
using Markdig.Helpers;
using Markdig.Syntax;
-namespace OliverBooth.Markdown.Callout;
+namespace OliverBooth.Extensions.Markdig.Markdown.Callout;
///
/// Represents a callout block.
diff --git a/OliverBooth/Markdown/Callout/CalloutExtension.cs b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutExtension.cs
similarity index 94%
rename from OliverBooth/Markdown/Callout/CalloutExtension.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutExtension.cs
index c9731a2..eda4718 100644
--- a/OliverBooth/Markdown/Callout/CalloutExtension.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutExtension.cs
@@ -3,7 +3,7 @@ using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using Markdig.Renderers.Html;
-namespace OliverBooth.Markdown.Callout;
+namespace OliverBooth.Extensions.Markdig.Markdown.Callout;
///
/// Extension for adding Obsidian-style callouts to a Markdown pipeline.
diff --git a/OliverBooth/Markdown/Callout/CalloutInlineParser.cs b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutInlineParser.cs
similarity index 98%
rename from OliverBooth/Markdown/Callout/CalloutInlineParser.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutInlineParser.cs
index ce7ddc5..9434011 100644
--- a/OliverBooth/Markdown/Callout/CalloutInlineParser.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutInlineParser.cs
@@ -5,7 +5,7 @@ using Markdig.Parsers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
-namespace OliverBooth.Markdown.Callout;
+namespace OliverBooth.Extensions.Markdig.Markdown.Callout;
///
/// An inline parser for Obsidian-style callouts ([!NOTE] etc.)
diff --git a/OliverBooth/Markdown/Callout/CalloutRenderer.cs b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutRenderer.cs
similarity index 96%
rename from OliverBooth/Markdown/Callout/CalloutRenderer.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutRenderer.cs
index f2e4f77..49dd2f2 100644
--- a/OliverBooth/Markdown/Callout/CalloutRenderer.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutRenderer.cs
@@ -4,7 +4,7 @@ using Markdig;
using Markdig.Renderers;
using Markdig.Renderers.Html;
-namespace OliverBooth.Markdown.Callout;
+namespace OliverBooth.Extensions.Markdig.Markdown.Callout;
///
/// Represents an HTML renderer which renders a .
@@ -96,7 +96,7 @@ internal sealed class CalloutRenderer : HtmlObjectRenderer
private static void WriteTitle(TextRendererBase renderer, MarkdownPipeline pipeline, string calloutTitle)
{
- string html = Markdig.Markdown.ToHtml(calloutTitle, pipeline);
+ string html = global::Markdig.Markdown.ToHtml(calloutTitle, pipeline);
var document = new HtmlDocument();
document.LoadHtml(html);
if (document.DocumentNode.FirstChild is { Name: "p" } child)
diff --git a/OliverBooth/Markdown/Template/TemplateExtension.cs b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateExtension.cs
similarity index 90%
rename from OliverBooth/Markdown/Template/TemplateExtension.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Template/TemplateExtension.cs
index ec1ea0d..c160735 100644
--- a/OliverBooth/Markdown/Template/TemplateExtension.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateExtension.cs
@@ -1,8 +1,8 @@
using Markdig;
using Markdig.Renderers;
-using OliverBooth.Services;
+using OliverBooth.Extensions.Markdig.Services;
-namespace OliverBooth.Markdown.Template;
+namespace OliverBooth.Extensions.Markdig.Markdown.Template;
///
/// Represents a Markdown extension that adds support for MediaWiki-style templates.
diff --git a/OliverBooth/Markdown/Template/TemplateInline.cs b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateInline.cs
similarity index 95%
rename from OliverBooth/Markdown/Template/TemplateInline.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Template/TemplateInline.cs
index a50f3b8..6b19845 100644
--- a/OliverBooth/Markdown/Template/TemplateInline.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateInline.cs
@@ -1,6 +1,6 @@
using Markdig.Syntax.Inlines;
-namespace OliverBooth.Markdown.Template;
+namespace OliverBooth.Extensions.Markdig.Markdown.Template;
///
/// Represents a Markdown inline element that represents a MediaWiki-style template.
diff --git a/OliverBooth/Markdown/Template/TemplateInlineParser.cs b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateInlineParser.cs
similarity index 99%
rename from OliverBooth/Markdown/Template/TemplateInlineParser.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Template/TemplateInlineParser.cs
index 57385b3..953f415 100644
--- a/OliverBooth/Markdown/Template/TemplateInlineParser.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateInlineParser.cs
@@ -2,7 +2,7 @@ using Cysharp.Text;
using Markdig.Helpers;
using Markdig.Parsers;
-namespace OliverBooth.Markdown.Template;
+namespace OliverBooth.Extensions.Markdig.Markdown.Template;
///
/// Represents a Markdown inline parser that handles MediaWiki-style templates.
diff --git a/OliverBooth/Markdown/Template/TemplateRenderer.cs b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateRenderer.cs
similarity index 88%
rename from OliverBooth/Markdown/Template/TemplateRenderer.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Template/TemplateRenderer.cs
index 9e70fb2..1c71480 100644
--- a/OliverBooth/Markdown/Template/TemplateRenderer.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateRenderer.cs
@@ -1,8 +1,8 @@
using Markdig.Renderers;
using Markdig.Renderers.Html;
-using OliverBooth.Services;
+using OliverBooth.Extensions.Markdig.Services;
-namespace OliverBooth.Markdown.Template;
+namespace OliverBooth.Extensions.Markdig.Markdown.Template;
///
/// Represents a Markdown object renderer that handles elements.
diff --git a/OliverBooth/Markdown/Timestamp/TimestampExtension.cs b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampExtension.cs
similarity index 91%
rename from OliverBooth/Markdown/Timestamp/TimestampExtension.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampExtension.cs
index 45ae2df..0a9dac3 100644
--- a/OliverBooth/Markdown/Timestamp/TimestampExtension.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampExtension.cs
@@ -1,7 +1,7 @@
using Markdig;
using Markdig.Renderers;
-namespace OliverBooth.Markdown.Timestamp;
+namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp;
///
/// Represents a Markdig extension that supports Discord-style timestamps.
diff --git a/OliverBooth/Markdown/Timestamp/TimestampFormat.cs b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampFormat.cs
similarity index 93%
rename from OliverBooth/Markdown/Timestamp/TimestampFormat.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampFormat.cs
index 29eca6b..49530be 100644
--- a/OliverBooth/Markdown/Timestamp/TimestampFormat.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampFormat.cs
@@ -1,4 +1,4 @@
-namespace OliverBooth.Markdown.Timestamp;
+namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp;
///
/// An enumeration of timestamp formats.
diff --git a/OliverBooth/Markdown/Timestamp/TimestampInline.cs b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInline.cs
similarity index 89%
rename from OliverBooth/Markdown/Timestamp/TimestampInline.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInline.cs
index 615ee03..d780ad9 100644
--- a/OliverBooth/Markdown/Timestamp/TimestampInline.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInline.cs
@@ -1,6 +1,6 @@
using Markdig.Syntax.Inlines;
-namespace OliverBooth.Markdown.Timestamp;
+namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp;
///
/// Represents a Markdown inline element that contains a timestamp.
diff --git a/OliverBooth/Markdown/Timestamp/TimestampInlineParser.cs b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInlineParser.cs
similarity index 97%
rename from OliverBooth/Markdown/Timestamp/TimestampInlineParser.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInlineParser.cs
index 414fc48..ceaaec5 100644
--- a/OliverBooth/Markdown/Timestamp/TimestampInlineParser.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInlineParser.cs
@@ -1,7 +1,7 @@
using Markdig.Helpers;
using Markdig.Parsers;
-namespace OliverBooth.Markdown.Timestamp;
+namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp;
///
/// Represents a Markdown inline parser that matches Discord-style timestamps.
diff --git a/OliverBooth/Markdown/Timestamp/TimestampRenderer.cs b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampRenderer.cs
similarity index 96%
rename from OliverBooth/Markdown/Timestamp/TimestampRenderer.cs
rename to OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampRenderer.cs
index b2c7f3d..ceda5ac 100644
--- a/OliverBooth/Markdown/Timestamp/TimestampRenderer.cs
+++ b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampRenderer.cs
@@ -3,7 +3,7 @@ using Humanizer;
using Markdig.Renderers;
using Markdig.Renderers.Html;
-namespace OliverBooth.Markdown.Timestamp;
+namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp;
///
/// Represents a Markdown object renderer that renders elements.
diff --git a/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs b/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs
new file mode 100644
index 0000000..5426937
--- /dev/null
+++ b/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs
@@ -0,0 +1,56 @@
+using Markdig;
+using OliverBooth.Extensions.Markdig.Markdown.Callout;
+using OliverBooth.Extensions.Markdig.Markdown.Template;
+using OliverBooth.Extensions.Markdig.Services;
+
+namespace OliverBooth.Extensions.Markdig;
+
+///
+/// Extension methods for .
+///
+public static class MarkdownPipelineExtensions
+{
+ ///
+ /// Enables the use of Obsidian-style callouts in this pipeline.
+ ///
+ /// The Markdig markdown pipeline builder.
+ /// The modified Markdig markdown pipeline builder.
+ /// is .
+ public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder builder)
+ {
+ if (builder is null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Extensions.AddIfNotAlready();
+ return builder;
+ }
+
+ ///
+ /// Enables the use of Wiki-style templates in this pipeline.
+ ///
+ /// The Markdig markdown pipeline builder.
+ /// The template service responsible for fetching and rendering templates.
+ /// The modified Markdig markdown pipeline builder.
+ ///
+ /// is .
+ /// -or-
+ /// is .
+ ///
+ public static MarkdownPipelineBuilder UseTemplates(this MarkdownPipelineBuilder builder, ITemplateService templateService)
+ {
+ if (builder is null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (templateService is null)
+ {
+ throw new ArgumentNullException(nameof(templateService));
+ }
+
+ builder.Use(new TemplateExtension(templateService));
+ return builder;
+ }
+}
diff --git a/OliverBooth.Extensions.Markdig/OliverBooth.Extensions.Markdig.csproj b/OliverBooth.Extensions.Markdig/OliverBooth.Extensions.Markdig.csproj
new file mode 100644
index 0000000..5f35f98
--- /dev/null
+++ b/OliverBooth.Extensions.Markdig/OliverBooth.Extensions.Markdig.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OliverBooth/Services/ITemplateService.cs b/OliverBooth.Extensions.Markdig/Services/ITemplateService.cs
similarity index 94%
rename from OliverBooth/Services/ITemplateService.cs
rename to OliverBooth.Extensions.Markdig/Services/ITemplateService.cs
index 5fa9239..30c8b86 100644
--- a/OliverBooth/Services/ITemplateService.cs
+++ b/OliverBooth.Extensions.Markdig/Services/ITemplateService.cs
@@ -1,8 +1,8 @@
using System.Diagnostics.CodeAnalysis;
-using OliverBooth.Data.Web;
-using OliverBooth.Markdown.Template;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Extensions.Markdig.Markdown.Template;
-namespace OliverBooth.Services;
+namespace OliverBooth.Extensions.Markdig.Services;
///
/// Represents a service that renders MediaWiki-style templates.
diff --git a/OliverBooth/Formatting/DateFormatter.cs b/OliverBooth.Extensions.SmartFormat/DateFormatter.cs
similarity index 95%
rename from OliverBooth/Formatting/DateFormatter.cs
rename to OliverBooth.Extensions.SmartFormat/DateFormatter.cs
index 6109cab..7b98f26 100644
--- a/OliverBooth/Formatting/DateFormatter.cs
+++ b/OliverBooth.Extensions.SmartFormat/DateFormatter.cs
@@ -1,7 +1,7 @@
using System.Globalization;
using SmartFormat.Core.Extensions;
-namespace OliverBooth.Formatting;
+namespace OliverBooth.Extensions.SmartFormat;
///
/// Represents a SmartFormat formatter that formats a date.
diff --git a/OliverBooth/Formatting/MarkdownFormatter.cs b/OliverBooth.Extensions.SmartFormat/MarkdownFormatter.cs
similarity index 92%
rename from OliverBooth/Formatting/MarkdownFormatter.cs
rename to OliverBooth.Extensions.SmartFormat/MarkdownFormatter.cs
index c682e2c..2f38c96 100644
--- a/OliverBooth/Formatting/MarkdownFormatter.cs
+++ b/OliverBooth.Extensions.SmartFormat/MarkdownFormatter.cs
@@ -1,7 +1,8 @@
using Markdig;
+using Microsoft.Extensions.DependencyInjection;
using SmartFormat.Core.Extensions;
-namespace OliverBooth.Formatting;
+namespace OliverBooth.Extensions.SmartFormat;
///
/// Represents a SmartFormat formatter that formats markdown.
diff --git a/OliverBooth.Extensions.SmartFormat/OliverBooth.Extensions.SmartFormat.csproj b/OliverBooth.Extensions.SmartFormat/OliverBooth.Extensions.SmartFormat.csproj
new file mode 100644
index 0000000..af76520
--- /dev/null
+++ b/OliverBooth.Extensions.SmartFormat/OliverBooth.Extensions.SmartFormat.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OliverBooth.sln b/OliverBooth.sln
index e2ba2dd..1d052f5 100644
--- a/OliverBooth.sln
+++ b/OliverBooth.sln
@@ -13,6 +13,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
global.json = global.json
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Extensions.Markdig", "OliverBooth.Extensions.Markdig\OliverBooth.Extensions.Markdig.csproj", "{3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Common", "OliverBooth.Common\OliverBooth.Common.csproj", "{AD231E0F-FAED-4661-963F-EB22F858E148}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Extensions.SmartFormat", "OliverBooth.Extensions.SmartFormat\OliverBooth.Extensions.SmartFormat.csproj", "{9D56FA9B-B95B-460D-8745-41AABAA8BF61}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -23,6 +29,18 @@ Global
{A58A6FA3-480C-400B-822A-3786741BF39C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AD231E0F-FAED-4661-963F-EB22F858E148}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AD231E0F-FAED-4661-963F-EB22F858E148}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AD231E0F-FAED-4661-963F-EB22F858E148}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AD231E0F-FAED-4661-963F-EB22F858E148}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9D56FA9B-B95B-460D-8745-41AABAA8BF61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9D56FA9B-B95B-460D-8745-41AABAA8BF61}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9D56FA9B-B95B-460D-8745-41AABAA8BF61}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9D56FA9B-B95B-460D-8745-41AABAA8BF61}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
diff --git a/OliverBooth/Controllers/Blog/BlogApiController.cs b/OliverBooth/Controllers/Blog/BlogApiController.cs
deleted file mode 100644
index 21eb070..0000000
--- a/OliverBooth/Controllers/Blog/BlogApiController.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using Humanizer;
-using Microsoft.AspNetCore.Mvc;
-using OliverBooth.Data.Blog;
-using OliverBooth.Services;
-
-namespace OliverBooth.Controllers.Blog;
-
-///
-/// Represents a controller for the blog API.
-///
-[ApiController]
-[Route("api/blog")]
-[Produces("application/json")]
-public sealed class BlogApiController : ControllerBase
-{
- private readonly IBlogPostService _blogPostService;
- private readonly IBlogUserService _userService;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The .
- /// The .
- public BlogApiController(IBlogPostService blogPostService, IBlogUserService userService)
- {
- _blogPostService = blogPostService;
- _userService = userService;
- }
-
- [Route("count")]
- public IActionResult Count()
- {
- return Ok(new { count = _blogPostService.GetBlogPostCount() });
- }
-
- [HttpGet("posts/{page:int?}")]
- public IActionResult GetAllBlogPosts(int page = 0)
- {
- const int itemsPerPage = 10;
- IReadOnlyList allPosts = _blogPostService.GetBlogPosts(page, itemsPerPage);
- return Ok(allPosts.Select(post => CreatePostObject(post)));
- }
-
- [HttpGet("posts/tagged/{tag}/{page:int?}")]
- public IActionResult GetTaggedBlogPosts(string tag, int page = 0)
- {
- const int itemsPerPage = 10;
- tag = tag.Replace('-', ' ').ToLowerInvariant();
-
- IReadOnlyList allPosts = _blogPostService.GetBlogPosts(page, itemsPerPage);
- allPosts = allPosts.Where(post => post.Tags.Contains(tag)).ToList();
- return Ok(allPosts.Select(post => CreatePostObject(post)));
- }
-
- [HttpGet("author/{id:guid}")]
- public IActionResult GetAuthor(Guid id)
- {
- if (!_userService.TryGetUser(id, out IUser? author)) return NotFound();
-
- return Ok(new
- {
- id = author.Id,
- name = author.DisplayName,
- avatarUrl = author.AvatarUrl,
- });
- }
-
- [HttpGet("post/{id:guid?}")]
- public IActionResult GetPost(Guid id)
- {
- if (!_blogPostService.TryGetPost(id, out IBlogPost? post)) return NotFound();
- return Ok(CreatePostObject(post, true));
- }
-
- private object CreatePostObject(IBlogPost post, bool includeContent = false)
- {
- return new
- {
- id = post.Id,
- commentsEnabled = post.EnableComments,
- identifier = post.GetDisqusIdentifier(),
- author = post.Author.Id,
- title = post.Title,
- published = post.Published.ToUnixTimeSeconds(),
- updated = post.Updated?.ToUnixTimeSeconds(),
- formattedPublishDate = post.Published.ToString("dddd, d MMMM yyyy HH:mm"),
- formattedUpdateDate = post.Updated?.ToString("dddd, d MMMM yyyy HH:mm"),
- humanizedTimestamp = post.Updated?.Humanize() ?? post.Published.Humanize(),
- excerpt = _blogPostService.RenderExcerpt(post, out bool trimmed),
- content = includeContent ? _blogPostService.RenderPost(post) : null,
- trimmed,
- tags = post.Tags.Select(t => t.Replace(' ', '-')),
- url = new
- {
- year = post.Published.ToString("yyyy"),
- month = post.Published.ToString("MM"),
- day = post.Published.ToString("dd"),
- slug = post.Slug
- }
- };
- }
-}
diff --git a/OliverBooth/Controllers/Blog/RssController.cs b/OliverBooth/Controllers/Blog/RssController.cs
index 83b1a08..04cd16d 100644
--- a/OliverBooth/Controllers/Blog/RssController.cs
+++ b/OliverBooth/Controllers/Blog/RssController.cs
@@ -1,8 +1,8 @@
using System.Xml.Serialization;
using Microsoft.AspNetCore.Mvc;
-using OliverBooth.Data.Blog;
+using OliverBooth.Common.Data.Blog;
+using OliverBooth.Common.Services;
using OliverBooth.Data.Blog.Rss;
-using OliverBooth.Services;
namespace OliverBooth.Controllers.Blog;
diff --git a/OliverBooth/Controllers/FormattedBlacklist.cs b/OliverBooth/Controllers/FormattedBlacklist.cs
index bce6143..5186ec9 100644
--- a/OliverBooth/Controllers/FormattedBlacklist.cs
+++ b/OliverBooth/Controllers/FormattedBlacklist.cs
@@ -1,7 +1,7 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
-using OliverBooth.Data.Web;
-using OliverBooth.Services;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Common.Services;
namespace OliverBooth.Controllers;
diff --git a/OliverBooth/Data/Blog/BlogPost.cs b/OliverBooth/Data/Blog/BlogPost.cs
index 59067b5..f84292a 100644
--- a/OliverBooth/Data/Blog/BlogPost.cs
+++ b/OliverBooth/Data/Blog/BlogPost.cs
@@ -1,4 +1,6 @@
using System.ComponentModel.DataAnnotations.Schema;
+using OliverBooth.Common.Data;
+using OliverBooth.Common.Data.Blog;
using SmartFormat;
namespace OliverBooth.Data.Blog;
diff --git a/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs b/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs
index 1ecde2f..fd7085b 100644
--- a/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs
+++ b/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using OliverBooth.Common.Data;
namespace OliverBooth.Data.Blog.Configuration;
diff --git a/OliverBooth/Data/Blog/LegacyComment.cs b/OliverBooth/Data/Blog/LegacyComment.cs
index a020979..cd8b615 100644
--- a/OliverBooth/Data/Blog/LegacyComment.cs
+++ b/OliverBooth/Data/Blog/LegacyComment.cs
@@ -1,4 +1,5 @@
using System.Web;
+using OliverBooth.Common.Data.Blog;
namespace OliverBooth.Data.Blog;
diff --git a/OliverBooth/Data/Blog/User.cs b/OliverBooth/Data/Blog/User.cs
index dcd50ab..1b6ce40 100644
--- a/OliverBooth/Data/Blog/User.cs
+++ b/OliverBooth/Data/Blog/User.cs
@@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Security.Cryptography;
using System.Text;
using Cysharp.Text;
+using OliverBooth.Common.Data.Blog;
namespace OliverBooth.Data.Blog;
diff --git a/OliverBooth/Data/Mastodon/MastodonStatus.cs b/OliverBooth/Data/Mastodon/MastodonStatus.cs
index a4d3ebf..4276c02 100644
--- a/OliverBooth/Data/Mastodon/MastodonStatus.cs
+++ b/OliverBooth/Data/Mastodon/MastodonStatus.cs
@@ -1,34 +1,24 @@
using System.Text.Json.Serialization;
+using OliverBooth.Common.Data.Mastodon;
namespace OliverBooth.Data.Mastodon;
-public sealed class MastodonStatus
+///
+internal sealed class MastodonStatus : IMastodonStatus
{
- ///
- /// Gets the content of the status.
- ///
- /// The content.
+ ///
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
- ///
- /// Gets the date and time at which this status was posted.
- ///
- /// The post timestamp.
+ ///
[JsonPropertyName("created_at")]
public DateTimeOffset CreatedAt { get; set; }
- ///
- /// Gets the media attachments for this status.
- ///
- /// The media attachments.
+ ///
[JsonPropertyName("media_attachments")]
- public IReadOnlyList MediaAttachments { get; set; } = ArraySegment.Empty;
+ public IReadOnlyList MediaAttachments { get; set; } = ArraySegment.Empty;
- ///
- /// Gets the original URI of the status.
- ///
- /// The original URI.
+ ///
[JsonPropertyName("url")]
public Uri OriginalUri { get; set; } = null!;
}
diff --git a/OliverBooth/Data/Web/BlacklistEntry.cs b/OliverBooth/Data/Web/BlacklistEntry.cs
index 374d9c2..a0f68d3 100644
--- a/OliverBooth/Data/Web/BlacklistEntry.cs
+++ b/OliverBooth/Data/Web/BlacklistEntry.cs
@@ -1,3 +1,5 @@
+using OliverBooth.Common.Data.Web;
+
namespace OliverBooth.Data.Web;
///
diff --git a/OliverBooth/Data/Web/Book.cs b/OliverBooth/Data/Web/Book.cs
index 278aa4e..10e41dd 100644
--- a/OliverBooth/Data/Web/Book.cs
+++ b/OliverBooth/Data/Web/Book.cs
@@ -1,4 +1,5 @@
using NetBarcode;
+using OliverBooth.Common.Data.Web;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Processing;
diff --git a/OliverBooth/Data/Web/CodeSnippet.cs b/OliverBooth/Data/Web/CodeSnippet.cs
index 19d29c1..13fb751 100644
--- a/OliverBooth/Data/Web/CodeSnippet.cs
+++ b/OliverBooth/Data/Web/CodeSnippet.cs
@@ -1,3 +1,5 @@
+using OliverBooth.Common.Data.Web;
+
namespace OliverBooth.Data.Web;
///
diff --git a/OliverBooth/Data/Web/Configuration/BookConfiguration.cs b/OliverBooth/Data/Web/Configuration/BookConfiguration.cs
index f27a1d2..fca84f9 100644
--- a/OliverBooth/Data/Web/Configuration/BookConfiguration.cs
+++ b/OliverBooth/Data/Web/Configuration/BookConfiguration.cs
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using OliverBooth.Common.Data.Web;
namespace OliverBooth.Data.Web.Configuration;
diff --git a/OliverBooth/Data/Web/Configuration/ProjectConfiguration.cs b/OliverBooth/Data/Web/Configuration/ProjectConfiguration.cs
index 08ef5d9..8949763 100644
--- a/OliverBooth/Data/Web/Configuration/ProjectConfiguration.cs
+++ b/OliverBooth/Data/Web/Configuration/ProjectConfiguration.cs
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using OliverBooth.Common.Data.Web;
namespace OliverBooth.Data.Web.Configuration;
diff --git a/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs b/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs
index b86a5b5..4caf190 100644
--- a/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs
+++ b/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using OliverBooth.Common.Data;
namespace OliverBooth.Data.Web.Configuration;
diff --git a/OliverBooth/Data/Web/Configuration/TutorialFolderConfiguration.cs b/OliverBooth/Data/Web/Configuration/TutorialFolderConfiguration.cs
index af1eef2..136d9c3 100644
--- a/OliverBooth/Data/Web/Configuration/TutorialFolderConfiguration.cs
+++ b/OliverBooth/Data/Web/Configuration/TutorialFolderConfiguration.cs
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using OliverBooth.Common.Data;
namespace OliverBooth.Data.Web.Configuration;
diff --git a/OliverBooth/Data/Web/ProgrammingLanguage.cs b/OliverBooth/Data/Web/ProgrammingLanguage.cs
index 53746d9..9a4135f 100644
--- a/OliverBooth/Data/Web/ProgrammingLanguage.cs
+++ b/OliverBooth/Data/Web/ProgrammingLanguage.cs
@@ -1,3 +1,5 @@
+using OliverBooth.Common.Data.Web;
+
namespace OliverBooth.Data.Web;
///
diff --git a/OliverBooth/Data/Web/Project.cs b/OliverBooth/Data/Web/Project.cs
index 05761a7..37b6b9e 100644
--- a/OliverBooth/Data/Web/Project.cs
+++ b/OliverBooth/Data/Web/Project.cs
@@ -1,3 +1,5 @@
+using OliverBooth.Common.Data.Web;
+
namespace OliverBooth.Data.Web;
///
diff --git a/OliverBooth/Data/Web/Template.cs b/OliverBooth/Data/Web/Template.cs
index cf029a1..29355c5 100644
--- a/OliverBooth/Data/Web/Template.cs
+++ b/OliverBooth/Data/Web/Template.cs
@@ -1,3 +1,5 @@
+using OliverBooth.Common.Data.Web;
+
namespace OliverBooth.Data.Web;
///
diff --git a/OliverBooth/Data/Web/TutorialArticle.cs b/OliverBooth/Data/Web/TutorialArticle.cs
index 43ef096..a1f9801 100644
--- a/OliverBooth/Data/Web/TutorialArticle.cs
+++ b/OliverBooth/Data/Web/TutorialArticle.cs
@@ -1,4 +1,6 @@
using System.ComponentModel.DataAnnotations.Schema;
+using OliverBooth.Common.Data;
+using OliverBooth.Common.Data.Web;
namespace OliverBooth.Data.Web;
diff --git a/OliverBooth/Data/Web/TutorialFolder.cs b/OliverBooth/Data/Web/TutorialFolder.cs
index 26c5bc1..1f69ef6 100644
--- a/OliverBooth/Data/Web/TutorialFolder.cs
+++ b/OliverBooth/Data/Web/TutorialFolder.cs
@@ -1,3 +1,6 @@
+using OliverBooth.Common.Data;
+using OliverBooth.Common.Data.Web;
+
namespace OliverBooth.Data.Web;
///
diff --git a/OliverBooth/Extensions/HtmlUtility.cs b/OliverBooth/Extensions/HtmlUtility.cs
index 94e3d6a..556cb6d 100644
--- a/OliverBooth/Extensions/HtmlUtility.cs
+++ b/OliverBooth/Extensions/HtmlUtility.cs
@@ -1,8 +1,8 @@
using System.Web;
using Cysharp.Text;
-using OliverBooth.Data.Blog;
-using OliverBooth.Data.Web;
-using OliverBooth.Services;
+using OliverBooth.Common.Data.Blog;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Common.Services;
namespace OliverBooth.Extensions;
diff --git a/OliverBooth/Markdown/MarkdownExtensions.cs b/OliverBooth/Markdown/MarkdownExtensions.cs
deleted file mode 100644
index d63d869..0000000
--- a/OliverBooth/Markdown/MarkdownExtensions.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Markdig;
-using OliverBooth.Markdown.Callout;
-
-namespace OliverBooth.Markdown;
-
-///
-/// Extension methods for .
-///
-internal static class MarkdownExtensions
-{
- ///
- /// Uses this extension to enable Obsidian-style callouts.
- ///
- /// The pipeline.
- /// The modified pipeline.
- public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder pipeline)
- {
- pipeline.Extensions.AddIfNotAlready();
- return pipeline;
- }
-}
diff --git a/OliverBooth/Markdown/Template/CodeSnippetTemplateRenderer.cs b/OliverBooth/Markdown/Template/CodeSnippetTemplateRenderer.cs
index e809801..f93abd8 100644
--- a/OliverBooth/Markdown/Template/CodeSnippetTemplateRenderer.cs
+++ b/OliverBooth/Markdown/Template/CodeSnippetTemplateRenderer.cs
@@ -1,8 +1,9 @@
using System.Diagnostics;
using System.Text;
using Markdig;
-using OliverBooth.Data.Web;
-using OliverBooth.Services;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Common.Services;
+using OliverBooth.Extensions.Markdig.Markdown.Template;
namespace OliverBooth.Markdown.Template;
diff --git a/OliverBooth/Markdown/Template/CustomTemplateRenderer.cs b/OliverBooth/Markdown/Template/CustomTemplateRenderer.cs
index 1d3f122..9fbbc2a 100644
--- a/OliverBooth/Markdown/Template/CustomTemplateRenderer.cs
+++ b/OliverBooth/Markdown/Template/CustomTemplateRenderer.cs
@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Web;
+using OliverBooth.Extensions.Markdig.Markdown.Template;
namespace OliverBooth.Markdown.Template;
diff --git a/OliverBooth/OliverBooth.csproj b/OliverBooth/OliverBooth.csproj
index 2922e7a..f822f33 100644
--- a/OliverBooth/OliverBooth.csproj
+++ b/OliverBooth/OliverBooth.csproj
@@ -30,13 +30,9 @@
-
-
-
-
@@ -45,10 +41,20 @@
-
-
+
+
+
+
+
+
+
+
+
+
+ _PageTabs.cshtml
+
diff --git a/OliverBooth/Pages/Blog/Article.cshtml b/OliverBooth/Pages/Blog/Article.cshtml
index bdc3ee5..3300621 100644
--- a/OliverBooth/Pages/Blog/Article.cshtml
+++ b/OliverBooth/Pages/Blog/Article.cshtml
@@ -1,9 +1,10 @@
@page "/blog/{year:int}/{month:int}/{day:int}/{slug}"
@using Humanizer
@using Markdig
-@using OliverBooth.Data
-@using OliverBooth.Data.Blog
-@using OliverBooth.Services
+@using Microsoft.AspNetCore.Mvc.TagHelpers
+@using OliverBooth.Common.Data
+@using OliverBooth.Common.Data.Blog
+@using OliverBooth.Common.Services
@inject IBlogPostService BlogPostService
@inject MarkdownPipeline MarkdownPipeline
@model Article
@@ -77,12 +78,7 @@
}
-
- @foreach (string tag in post.Tags)
- {
-
@tag
- }
-
+
@@ -91,6 +87,18 @@
+
+
+
+ @foreach (string tag in post.Tags)
+ {
+
+ @tag
+
+ }
+
+
+
@if (BlogPostService.GetPreviousPost(post) is { } previousPost)
diff --git a/OliverBooth/Pages/Blog/Article.cshtml.cs b/OliverBooth/Pages/Blog/Article.cshtml.cs
index 17a9337..92315be 100644
--- a/OliverBooth/Pages/Blog/Article.cshtml.cs
+++ b/OliverBooth/Pages/Blog/Article.cshtml.cs
@@ -1,8 +1,8 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Primitives;
-using OliverBooth.Data.Blog;
-using OliverBooth.Services;
+using OliverBooth.Common.Data.Blog;
+using OliverBooth.Common.Services;
using BC = BCrypt.Net.BCrypt;
namespace OliverBooth.Pages.Blog;
@@ -79,7 +79,6 @@ public class Article : PageModel
var date = new DateOnly(year, month, day);
if (!_blogPostService.TryGetPost(date, slug, out IBlogPost? post))
{
- Response.StatusCode = 404;
return NotFound();
}
diff --git a/OliverBooth/Pages/Blog/Index.cshtml b/OliverBooth/Pages/Blog/Index.cshtml
index 0229989..876e06f 100644
--- a/OliverBooth/Pages/Blog/Index.cshtml
+++ b/OliverBooth/Pages/Blog/Index.cshtml
@@ -1,75 +1,26 @@
@page
-@using Humanizer
-@using OliverBooth.Data.Mastodon
-@using OliverBooth.Services
+@using OliverBooth.Common.Data
+@using OliverBooth.Common.Data.Blog
+@using OliverBooth.Common.Services
@model Index
-@inject IMastodonService MastodonService
+@inject IBlogPostService BlogPostService
@{
ViewData["Title"] = "Blog";
- MastodonStatus latestStatus = MastodonService.GetLatestStatus();
}
-
-
- @Html.Raw(latestStatus.Content)
- @foreach (MediaAttachment attachment in latestStatus.MediaAttachments)
- {
- switch (attachment.Type)
- {
- case AttachmentType.Audio:
-
- break;
-
- case AttachmentType.Video:
-
- break;
-
- case AttachmentType.Image:
- case AttachmentType.GifV:
-
- break;
- }
- }
-
-
-
+@await Html.PartialAsync("Partials/_MastodonStatus")
- @await Html.PartialAsync("_LoadingSpinner")
+ @foreach (IBlogPost post in BlogPostService.GetBlogPosts(0))
+ {
+ @await Html.PartialAsync("Partials/_BlogCard", post)
+ }
-
\ No newline at end of file
+@await Html.PartialAsync("Partials/_PageTabs", new ViewDataDictionary(ViewData)
+{
+ ["UrlRoot"] = "/blog",
+ ["Page"] = 1,
+ ["PageCount"] = BlogPostService.GetPageCount(visibility: Visibility.Published)
+})
\ No newline at end of file
diff --git a/OliverBooth/Pages/Blog/Index.cshtml.cs b/OliverBooth/Pages/Blog/Index.cshtml.cs
index 97104b6..378ceb2 100644
--- a/OliverBooth/Pages/Blog/Index.cshtml.cs
+++ b/OliverBooth/Pages/Blog/Index.cshtml.cs
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
-using OliverBooth.Data.Blog;
-using OliverBooth.Services;
+using OliverBooth.Common.Data.Blog;
+using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Blog;
@@ -36,7 +36,7 @@ public class Index : PageModel
return _blogPostService.TryGetPost(wpPostId, out IBlogPost? post) ? RedirectToPost(post) : NotFound();
}
- private IActionResult RedirectToPost(IBlogPost post)
+ private RedirectResult RedirectToPost(IBlogPost post)
{
var route = new
{
diff --git a/OliverBooth/Pages/Blog/List.cshtml b/OliverBooth/Pages/Blog/List.cshtml
new file mode 100644
index 0000000..61fb3d8
--- /dev/null
+++ b/OliverBooth/Pages/Blog/List.cshtml
@@ -0,0 +1,23 @@
+@page "/blog/page/{pageNumber:int}"
+@model List
+@using OliverBooth.Common.Data
+@using OliverBooth.Common.Data.Blog
+@using OliverBooth.Common.Services
+
+@inject IBlogPostService BlogPostService
+
+@await Html.PartialAsync("Partials/_MastodonStatus")
+
+
+ @foreach (IBlogPost post in BlogPostService.GetBlogPosts(Model.PageNumber))
+ {
+ @await Html.PartialAsync("Partials/_BlogCard", post)
+ }
+
+
+@await Html.PartialAsync("Partials/_PageTabs", new ViewDataDictionary(ViewData)
+{
+ ["UrlRoot"] = "/blog",
+ ["Page"] = Model.PageNumber,
+ ["PageCount"] = BlogPostService.GetPageCount(visibility: Visibility.Published)
+})
\ No newline at end of file
diff --git a/OliverBooth/Pages/Blog/List.cshtml.cs b/OliverBooth/Pages/Blog/List.cshtml.cs
new file mode 100644
index 0000000..f0aa6dd
--- /dev/null
+++ b/OliverBooth/Pages/Blog/List.cshtml.cs
@@ -0,0 +1,32 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace OliverBooth.Pages.Blog;
+
+///
+/// Represents a class which defines the model for the /blog/page/# route.
+///
+public class List : PageModel
+{
+ ///
+ /// Gets the requested page number.
+ ///
+ ///
The requested page number.
+ public int PageNumber { get; private set; }
+
+ ///
+ /// Handles the incoming GET request to the page.
+ ///
+ ///
The requested page number, starting from 1.
+ ///
+ public IActionResult OnGet([FromRoute(Name = "pageNumber")] int page = 1)
+ {
+ if (page < 2)
+ {
+ return RedirectToPage("Index");
+ }
+
+ PageNumber = page;
+ return Page();
+ }
+}
diff --git a/OliverBooth/Pages/Blog/RawArticle.cshtml.cs b/OliverBooth/Pages/Blog/RawArticle.cshtml.cs
index 1fc34eb..adbd374 100644
--- a/OliverBooth/Pages/Blog/RawArticle.cshtml.cs
+++ b/OliverBooth/Pages/Blog/RawArticle.cshtml.cs
@@ -1,8 +1,8 @@
using Cysharp.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
-using OliverBooth.Data.Blog;
-using OliverBooth.Services;
+using OliverBooth.Common.Data.Blog;
+using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Blog;
diff --git a/OliverBooth/Pages/Books.cshtml b/OliverBooth/Pages/Books.cshtml
index dd83d7e..f7bbb79 100644
--- a/OliverBooth/Pages/Books.cshtml
+++ b/OliverBooth/Pages/Books.cshtml
@@ -1,5 +1,5 @@
@page
-@using OliverBooth.Data.Web
+@using OliverBooth.Common.Data.Web
@model OliverBooth.Pages.Books
@{
ViewData["Title"] = "Reading List";
diff --git a/OliverBooth/Pages/Books.cshtml.cs b/OliverBooth/Pages/Books.cshtml.cs
index 6cbc5af..6668ed3 100644
--- a/OliverBooth/Pages/Books.cshtml.cs
+++ b/OliverBooth/Pages/Books.cshtml.cs
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
-using OliverBooth.Data.Web;
-using OliverBooth.Services;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Common.Services;
namespace OliverBooth.Pages;
diff --git a/OliverBooth/Pages/Contact/Blacklist.cshtml b/OliverBooth/Pages/Contact/Blacklist.cshtml
index a0101ac..20feb7a 100644
--- a/OliverBooth/Pages/Contact/Blacklist.cshtml
+++ b/OliverBooth/Pages/Contact/Blacklist.cshtml
@@ -1,6 +1,7 @@
@page
-@using OliverBooth.Data.Web
-@using OliverBooth.Services
+@using Microsoft.AspNetCore.Mvc.TagHelpers
+@using OliverBooth.Common.Data.Web
+@using OliverBooth.Common.Services
@inject IContactService ContactService
@{
ViewData["Title"] = "Blacklist";
diff --git a/OliverBooth/Pages/Contact/Index.cshtml b/OliverBooth/Pages/Contact/Index.cshtml
index 12d5f70..95bdd86 100644
--- a/OliverBooth/Pages/Contact/Index.cshtml
+++ b/OliverBooth/Pages/Contact/Index.cshtml
@@ -1,4 +1,5 @@
@page
+@using Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "Contact";
}
diff --git a/OliverBooth/Pages/Contact/Result.cshtml b/OliverBooth/Pages/Contact/Result.cshtml
index 65de504..bc5b79d 100644
--- a/OliverBooth/Pages/Contact/Result.cshtml
+++ b/OliverBooth/Pages/Contact/Result.cshtml
@@ -1,4 +1,5 @@
@page
+@using Microsoft.AspNetCore.Mvc.TagHelpers
@model OliverBooth.Pages.Contact.Result
@{
diff --git a/OliverBooth/Pages/Error.cshtml b/OliverBooth/Pages/Error.cshtml
deleted file mode 100644
index b7d6b09..0000000
--- a/OliverBooth/Pages/Error.cshtml
+++ /dev/null
@@ -1,34 +0,0 @@
-@page "/error/{code:int?}"
-@model OliverBooth.Pages.ErrorModel
-@{
- Layout = "_MinimalLayout";
- ViewData["Title"] = "Error";
-}
-
-
- @switch (Model.HttpStatusCode)
- {
- case 403:
- 403 Forbidden
- break;
-
- case 404:
- 404 Page not found
- break;
-
- case 500:
- Internal server error
- break;
-
- default:
- Something went wrong
- break;
- }
-
-
-@if (Model.ShowRequestId)
-{
-
- Request ID: @Model.RequestId
-
-}
\ No newline at end of file
diff --git a/OliverBooth/Pages/Error.cshtml.cs b/OliverBooth/Pages/Error.cshtml.cs
deleted file mode 100644
index 04f1916..0000000
--- a/OliverBooth/Pages/Error.cshtml.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System.Diagnostics;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.RazorPages;
-
-namespace OliverBooth.Pages;
-
-[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
-[IgnoreAntiforgeryToken]
-public class ErrorModel : PageModel
-{
- public string? RequestId { get; set; }
-
- public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
-
- public int HttpStatusCode { get; private set; }
-
- public IActionResult OnGet(int? code = null)
- {
- HttpStatusCode = code ?? HttpContext.Response.StatusCode;
- if (HttpStatusCode == 200)
- {
- return RedirectToPage("/Index");
- }
-
- RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
- return Page();
- }
-}
diff --git a/OliverBooth/Pages/Error/BadRequest.cshtml b/OliverBooth/Pages/Error/BadRequest.cshtml
new file mode 100644
index 0000000..d1e3fbc
--- /dev/null
+++ b/OliverBooth/Pages/Error/BadRequest.cshtml
@@ -0,0 +1,17 @@
+@page "/error/400"
+
+
+
+
+
400 Bad Request
+
+
+
Received invalid request message. Check your request and try again.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OliverBooth/Pages/Error/Forbidden.cshtml b/OliverBooth/Pages/Error/Forbidden.cshtml
new file mode 100644
index 0000000..2fd5597
--- /dev/null
+++ b/OliverBooth/Pages/Error/Forbidden.cshtml
@@ -0,0 +1,15 @@
+@page "/error/403"
+
+
+
+
403 Forbidden
+
+
+
+
+
+
+
Access to the requested page is forbidden.
+
+
+
\ No newline at end of file
diff --git a/OliverBooth/Pages/Error/GatewayTimeout.cshtml b/OliverBooth/Pages/Error/GatewayTimeout.cshtml
new file mode 100644
index 0000000..b44d160
--- /dev/null
+++ b/OliverBooth/Pages/Error/GatewayTimeout.cshtml
@@ -0,0 +1,17 @@
+@page "/error/504"
+
+
+
+
+
+
+
+
+
504 Gateway Timeout
+
+
+
The server is slacking. Give it more coffee.
+
+
+
+
\ No newline at end of file
diff --git a/OliverBooth/Pages/Error/Gone.cshtml b/OliverBooth/Pages/Error/Gone.cshtml
new file mode 100644
index 0000000..dc5084a
--- /dev/null
+++ b/OliverBooth/Pages/Error/Gone.cshtml
@@ -0,0 +1,17 @@
+@page "/error/400"
+
+
+
+
+
410 Gone
+
+
+
The requested page has mysteriously disappeared.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OliverBooth/Pages/Error/ImATeapot.cshtml b/OliverBooth/Pages/Error/ImATeapot.cshtml
new file mode 100644
index 0000000..c53f758
--- /dev/null
+++ b/OliverBooth/Pages/Error/ImATeapot.cshtml
@@ -0,0 +1,17 @@
+@page "/error/418"
+
+
+
+
+
+
+
+
+
418 I'm A Teapot
+
+
+
No coffee available. I am only capable of brewing tea.
+
+
+
+
\ No newline at end of file
diff --git a/OliverBooth/Pages/Error/InternalServerError.cshtml b/OliverBooth/Pages/Error/InternalServerError.cshtml
new file mode 100644
index 0000000..27b4605
--- /dev/null
+++ b/OliverBooth/Pages/Error/InternalServerError.cshtml
@@ -0,0 +1,17 @@
+@page "/error/500"
+
+
+
+
+
+
+
+
+
500 Internal Server Error
+
+
+
This is my fault, not yours.
+
+
+
+
\ No newline at end of file
diff --git a/OliverBooth/Pages/Error/NotFound.cshtml b/OliverBooth/Pages/Error/NotFound.cshtml
new file mode 100644
index 0000000..cb54df4
--- /dev/null
+++ b/OliverBooth/Pages/Error/NotFound.cshtml
@@ -0,0 +1,17 @@
+@page "/error/404"
+
+
+
+
+
+
+
+
+
404 Not Found
+
+
+
The requested page could not be found.
+
+
+
+
\ No newline at end of file
diff --git a/OliverBooth/Pages/Error/ServiceUnavailable.cshtml b/OliverBooth/Pages/Error/ServiceUnavailable.cshtml
new file mode 100644
index 0000000..a33a4de
--- /dev/null
+++ b/OliverBooth/Pages/Error/ServiceUnavailable.cshtml
@@ -0,0 +1,17 @@
+@page "/error/503"
+
+
+
+
+
503 Service Unavailable
+
+
+
The server is currently unable to process your request. Please try again later.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OliverBooth/Pages/Error/TooManyRequests.cshtml b/OliverBooth/Pages/Error/TooManyRequests.cshtml
new file mode 100644
index 0000000..046b860
--- /dev/null
+++ b/OliverBooth/Pages/Error/TooManyRequests.cshtml
@@ -0,0 +1,17 @@
+@page "/error/429"
+
+
+
+
+
429 Too Many Requests
+
+
+
You are being rate limited.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OliverBooth/Pages/Index.cshtml b/OliverBooth/Pages/Index.cshtml
index 0f85b37..89821cc 100644
--- a/OliverBooth/Pages/Index.cshtml
+++ b/OliverBooth/Pages/Index.cshtml
@@ -1,4 +1,5 @@
@page
+@using Microsoft.AspNetCore.Mvc.TagHelpers
diff --git a/OliverBooth/Pages/Privacy/FiveOClockSomewhere.cshtml b/OliverBooth/Pages/Privacy/FiveOClockSomewhere.cshtml
index 5c5365a..07fe6a5 100644
--- a/OliverBooth/Pages/Privacy/FiveOClockSomewhere.cshtml
+++ b/OliverBooth/Pages/Privacy/FiveOClockSomewhere.cshtml
@@ -1,4 +1,5 @@
@page "/privacy/five-oclock-somewhere"
+@using Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "It's 5 O'Clock Somewhere Privacy Policy";
}
diff --git a/OliverBooth/Pages/Privacy/GooglePlay.cshtml b/OliverBooth/Pages/Privacy/GooglePlay.cshtml
index 37caca9..9d340c5 100644
--- a/OliverBooth/Pages/Privacy/GooglePlay.cshtml
+++ b/OliverBooth/Pages/Privacy/GooglePlay.cshtml
@@ -1,4 +1,5 @@
@page "/privacy/google-play"
+@using Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "Google Play Privacy Policy";
}
diff --git a/OliverBooth/Pages/Privacy/Index.cshtml b/OliverBooth/Pages/Privacy/Index.cshtml
index d4afbd3..d5ef608 100644
--- a/OliverBooth/Pages/Privacy/Index.cshtml
+++ b/OliverBooth/Pages/Privacy/Index.cshtml
@@ -1,4 +1,5 @@
@page
+@using Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "Privacy Policy";
}
diff --git a/OliverBooth/Pages/Projects/Index.cshtml b/OliverBooth/Pages/Projects/Index.cshtml
index 90468e5..a9e8f77 100644
--- a/OliverBooth/Pages/Projects/Index.cshtml
+++ b/OliverBooth/Pages/Projects/Index.cshtml
@@ -1,6 +1,7 @@
@page
-@using OliverBooth.Data.Web
-@using OliverBooth.Services
+@using Microsoft.AspNetCore.Mvc.TagHelpers
+@using OliverBooth.Common.Data.Web
+@using OliverBooth.Common.Services
@inject IProjectService ProjectService
@{
ViewData["Title"] = "Projects";
diff --git a/OliverBooth/Pages/Projects/Project.cshtml b/OliverBooth/Pages/Projects/Project.cshtml
index b9c9933..80141dc 100644
--- a/OliverBooth/Pages/Projects/Project.cshtml
+++ b/OliverBooth/Pages/Projects/Project.cshtml
@@ -1,7 +1,8 @@
@page "/project/{slug}"
@using Markdig
-@using OliverBooth.Data.Web
-@using OliverBooth.Services
+@using Microsoft.AspNetCore.Mvc.TagHelpers
+@using OliverBooth.Common.Data.Web
+@using OliverBooth.Common.Services
@model Project
@inject IProjectService ProjectService
@inject MarkdownPipeline MarkdownPipeline
diff --git a/OliverBooth/Pages/Projects/Project.cshtml.cs b/OliverBooth/Pages/Projects/Project.cshtml.cs
index 87782ec..a762e6d 100644
--- a/OliverBooth/Pages/Projects/Project.cshtml.cs
+++ b/OliverBooth/Pages/Projects/Project.cshtml.cs
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
-using OliverBooth.Data.Web;
-using OliverBooth.Services;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Projects;
diff --git a/OliverBooth/Pages/Shared/Partials/PageTabsUtility.cs b/OliverBooth/Pages/Shared/Partials/PageTabsUtility.cs
new file mode 100644
index 0000000..833cd77
--- /dev/null
+++ b/OliverBooth/Pages/Shared/Partials/PageTabsUtility.cs
@@ -0,0 +1,205 @@
+using Cysharp.Text;
+using HtmlAgilityPack;
+
+namespace OliverBooth.Pages.Shared.Partials;
+
+///
+/// Provides methods for displaying pagination tabs.
+///
+public class PageTabsUtility
+{
+ private string _urlRoot = string.Empty;
+
+ ///
+ /// Gets or sets the current page number.
+ ///
+ ///
The current page number.
+ public int CurrentPage { get; set; } = 1;
+
+ ///
+ /// Gets or sets the page count.
+ ///
+ ///
The page count.
+ public int PageCount { get; set; } = 1;
+
+ ///
+ /// Gets or sets the URL root.
+ ///
+ ///
The URL root.
+ public string UrlRoot
+ {
+ get => _urlRoot;
+ set => _urlRoot = string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
+ }
+
+ ///
+ /// Shows the bound chevrons for the specified bounds type.
+ ///
+ ///
The bounds type to display.
+ ///
An HTML string containing the elements representing the bound chevrons.
+ public string ShowBounds(BoundsType bounds)
+ {
+ return bounds switch
+ {
+ BoundsType.Lower => ShowLowerBound(),
+ BoundsType.Upper => ShowUpperBound(PageCount),
+ _ => string.Empty
+ };
+ }
+
+ ///
+ /// Shows the specified page tab.
+ ///
+ ///
The tab to display.
+ ///
An HTML string containing the element for the specified page tab.
+ public string ShowTab(int tab)
+ {
+ var document = new HtmlDocument();
+ HtmlNode listItem = document.CreateElement("li");
+ HtmlNode pageLink;
+ listItem.AddClass("page-item");
+
+ switch (tab)
+ {
+ case 0:
+ // show ... to indicate truncation
+ pageLink = document.CreateElement("span");
+ pageLink.InnerHtml = "...";
+ break;
+
+ case var _ when CurrentPage == tab:
+ listItem.AddClass("active");
+ listItem.SetAttributeValue("aria-current", "page");
+
+ pageLink = document.CreateElement("span");
+ pageLink.InnerHtml = tab.ToString();
+ break;
+
+ default:
+ pageLink = document.CreateElement("a");
+ pageLink.SetAttributeValue("href", GetLinkForPage(tab));
+ pageLink.InnerHtml = tab.ToString();
+ break;
+ }
+
+ pageLink.AddClass("page-link");
+ listItem.AppendChild(pageLink);
+
+ document.DocumentNode.AppendChild(listItem);
+ return document.DocumentNode.InnerHtml;
+ }
+
+ ///
+ /// Shows the paginated tab window.
+ ///
+ ///
An HTML string representing the page tabs.
+ public string ShowTabWindow()
+ {
+ using Utf16ValueStringBuilder builder = ZString.CreateStringBuilder();
+
+ int windowLowerBound = Math.Max(CurrentPage - 2, 1);
+ int windowUpperBound = Math.Min(CurrentPage + 2, PageCount);
+
+ if (windowLowerBound > 2)
+ {
+ // show lower truncation ...
+ builder.AppendLine(ShowTab(0));
+ }
+
+ for (int pageIndex = windowLowerBound; pageIndex <= windowUpperBound; pageIndex++)
+ {
+ if (pageIndex == 1 || pageIndex == PageCount)
+ {
+ // don't show bounds, these are explicitly written
+ continue;
+ }
+
+ builder.AppendLine(ShowTab(pageIndex));
+ }
+
+ if (windowUpperBound < PageCount - 1)
+ {
+ // show upper truncation ...
+ builder.AppendLine(ShowTab(0));
+ }
+
+ return builder.ToString();
+ }
+
+ private string GetLinkForPage(int page)
+ {
+ // page 1 doesn't use /page/n route
+ return page == 1 ? _urlRoot : $"{_urlRoot}/page/{page}";
+ }
+
+ private string ShowLowerBound()
+ {
+ if (CurrentPage <= 1)
+ {
+ return string.Empty;
+ }
+
+ var document = new HtmlDocument();
+ HtmlNode listItem = document.CreateElement("li");
+ listItem.AddClass("page-item");
+
+ HtmlNode pageLink = document.CreateElement("a");
+ listItem.AppendChild(pageLink);
+ pageLink.AddClass("page-link");
+ pageLink.SetAttributeValue("href", UrlRoot);
+ pageLink.InnerHtml = "≪";
+
+ document.DocumentNode.AppendChild(listItem);
+
+ listItem = document.CreateElement("li");
+ listItem.AddClass("page-item");
+
+ pageLink = document.CreateElement("a");
+ listItem.AppendChild(pageLink);
+ pageLink.AddClass("page-link");
+ pageLink.InnerHtml = "<";
+ pageLink.SetAttributeValue("href", GetLinkForPage(CurrentPage - 1));
+
+ document.DocumentNode.AppendChild(listItem);
+
+ return document.DocumentNode.InnerHtml;
+ }
+
+ private string ShowUpperBound(int pageCount)
+ {
+ if (CurrentPage >= pageCount)
+ {
+ return string.Empty;
+ }
+
+ var document = new HtmlDocument();
+
+ HtmlNode pageLink = document.CreateElement("a");
+ pageLink.AddClass("page-link");
+ pageLink.SetAttributeValue("href", GetLinkForPage(CurrentPage + 1));
+ pageLink.InnerHtml = ">";
+
+ HtmlNode listItem = document.CreateElement("li");
+ listItem.AddClass("page-item");
+ listItem.AppendChild(pageLink);
+ document.DocumentNode.AppendChild(listItem);
+
+ pageLink = document.CreateElement("a");
+ pageLink.AddClass("page-link");
+ pageLink.SetAttributeValue("href", GetLinkForPage(pageCount));
+ pageLink.InnerHtml = "≫";
+
+ listItem = document.CreateElement("li");
+ listItem.AddClass("page-item");
+ listItem.AppendChild(pageLink);
+ document.DocumentNode.AppendChild(listItem);
+
+ return document.DocumentNode.InnerHtml;
+ }
+
+ public enum BoundsType
+ {
+ Lower,
+ Upper
+ }
+}
diff --git a/OliverBooth/Pages/Shared/Partials/_BlogCard.cshtml b/OliverBooth/Pages/Shared/Partials/_BlogCard.cshtml
new file mode 100644
index 0000000..134f78a
--- /dev/null
+++ b/OliverBooth/Pages/Shared/Partials/_BlogCard.cshtml
@@ -0,0 +1,65 @@
+@using Humanizer
+@using Microsoft.AspNetCore.Mvc.TagHelpers
+@using OliverBooth.Common.Data.Blog
+@using OliverBooth.Common.Services
+@model IBlogPost
+@inject IBlogPostService BlogPostService
+
+@{
+ IBlogAuthor author = Model.Author;
+ DateTimeOffset published = Model.Published;
+ DateTimeOffset? updated = Model.Updated;
+ DateTimeOffset time = updated ?? published;
+ string verb = updated is null ? "Published" : "Updated";
+}
+
+
+
+
+
+
+ @author.DisplayName
+ •
+ @verb @time.Humanize()
+
+
+
+ @Html.Raw(BlogPostService.RenderExcerpt(Model, out bool trimmed))
+
+
+ @if (trimmed || Model.Excerpt is not null)
+ {
+
+
+ Read more...
+
+
+ }
+
+
+
+
+
+
+ @foreach (string tag in Model.Tags)
+ {
+
+ @tag
+
+ }
+
+
+
+
\ No newline at end of file
diff --git a/OliverBooth/Pages/Shared/Partials/_MastodonStatus.cshtml b/OliverBooth/Pages/Shared/Partials/_MastodonStatus.cshtml
new file mode 100644
index 0000000..0a309ad
--- /dev/null
+++ b/OliverBooth/Pages/Shared/Partials/_MastodonStatus.cshtml
@@ -0,0 +1,42 @@
+@using Humanizer
+@using OliverBooth.Common.Data.Mastodon
+@using OliverBooth.Common.Services
+@inject IMastodonService MastodonService
+@{
+ IMastodonStatus latestStatus = MastodonService.GetLatestStatus();
+}
+
+
+
+ @Html.Raw(latestStatus.Content)
+ @foreach (MediaAttachment attachment in latestStatus.MediaAttachments)
+ {
+ switch (attachment.Type)
+ {
+ case AttachmentType.Audio:
+
+
+
+ break;
+
+ case AttachmentType.Video:
+
+
+
+ break;
+
+ case AttachmentType.Image:
+ case AttachmentType.GifV:
+
+
+
+ break;
+ }
+ }
+
+
+
diff --git a/OliverBooth/Pages/Shared/Partials/_PageTabs.cshtml b/OliverBooth/Pages/Shared/Partials/_PageTabs.cshtml
new file mode 100644
index 0000000..6293a4f
--- /dev/null
+++ b/OliverBooth/Pages/Shared/Partials/_PageTabs.cshtml
@@ -0,0 +1,21 @@
+@{
+ var urlRoot = ViewData["UrlRoot"]?.ToString() ?? string.Empty;
+ var page = (int)(ViewData["Page"] ?? 1);
+ var pageCount = (int)(ViewData["PageCount"] ?? 1);
+
+ var utility = new PageTabsUtility
+ {
+ CurrentPage = page,
+ PageCount = pageCount,
+ UrlRoot = urlRoot
+ };
+}
+
+
+
\ No newline at end of file
diff --git a/OliverBooth/Pages/Shared/_Layout.cshtml b/OliverBooth/Pages/Shared/_Layout.cshtml
index db84eee..4622e8f 100644
--- a/OliverBooth/Pages/Shared/_Layout.cshtml
+++ b/OliverBooth/Pages/Shared/_Layout.cshtml
@@ -1,7 +1,8 @@
-@using OliverBooth.Data.Blog
-@using OliverBooth.Data.Web
+@using Microsoft.AspNetCore.Mvc.TagHelpers
+@using OliverBooth.Common.Data.Blog
+@using OliverBooth.Common.Data.Web
+@using OliverBooth.Common.Services
@using OliverBooth.Extensions
-@using OliverBooth.Services
@inject IBlogPostService BlogPostService
@inject ITutorialService TutorialService
@{
diff --git a/OliverBooth/Pages/Shared/_MinimalLayout.cshtml b/OliverBooth/Pages/Shared/_MinimalLayout.cshtml
index 48fd1a8..b40e15f 100644
--- a/OliverBooth/Pages/Shared/_MinimalLayout.cshtml
+++ b/OliverBooth/Pages/Shared/_MinimalLayout.cshtml
@@ -1,3 +1,4 @@
+@using Microsoft.AspNetCore.Mvc.TagHelpers
diff --git a/OliverBooth/Pages/Tutorials/Article.cshtml b/OliverBooth/Pages/Tutorials/Article.cshtml
index 2970273..0433dbb 100644
--- a/OliverBooth/Pages/Tutorials/Article.cshtml
+++ b/OliverBooth/Pages/Tutorials/Article.cshtml
@@ -2,10 +2,10 @@
@using Humanizer
@using Markdig
@using Microsoft.AspNetCore.Mvc.TagHelpers
-@using OliverBooth.Data
-@using OliverBooth.Data.Blog
-@using OliverBooth.Data.Web
-@using OliverBooth.Services
+@using OliverBooth.Common.Data
+@using OliverBooth.Common.Data.Blog
+@using OliverBooth.Common.Data.Web
+@using OliverBooth.Common.Services
@inject ITutorialService TutorialService
@inject MarkdownPipeline MarkdownPipeline
@model Article
diff --git a/OliverBooth/Pages/Tutorials/Article.cshtml.cs b/OliverBooth/Pages/Tutorials/Article.cshtml.cs
index 7f5f6c7..2b87435 100644
--- a/OliverBooth/Pages/Tutorials/Article.cshtml.cs
+++ b/OliverBooth/Pages/Tutorials/Article.cshtml.cs
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
-using OliverBooth.Data.Web;
-using OliverBooth.Services;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Tutorials;
diff --git a/OliverBooth/Pages/Tutorials/Index.cshtml b/OliverBooth/Pages/Tutorials/Index.cshtml
index aaa2346..06f7a7a 100644
--- a/OliverBooth/Pages/Tutorials/Index.cshtml
+++ b/OliverBooth/Pages/Tutorials/Index.cshtml
@@ -1,8 +1,9 @@
@page "/tutorials/{**slug}"
@using System.Text
-@using OliverBooth.Data
-@using OliverBooth.Data.Web
-@using OliverBooth.Services
+@using Microsoft.AspNetCore.Mvc.TagHelpers
+@using OliverBooth.Common.Data
+@using OliverBooth.Common.Data.Web
+@using OliverBooth.Common.Services
@model Index
@inject ITutorialService TutorialService
@{
diff --git a/OliverBooth/Pages/Tutorials/Index.cshtml.cs b/OliverBooth/Pages/Tutorials/Index.cshtml.cs
index ac08919..23892fd 100644
--- a/OliverBooth/Pages/Tutorials/Index.cshtml.cs
+++ b/OliverBooth/Pages/Tutorials/Index.cshtml.cs
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
-using OliverBooth.Data.Web;
-using OliverBooth.Services;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Common.Services;
namespace OliverBooth.Pages.Tutorials;
diff --git a/OliverBooth/Program.cs b/OliverBooth/Program.cs
index 6360e7a..472e602 100644
--- a/OliverBooth/Program.cs
+++ b/OliverBooth/Program.cs
@@ -1,12 +1,12 @@
using AspNetCore.ReCaptcha;
using Markdig;
+using OliverBooth.Common.Services;
using OliverBooth.Data.Blog;
using OliverBooth.Data.Web;
using OliverBooth.Extensions;
-using OliverBooth.Markdown;
-using OliverBooth.Markdown.Callout;
-using OliverBooth.Markdown.Template;
-using OliverBooth.Markdown.Timestamp;
+using OliverBooth.Extensions.Markdig;
+using OliverBooth.Extensions.Markdig.Markdown.Timestamp;
+using OliverBooth.Extensions.Markdig.Services;
using OliverBooth.Services;
using Serilog;
@@ -25,7 +25,7 @@ builder.Logging.AddSerilog();
builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
.Use
()
- .Use(new TemplateExtension(provider.GetRequiredService()))
+ .UseTemplates(provider.GetRequiredService())
// we have our own "alert blocks"
.UseCallouts()
@@ -69,7 +69,7 @@ builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
-builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
+builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();
builder.Services.AddRouting(options => options.LowercaseUrls = true);
builder.Services.AddReCaptcha(builder.Configuration.GetSection("ReCaptcha"));
@@ -81,15 +81,83 @@ if (builder.Environment.IsProduction())
WebApplication app = builder.Build();
+app.Use(async (ctx, next) =>
+{
+ await next();
+
+ if (ctx.Response.HasStarted)
+ {
+ return;
+ }
+
+ string? originalPath = ctx.Request.Path.Value;
+ ctx.Items["originalPath"] = originalPath;
+
+ bool matchedErrorPage = false;
+
+ switch (ctx.Response.StatusCode)
+ {
+ case 400:
+ ctx.Request.Path = "/error/401";
+ matchedErrorPage = true;
+ break;
+
+ case 403:
+ ctx.Request.Path = "/error/403";
+ matchedErrorPage = true;
+ break;
+
+ case 404:
+ ctx.Request.Path = "/error/404";
+ matchedErrorPage = true;
+ break;
+
+ case 410:
+ ctx.Request.Path = "/error/410";
+ matchedErrorPage = true;
+ break;
+
+ case 418:
+ ctx.Request.Path = "/error/418";
+ matchedErrorPage = true;
+ break;
+
+ case 429:
+ ctx.Request.Path = "/error/429";
+ matchedErrorPage = true;
+ break;
+
+ case 500:
+ ctx.Request.Path = "/error/500";
+ matchedErrorPage = true;
+ break;
+
+ case 503:
+ ctx.Request.Path = "/error/503";
+ matchedErrorPage = true;
+ break;
+
+ case 504:
+ ctx.Request.Path = "/error/504";
+ matchedErrorPage = true;
+ break;
+ }
+
+ if (matchedErrorPage)
+ {
+ await next();
+ }
+});
+app.UseStatusCodePagesWithReExecute("/error/{0}");
+
if (!app.Environment.IsDevelopment())
{
- app.UseExceptionHandler("/Error");
+ app.UseExceptionHandler("/error/500");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
-app.UseStatusCodePagesWithRedirects("/error/{0}");
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
diff --git a/OliverBooth/Properties/launchSettings.json b/OliverBooth/Properties/launchSettings.json
new file mode 100644
index 0000000..3012f63
--- /dev/null
+++ b/OliverBooth/Properties/launchSettings.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:5000",
+ "sslPort": 2845
+ }
+ },
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "workingDirectory": "bin/Debug/net8.0/",
+ "applicationUrl": "https://localhost:2845",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/OliverBooth/Services/BlogPostService.cs b/OliverBooth/Services/BlogPostService.cs
index 38985d3..e844024 100644
--- a/OliverBooth/Services/BlogPostService.cs
+++ b/OliverBooth/Services/BlogPostService.cs
@@ -2,7 +2,9 @@ using System.Diagnostics.CodeAnalysis;
using Humanizer;
using Markdig;
using Microsoft.EntityFrameworkCore;
-using OliverBooth.Data;
+using OliverBooth.Common.Data;
+using OliverBooth.Common.Data.Blog;
+using OliverBooth.Common.Services;
using OliverBooth.Data.Blog;
namespace OliverBooth.Services;
@@ -34,10 +36,12 @@ internal sealed class BlogPostService : IBlogPostService
}
///
- public int GetBlogPostCount()
+ public int GetBlogPostCount(Visibility visibility = Visibility.None)
{
using BlogContext context = _dbContextFactory.CreateDbContext();
- return context.BlogPosts.Count();
+ return visibility == Visibility.None
+ ? context.BlogPosts.Count()
+ : context.BlogPosts.Count(p => p.Visibility == visibility);
}
///
@@ -98,6 +102,13 @@ internal sealed class BlogPostService : IBlogPostService
.FirstOrDefault(post => post.Published > blogPost.Published);
}
+ ///
+ public int GetPageCount(int pageSize = 10, Visibility visibility = Visibility.None)
+ {
+ float postCount = GetBlogPostCount(visibility);
+ return (int)MathF.Ceiling(postCount / pageSize);
+ }
+
///
public IBlogPost? GetPreviousPost(IBlogPost blogPost)
{
diff --git a/OliverBooth/Services/BlogUserService.cs b/OliverBooth/Services/BlogUserService.cs
index 9b797eb..f7e9137 100644
--- a/OliverBooth/Services/BlogUserService.cs
+++ b/OliverBooth/Services/BlogUserService.cs
@@ -1,6 +1,8 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
+using OliverBooth.Common.Data.Blog;
+using OliverBooth.Common.Services;
using OliverBooth.Data.Blog;
namespace OliverBooth.Services;
diff --git a/OliverBooth/Services/CodeSnippetService.cs b/OliverBooth/Services/CodeSnippetService.cs
index 643f6ea..0c1f6d6 100644
--- a/OliverBooth/Services/CodeSnippetService.cs
+++ b/OliverBooth/Services/CodeSnippetService.cs
@@ -1,5 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Common.Services;
using OliverBooth.Data.Web;
namespace OliverBooth.Services;
diff --git a/OliverBooth/Services/ContactService.cs b/OliverBooth/Services/ContactService.cs
index de2b591..21cb751 100644
--- a/OliverBooth/Services/ContactService.cs
+++ b/OliverBooth/Services/ContactService.cs
@@ -1,4 +1,6 @@
using Microsoft.EntityFrameworkCore;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Common.Services;
using OliverBooth.Data.Web;
namespace OliverBooth.Services;
diff --git a/OliverBooth/Services/MastodonService.cs b/OliverBooth/Services/MastodonService.cs
index 121a6cc..1e4c9d3 100644
--- a/OliverBooth/Services/MastodonService.cs
+++ b/OliverBooth/Services/MastodonService.cs
@@ -1,6 +1,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using HtmlAgilityPack;
+using OliverBooth.Common.Data.Mastodon;
+using OliverBooth.Common.Services;
using OliverBooth.Data.Mastodon;
namespace OliverBooth.Services;
@@ -13,18 +15,20 @@ internal sealed class MastodonService : IMastodonService
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
+ private readonly IConfiguration _configuration;
private readonly HttpClient _httpClient;
- public MastodonService(HttpClient httpClient)
+ public MastodonService(IConfiguration configuration, HttpClient httpClient)
{
+ _configuration = configuration;
_httpClient = httpClient;
}
///
- public MastodonStatus GetLatestStatus()
+ public IMastodonStatus GetLatestStatus()
{
- string token = Environment.GetEnvironmentVariable("MASTODON_TOKEN") ?? string.Empty;
- string account = Environment.GetEnvironmentVariable("MASTODON_ACCOUNT") ?? string.Empty;
+ string token = _configuration.GetSection("Mastodon:Token").Value ?? string.Empty;
+ string account = _configuration.GetSection("Mastodon:Account").Value ?? string.Empty;
using var request = new HttpRequestMessage();
request.Headers.Add("Authorization", $"Bearer {token}");
request.RequestUri = new Uri($"https://mastodon.olivr.me/api/v1/accounts/{account}/statuses");
diff --git a/OliverBooth/Services/ProgrammingLanguageService.cs b/OliverBooth/Services/ProgrammingLanguageService.cs
index 3f82bbc..d0f1c4d 100644
--- a/OliverBooth/Services/ProgrammingLanguageService.cs
+++ b/OliverBooth/Services/ProgrammingLanguageService.cs
@@ -1,21 +1,9 @@
using Microsoft.EntityFrameworkCore;
+using OliverBooth.Common.Services;
using OliverBooth.Data.Web;
namespace OliverBooth.Services;
-///
-/// Represents a service which can perform programming language lookup.
-///
-public interface IProgrammingLanguageService
-{
- ///
- /// Returns the human-readable name of a language.
- ///
- /// The alias of the language.
- /// The human-readable name, or if the name could not be found.
- string GetLanguageName(string alias);
-}
-
///
internal sealed class ProgrammingLanguageService : IProgrammingLanguageService
{
diff --git a/OliverBooth/Services/ProjectService.cs b/OliverBooth/Services/ProjectService.cs
index 6f75ca5..75eff1b 100644
--- a/OliverBooth/Services/ProjectService.cs
+++ b/OliverBooth/Services/ProjectService.cs
@@ -2,6 +2,8 @@ using System.Diagnostics.CodeAnalysis;
using Humanizer;
using Markdig;
using Microsoft.EntityFrameworkCore;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Common.Services;
using OliverBooth.Data.Web;
namespace OliverBooth.Services;
diff --git a/OliverBooth/Services/ReadingListService.cs b/OliverBooth/Services/ReadingListService.cs
index 54da5fb..705f056 100644
--- a/OliverBooth/Services/ReadingListService.cs
+++ b/OliverBooth/Services/ReadingListService.cs
@@ -1,4 +1,6 @@
using Microsoft.EntityFrameworkCore;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Common.Services;
using OliverBooth.Data.Web;
namespace OliverBooth.Services;
diff --git a/OliverBooth/Services/TemplateService.cs b/OliverBooth/Services/TemplateService.cs
index 623a1fd..091da99 100644
--- a/OliverBooth/Services/TemplateService.cs
+++ b/OliverBooth/Services/TemplateService.cs
@@ -1,10 +1,11 @@
using System.Buffers.Binary;
-using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
-using Markdig;
using Microsoft.EntityFrameworkCore;
+using OliverBooth.Common.Data.Web;
using OliverBooth.Data.Web;
-using OliverBooth.Formatting;
+using OliverBooth.Extensions.Markdig.Markdown.Template;
+using OliverBooth.Extensions.Markdig.Services;
+using OliverBooth.Extensions.SmartFormat;
using OliverBooth.Markdown.Template;
using SmartFormat;
using SmartFormat.Extensions;
diff --git a/OliverBooth/Services/TutorialService.cs b/OliverBooth/Services/TutorialService.cs
index 3fd21ed..4a1e387 100644
--- a/OliverBooth/Services/TutorialService.cs
+++ b/OliverBooth/Services/TutorialService.cs
@@ -3,7 +3,10 @@ using Cysharp.Text;
using Humanizer;
using Markdig;
using Microsoft.EntityFrameworkCore;
-using OliverBooth.Data;
+using OliverBooth.Common.Data;
+using OliverBooth.Common.Data.Blog;
+using OliverBooth.Common.Data.Web;
+using OliverBooth.Common.Services;
using OliverBooth.Data.Blog;
using OliverBooth.Data.Web;
diff --git a/src/img/error/400-bad-request.png b/src/img/error/400-bad-request.png
new file mode 100644
index 0000000..4e09ca8
Binary files /dev/null and b/src/img/error/400-bad-request.png differ
diff --git a/src/img/error/403-forbidden.png b/src/img/error/403-forbidden.png
new file mode 100644
index 0000000..4abd030
Binary files /dev/null and b/src/img/error/403-forbidden.png differ
diff --git a/src/img/error/404-not-found.png b/src/img/error/404-not-found.png
new file mode 100644
index 0000000..803530d
Binary files /dev/null and b/src/img/error/404-not-found.png differ
diff --git a/src/img/error/410-gone.png b/src/img/error/410-gone.png
new file mode 100644
index 0000000..69c32db
Binary files /dev/null and b/src/img/error/410-gone.png differ
diff --git a/src/img/error/418-im-a-teapot.png b/src/img/error/418-im-a-teapot.png
new file mode 100644
index 0000000..82292e0
Binary files /dev/null and b/src/img/error/418-im-a-teapot.png differ
diff --git a/src/img/error/429-too-many-requests.png b/src/img/error/429-too-many-requests.png
new file mode 100644
index 0000000..89bf1fc
Binary files /dev/null and b/src/img/error/429-too-many-requests.png differ
diff --git a/src/img/error/500-internal-server-error.png b/src/img/error/500-internal-server-error.png
new file mode 100644
index 0000000..04039fc
Binary files /dev/null and b/src/img/error/500-internal-server-error.png differ
diff --git a/src/img/error/503-service-unavailable.png b/src/img/error/503-service-unavailable.png
new file mode 100644
index 0000000..6f2b738
Binary files /dev/null and b/src/img/error/503-service-unavailable.png differ
diff --git a/src/img/error/504-gateway-timeout.png b/src/img/error/504-gateway-timeout.png
new file mode 100644
index 0000000..6f48f98
Binary files /dev/null and b/src/img/error/504-gateway-timeout.png differ
diff --git a/src/scss/app.scss b/src/scss/app.scss
index df43c57..6960532 100644
--- a/src/scss/app.scss
+++ b/src/scss/app.scss
@@ -1,4 +1,5 @@
@import "markdown";
+@import "blog";
html, body {
background: #121212;
@@ -229,19 +230,6 @@ article {
}
}
-.blog-card {
- transition: all 0.2s ease-in-out;
-
- &:hover {
- transform: scale(1.05);
- }
-
- article {
- background: none;
- padding: 0;
- }
-}
-
code:not([class*="language-"]) {
background: #1e1e1e !important;
color: #dcdcda !important;
@@ -465,7 +453,13 @@ td.trim-p p:last-child {
.mastodon-update-card.card {
background-color: desaturate(darken(#6364FF, 50%), 50%);
- margin-bottom: 50px;
+ margin-bottom: 20px;
+ border-radius: 3px;
+ border: none;
+
+ .card-body, .card-footer {
+ border: none;
+ }
p:last-child {
margin-bottom: 0;
diff --git a/src/scss/blog.scss b/src/scss/blog.scss
new file mode 100644
index 0000000..0bfc55f
--- /dev/null
+++ b/src/scss/blog.scss
@@ -0,0 +1,74 @@
+$blog-card-bg: #333333;
+$blog-card-gutter: 20px;
+$border-radius: 3px;
+
+div.blog-card {
+ background: $blog-card-bg;
+ margin-bottom: $blog-card-gutter;
+ padding: $blog-card-gutter;
+ border-radius: $border-radius;
+
+ :last-child {
+ margin-bottom: 0;
+ }
+
+ article {
+ padding: 0;
+ margin: 0;
+ }
+}
+
+ul.pagination {
+ border: none;
+
+ li {
+ a, span {
+ border-radius: $border-radius !important;
+ border: none;
+
+ &:link, &:visited, &:active {
+ color: #007ec6;
+ }
+ }
+
+ &.active a, &.active span {
+ background: #007ec6;
+ }
+
+ &:not(.active) a, &:not(.active) span {
+ background: none;
+ }
+
+ &:hover {
+ a {
+ color: #ffffff !important;
+ }
+ }
+ }
+}
+
+ul.post-tags {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+
+ li.post-tag {
+ display: inline-block;
+ margin: 0 5px;
+
+ a {
+ border-radius: 5px;
+ border: 1px solid #007ec6;
+ padding: 5px;
+
+ &:link, &:active, &:visited {
+ color: #007ec6;
+ }
+
+ &:hover {
+ border-color: #ffffff;
+ color: #ffffff;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ts/API.ts b/src/ts/API.ts
deleted file mode 100644
index a1e070d..0000000
--- a/src/ts/API.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import BlogPost from "./BlogPost";
-import Author from "./Author";
-
-class API {
- private static readonly BASE_URL: string = "/api";
- private static readonly BLOG_URL: string = "/blog";
-
- static async getBlogPostCount(): Promise {
- const response = await API.getResponse(`count`);
- return response.count;
- }
-
- static async getBlogPost(id: string): Promise {
- const response = await API.getResponse(`post/${id}`);
- return new BlogPost(response);
- }
-
- static async getBlogPosts(page: number): Promise {
- const response = await API.getResponse(`posts/${page}`);
- return response.map(obj => new BlogPost(obj));
- }
-
- static async getBlogPostsByTag(tag: string, page: number): Promise {
- const response = await API.getResponse(`posts/tagged/${tag}/${page}`);
- return response.map(obj => new BlogPost(obj));
- }
-
- static async getAuthor(id: string): Promise {
- const response = await API.getResponse(`author/${id}`);
- return new Author(response);
- }
-
- private static async getResponse(url: string): Promise {
- const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/${url}`);
- if (response.status !== 200) {
- throw new Error("Invalid response from server");
- }
-
- const text = await response.text();
- return JSON.parse(text);
- }
-}
-
-export default API;
\ No newline at end of file
diff --git a/src/ts/Author.ts b/src/ts/Author.ts
deleted file mode 100644
index 8faf1a5..0000000
--- a/src/ts/Author.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-class Author {
- private readonly _id: string;
- private readonly _name: string;
- private readonly _avatarUrl: string;
-
- constructor(json: any) {
- this._id = json.id;
- this._name = json.name;
- this._avatarUrl = json.avatarUrl;
- }
-
- get id(): string {
- return this._id;
- }
-
- get name(): string {
- return this._name;
- }
-
- get avatarUrl(): string {
- return this._avatarUrl;
- }
-}
-
-export default Author;
\ No newline at end of file
diff --git a/src/ts/BlogPost.ts b/src/ts/BlogPost.ts
deleted file mode 100644
index 1186964..0000000
--- a/src/ts/BlogPost.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import BlogUrl from "./BlogUrl";
-
-class BlogPost {
- private readonly _id: string;
- private readonly _commentsEnabled: boolean;
- private readonly _title: string;
- private readonly _excerpt: string;
- private readonly _content: string;
- private readonly _authorId: string;
- private readonly _published: Date;
- private readonly _updated?: Date;
- private readonly _url: BlogUrl;
- private readonly _trimmed: boolean;
- private readonly _identifier: string;
- private readonly _humanizedTimestamp: string;
- private readonly _formattedPublishDate: string;
- private readonly _formattedUpdateDate: string;
- private readonly _tags: string[];
-
- constructor(json: any) {
- this._id = json.id;
- this._commentsEnabled = json.commentsEnabled;
- this._title = json.title;
- this._excerpt = json.excerpt;
- this._content = json.content;
- this._authorId = json.author;
- this._published = new Date(json.published * 1000);
- this._updated = (json.updated && new Date(json.updated * 1000)) || null;
- this._url = new BlogUrl(json.url);
- this._trimmed = json.trimmed;
- this._identifier = json.identifier;
- this._humanizedTimestamp = json.humanizedTimestamp;
- this._formattedPublishDate = json.formattedPublishDate;
- this._formattedUpdateDate = json.formattedUpdateDate;
- this._tags = json.tags;
- }
-
- get id(): string {
- return this._id;
- }
-
- get commentsEnabled(): boolean {
- return this._commentsEnabled;
- }
-
- get title(): string {
- return this._title;
- }
-
- get excerpt(): string {
- return this._excerpt;
- }
-
- get content(): string {
- return this._content;
- }
-
- get authorId(): string {
- return this._authorId;
- }
-
- get published(): Date {
- return this._published;
- }
-
- get updated(): Date {
- return this._updated;
- }
-
- get url(): BlogUrl {
- return this._url;
- }
-
- get tags(): string[] {
- return this._tags;
- }
-
- get trimmed(): boolean {
- return this._trimmed;
- }
-
- get identifier(): string {
- return this._identifier;
- }
-
- get humanizedTimestamp(): string {
- return this._humanizedTimestamp;
- }
-
- get formattedPublishDate(): string {
- return this._formattedPublishDate;
- }
-
- get formattedUpdateDate(): string {
- return this._formattedUpdateDate;
- }
-}
-
-export default BlogPost;
\ No newline at end of file
diff --git a/src/ts/BlogUrl.ts b/src/ts/BlogUrl.ts
deleted file mode 100644
index 351ad3c..0000000
--- a/src/ts/BlogUrl.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-class BlogUrl {
- private readonly _year: string;
- private readonly _month: string;
- private readonly _day: string;
- private readonly _slug: string;
-
- constructor(json: any) {
- this._year = json.year;
- this._month = json.month;
- this._day = json.day;
- this._slug = json.slug;
- }
-
-
- get year(): string {
- return this._year;
- }
-
- get month(): string {
- return this._month;
- }
-
- get day(): string {
- return this._day;
- }
-
- get slug(): string {
- return this._slug;
- }
-}
-
-export default BlogUrl;
\ No newline at end of file
diff --git a/src/ts/UI.ts b/src/ts/UI.ts
index 3c9d9ed..554eb07 100644
--- a/src/ts/UI.ts
+++ b/src/ts/UI.ts
@@ -1,5 +1,3 @@
-import BlogPost from "./BlogPost";
-import Author from "./Author";
import TimeUtility from "./TimeUtility";
declare const bootstrap: any;
@@ -7,18 +5,6 @@ declare const katex: any;
declare const Prism: any;
class UI {
- public static get blogPost(): HTMLDivElement {
- return document.querySelector("article[data-blog-post='true']");
- }
-
- public static get blogPostContainer(): HTMLDivElement {
- return document.querySelector("#all-blog-posts");
- }
-
- public static get blogPostTemplate(): HTMLDivElement {
- return document.querySelector("#blog-post-template");
- }
-
/**
* Creates a