From e8bc50bbdff16bf2e46b0c6f3f300bda2cdbc482 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sat, 12 Aug 2023 20:13:47 +0100 Subject: [PATCH] refactor: move blog to separate app I'd ideally like to keep the blog. subdomain the same, and while there are a few ways to achieve this it is much simpler to just dedicate a separate application for the subdomain. This change also adjusts the webhost builder extensions to default to ports 443/80, and each app now explicitly sets the port it needs. --- .../Controllers/BlogApiController.cs | 48 +++-- .../Data/BlogContext.cs | 19 +- OliverBooth.Blog/Data/BlogPost.cs | 102 ++++++++++ .../Configuration/BlogPostConfiguration.cs | 8 +- .../Data/Configuration/UserConfiguration.cs | 21 ++ OliverBooth.Blog/Data/IBlogAuthor.cs | 32 +++ OliverBooth.Blog/Data/IBlogPost.cs | 89 +++++++++ OliverBooth.Blog/Data/IUser.cs | 54 +++++ .../Data/Rss/AtomLink.cs | 0 .../Data/Rss/BlogChannel.cs | 0 .../Data/Rss/BlogItem.cs | 0 .../Data/Rss/BlogRoot.cs | 0 OliverBooth.Blog/Data/User.cs | 73 +++++++ .../Middleware/RssEndpointExtensions.cs | 2 +- .../Middleware/RssMiddleware.cs | 49 ++--- OliverBooth.Blog/OliverBooth.Blog.csproj | 13 ++ .../Pages/Article.cshtml | 20 +- .../Pages/Article.cshtml.cs | 36 ++-- .../Pages/Index.cshtml | 2 +- .../Pages/Index.cshtml.cs | 18 +- OliverBooth.Blog/Pages/RawArticle.cshtml | 2 + .../Pages/RawArticle.cshtml.cs | 23 +-- OliverBooth.Blog/Pages/_ViewImports.cshtml | 2 + .../Pages/_ViewStart.cshtml | 0 OliverBooth.Blog/Program.cs | 26 +++ OliverBooth.Blog/Services/BlogPostService.cs | 124 ++++++++++++ OliverBooth.Blog/Services/IBlogPostService.cs | 91 +++++++++ OliverBooth.Blog/Services/IUserService.cs | 23 +++ OliverBooth.Blog/Services/UserService.cs | 32 +++ .../Extensions/WebHostBuilderExtensions.cs | 18 +- OliverBooth.Common/OliverBooth.Common.csproj | 1 + .../Areas/Blog/Pages/RawArticle.cshtml | 2 - .../Areas/Blog/Pages/_ViewImports.cshtml | 2 - OliverBooth/Data/Blog/BlogPost.cs | 186 ------------------ OliverBooth/OliverBooth.csproj | 1 - OliverBooth/Program.cs | 8 +- OliverBooth/Services/BlogService.cs | 135 ------------- OliverBooth/Services/BlogUserService.cs | 83 -------- 38 files changed, 802 insertions(+), 543 deletions(-) rename {OliverBooth => OliverBooth.Blog}/Controllers/BlogApiController.cs (59%) rename {OliverBooth => OliverBooth.Blog}/Data/BlogContext.cs (70%) create mode 100644 OliverBooth.Blog/Data/BlogPost.cs rename {OliverBooth/Data/Blog => OliverBooth.Blog/Data}/Configuration/BlogPostConfiguration.cs (83%) create mode 100644 OliverBooth.Blog/Data/Configuration/UserConfiguration.cs create mode 100644 OliverBooth.Blog/Data/IBlogAuthor.cs create mode 100644 OliverBooth.Blog/Data/IBlogPost.cs create mode 100644 OliverBooth.Blog/Data/IUser.cs rename {OliverBooth => OliverBooth.Blog}/Data/Rss/AtomLink.cs (100%) rename {OliverBooth => OliverBooth.Blog}/Data/Rss/BlogChannel.cs (100%) rename {OliverBooth => OliverBooth.Blog}/Data/Rss/BlogItem.cs (100%) rename {OliverBooth => OliverBooth.Blog}/Data/Rss/BlogRoot.cs (100%) create mode 100644 OliverBooth.Blog/Data/User.cs rename {OliverBooth => OliverBooth.Blog}/Middleware/RssEndpointExtensions.cs (90%) rename {OliverBooth => OliverBooth.Blog}/Middleware/RssMiddleware.cs (53%) create mode 100644 OliverBooth.Blog/OliverBooth.Blog.csproj rename {OliverBooth/Areas/Blog => OliverBooth.Blog}/Pages/Article.cshtml (79%) rename {OliverBooth/Areas/Blog => OliverBooth.Blog}/Pages/Article.cshtml.cs (52%) rename {OliverBooth/Areas/Blog => OliverBooth.Blog}/Pages/Index.cshtml (96%) rename {OliverBooth/Areas/Blog => OliverBooth.Blog}/Pages/Index.cshtml.cs (64%) create mode 100644 OliverBooth.Blog/Pages/RawArticle.cshtml rename {OliverBooth/Areas/Blog => OliverBooth.Blog}/Pages/RawArticle.cshtml.cs (57%) create mode 100644 OliverBooth.Blog/Pages/_ViewImports.cshtml rename {OliverBooth/Areas/Blog => OliverBooth.Blog}/Pages/_ViewStart.cshtml (100%) create mode 100644 OliverBooth.Blog/Program.cs create mode 100644 OliverBooth.Blog/Services/BlogPostService.cs create mode 100644 OliverBooth.Blog/Services/IBlogPostService.cs create mode 100644 OliverBooth.Blog/Services/IUserService.cs create mode 100644 OliverBooth.Blog/Services/UserService.cs delete mode 100644 OliverBooth/Areas/Blog/Pages/RawArticle.cshtml delete mode 100644 OliverBooth/Areas/Blog/Pages/_ViewImports.cshtml delete mode 100644 OliverBooth/Data/Blog/BlogPost.cs delete mode 100644 OliverBooth/Services/BlogService.cs delete mode 100644 OliverBooth/Services/BlogUserService.cs diff --git a/OliverBooth/Controllers/BlogApiController.cs b/OliverBooth.Blog/Controllers/BlogApiController.cs similarity index 59% rename from OliverBooth/Controllers/BlogApiController.cs rename to OliverBooth.Blog/Controllers/BlogApiController.cs index 800eab6..3f24192 100644 --- a/OliverBooth/Controllers/BlogApiController.cs +++ b/OliverBooth.Blog/Controllers/BlogApiController.cs @@ -1,58 +1,66 @@ using Humanizer; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; -using OliverBooth.Data.Blog; -using OliverBooth.Services; +using OliverBooth.Blog.Data; +using OliverBooth.Blog.Services; -namespace OliverBooth.Controllers; +namespace OliverBooth.Blog.Controllers; /// /// Represents a controller for the blog API. /// [ApiController] -[Route("api/blog")] +[Route("api")] [Produces("application/json")] -[EnableCors("BlogApi")] +[EnableCors("OliverBooth")] public sealed class BlogApiController : ControllerBase { - private readonly BlogService _blogService; - private readonly BlogUserService _blogUserService; + private readonly IBlogPostService _blogPostService; + private readonly IUserService _userService; /// /// Initializes a new instance of the class. /// - /// The . - /// The . - public BlogApiController(BlogService blogService, BlogUserService blogUserService) + /// The . + /// The . + public BlogApiController(IBlogPostService blogPostService, IUserService userService) { - _blogService = blogService; - _blogUserService = blogUserService; + _blogPostService = blogPostService; + _userService = userService; } [Route("count")] public IActionResult Count() { if (!ValidateReferer()) return NotFound(); - return Ok(new { count = _blogService.AllPosts.Count }); + return Ok(new { count = _blogPostService.GetAllBlogPosts().Count }); } [HttpGet("all/{skip:int?}/{take:int?}")] public IActionResult GetAllBlogPosts(int skip = 0, int take = -1) { if (!ValidateReferer()) return NotFound(); - if (take == -1) take = _blogService.AllPosts.Count; - return Ok(_blogService.AllPosts.Skip(skip).Take(take).Select(post => new + + // TODO yes I'm aware I can use the new pagination I wrote, this will be added soon. + IReadOnlyList allPosts = _blogPostService.GetAllBlogPosts(); + + if (take == -1) + { + take = allPosts.Count; + } + + return Ok(allPosts.Skip(skip).Take(take).Select(post => new { id = post.Id, commentsEnabled = post.EnableComments, identifier = post.GetDisqusIdentifier(), - author = post.AuthorId, + author = post.Author.Id, title = post.Title, published = post.Published.ToUnixTimeSeconds(), formattedDate = post.Published.ToString("dddd, d MMMM yyyy HH:mm"), updated = post.Updated?.ToUnixTimeSeconds(), humanizedTimestamp = post.Updated?.Humanize() ?? post.Published.Humanize(), - excerpt = _blogService.GetExcerpt(post, out bool trimmed), + excerpt = _blogPostService.RenderExcerpt(post, out bool trimmed), trimmed, url = Url.Page("/Article", new @@ -70,19 +78,19 @@ public sealed class BlogApiController : ControllerBase public IActionResult GetAuthor(Guid id) { if (!ValidateReferer()) return NotFound(); - if (!_blogUserService.TryGetUser(id, out User? author)) return NotFound(); + if (!_userService.TryGetUser(id, out IUser? author)) return NotFound(); return Ok(new { id = author.Id, name = author.DisplayName, - avatarHash = author.AvatarHash, + avatarUrl = author.AvatarUrl, }); } private bool ValidateReferer() { var referer = Request.Headers["Referer"].ToString(); - return referer.StartsWith(Url.PageLink("/index", values: new { area = "blog" })!); + return referer.StartsWith(Url.PageLink("/index")!); } } diff --git a/OliverBooth/Data/BlogContext.cs b/OliverBooth.Blog/Data/BlogContext.cs similarity index 70% rename from OliverBooth/Data/BlogContext.cs rename to OliverBooth.Blog/Data/BlogContext.cs index f5af0e0..cb3ea4e 100644 --- a/OliverBooth/Data/BlogContext.cs +++ b/OliverBooth.Blog/Data/BlogContext.cs @@ -1,13 +1,12 @@ using Microsoft.EntityFrameworkCore; -using OliverBooth.Data.Blog; -using OliverBooth.Data.Blog.Configuration; +using OliverBooth.Blog.Data.Configuration; -namespace OliverBooth.Data; +namespace OliverBooth.Blog.Data; /// /// Represents a session with the blog database. /// -public sealed class BlogContext : DbContext +internal sealed class BlogContext : DbContext { private readonly IConfiguration _configuration; @@ -21,16 +20,16 @@ public sealed class BlogContext : DbContext } /// - /// Gets the set of blog posts. + /// Gets the collection of blog posts in the database. /// - /// The set of blog posts. - public DbSet BlogPosts { get; internal set; } = null!; + /// The collection of blog posts. + public DbSet BlogPosts { get; private set; } = null!; /// - /// Gets the set of users. + /// Gets the collection of users in the database. /// - /// The set of users. - public DbSet Users { get; internal set; } = null!; + /// The collection of users. + public DbSet Users { get; private set; } = null!; /// protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) diff --git a/OliverBooth.Blog/Data/BlogPost.cs b/OliverBooth.Blog/Data/BlogPost.cs new file mode 100644 index 0000000..ccea0d5 --- /dev/null +++ b/OliverBooth.Blog/Data/BlogPost.cs @@ -0,0 +1,102 @@ +using System.ComponentModel.DataAnnotations.Schema; +using SmartFormat; + +namespace OliverBooth.Blog.Data; + +/// +internal sealed class BlogPost : IBlogPost +{ + /// + [NotMapped] + public IBlogAuthor Author { get; internal set; } = null!; + + /// + public string Body { get; internal set; } = string.Empty; + + /// + public bool EnableComments { get; internal set; } + + /// + public Guid Id { get; private set; } = Guid.NewGuid(); + + /// + public bool IsRedirect { get; internal set; } + + /// + public DateTimeOffset Published { get; internal set; } + + /// + public Uri? RedirectUrl { get; internal set; } + + /// + public string Slug { get; internal set; } = string.Empty; + + /// + public string Title { get; internal set; } = string.Empty; + + /// + public DateTimeOffset? Updated { get; internal set; } + + /// + /// Gets or sets the ID of the author of this blog post. + /// + /// The ID of the author of this blog post. + internal Guid AuthorId { get; set; } + + /// + /// Gets or sets the base URL of the Disqus comments for the blog post. + /// + /// The Disqus base URL. + internal string? DisqusDomain { get; set; } + + /// + /// Gets or sets the identifier of the Disqus comments for the blog post. + /// + /// The Disqus identifier. + internal string? DisqusIdentifier { get; set; } + + /// + /// Gets or sets the URL path of the Disqus comments for the blog post. + /// + /// The Disqus URL path. + internal string? DisqusPath { get; set; } + + /// + /// Gets or sets the WordPress ID of this blog post. + /// + /// The WordPress ID of this blog post. + internal int? WordPressId { get; set; } + + /// + /// Gets the Disqus domain for the blog post. + /// + /// The Disqus domain. + public string GetDisqusDomain() + { + return string.IsNullOrWhiteSpace(DisqusDomain) + ? "https://oliverbooth.dev/blog" + : Smart.Format(DisqusDomain, this); + } + + /// + public string GetDisqusIdentifier() + { + return string.IsNullOrWhiteSpace(DisqusIdentifier) ? $"post-{Id}" : Smart.Format(DisqusIdentifier, this); + } + + /// + public string GetDisqusUrl() + { + string path = string.IsNullOrWhiteSpace(DisqusPath) + ? $"{Published:yyyy/MM/dd}/{Slug}/" + : Smart.Format(DisqusPath, this); + + return $"{GetDisqusDomain()}/{path}"; + } + + /// + public string GetDisqusPostId() + { + return WordPressId?.ToString() ?? Id.ToString(); + } +} diff --git a/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs b/OliverBooth.Blog/Data/Configuration/BlogPostConfiguration.cs similarity index 83% rename from OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs rename to OliverBooth.Blog/Data/Configuration/BlogPostConfiguration.cs index e11c48c..90a0bb2 100644 --- a/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs +++ b/OliverBooth.Blog/Data/Configuration/BlogPostConfiguration.cs @@ -1,11 +1,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace OliverBooth.Data.Blog.Configuration; +namespace OliverBooth.Blog.Data.Configuration; -/// -/// Represents the configuration for the entity. -/// internal sealed class BlogPostConfiguration : IEntityTypeConfiguration { /// @@ -23,7 +21,7 @@ internal sealed class BlogPostConfiguration : IEntityTypeConfiguration builder.Property(e => e.Title).HasMaxLength(255).IsRequired(); builder.Property(e => e.Body).IsRequired(); builder.Property(e => e.IsRedirect).IsRequired(); - builder.Property(e => e.RedirectUrl).IsRequired(false); + builder.Property(e => e.RedirectUrl).HasConversion().HasMaxLength(255).IsRequired(false); builder.Property(e => e.EnableComments).IsRequired(); builder.Property(e => e.DisqusDomain).IsRequired(false); builder.Property(e => e.DisqusIdentifier).IsRequired(false); diff --git a/OliverBooth.Blog/Data/Configuration/UserConfiguration.cs b/OliverBooth.Blog/Data/Configuration/UserConfiguration.cs new file mode 100644 index 0000000..68fa803 --- /dev/null +++ b/OliverBooth.Blog/Data/Configuration/UserConfiguration.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace OliverBooth.Blog.Data.Configuration; + +internal sealed class UserConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + RelationalEntityTypeBuilderExtensions.ToTable((EntityTypeBuilder)builder, "User"); + builder.HasKey(e => e.Id); + + builder.Property(e => e.Id).IsRequired(); + builder.Property(e => e.DisplayName).HasMaxLength(50).IsRequired(); + builder.Property(e => e.EmailAddress).HasMaxLength(255).IsRequired(); + builder.Property(e => e.Password).HasMaxLength(255).IsRequired(); + builder.Property(e => e.Salt).HasMaxLength(255).IsRequired(); + builder.Property(e => e.Registered).IsRequired(); + } +} diff --git a/OliverBooth.Blog/Data/IBlogAuthor.cs b/OliverBooth.Blog/Data/IBlogAuthor.cs new file mode 100644 index 0000000..cf9b4c0 --- /dev/null +++ b/OliverBooth.Blog/Data/IBlogAuthor.cs @@ -0,0 +1,32 @@ +namespace OliverBooth.Blog.Data; + +/// +/// Represents the author of a blog post. +/// +public interface IBlogAuthor +{ + /// + /// Gets the URL of the author's avatar. + /// + /// The URL of the author's avatar. + Uri AvatarUrl { get; } + + /// + /// Gets the display name of the author. + /// + /// The display name of the author. + string DisplayName { get; } + + /// + /// Gets the unique identifier of the author. + /// + /// The unique identifier of the author. + Guid Id { get; } + + /// + /// Gets the URL of the author's avatar. + /// + /// The size of the avatar. + /// The URL of the author's avatar. + Uri GetAvatarUrl(int size = 28); +} diff --git a/OliverBooth.Blog/Data/IBlogPost.cs b/OliverBooth.Blog/Data/IBlogPost.cs new file mode 100644 index 0000000..af79935 --- /dev/null +++ b/OliverBooth.Blog/Data/IBlogPost.cs @@ -0,0 +1,89 @@ +namespace OliverBooth.Blog.Data; + +/// +/// Represents a blog post. +/// +public interface IBlogPost +{ + /// + /// Gets the author of the post. + /// + /// The author of the post. + IBlogAuthor Author { get; } + + /// + /// Gets the body of the post. + /// + /// The body of the post. + string Body { get; } + + /// + /// Gets a value indicating whether comments are enabled for the post. + /// + /// + /// if comments are enabled for the post; otherwise, . + /// + bool EnableComments { get; } + + /// + /// Gets the ID of the post. + /// + /// The ID of the post. + Guid Id { get; } + + /// + /// Gets a value indicating whether the post redirects to another URL. + /// + /// + /// if the post redirects to another URL; otherwise, . + /// + bool IsRedirect { get; } + + /// + /// Gets the date and time the post was published. + /// + /// The publication date and time. + DateTimeOffset Published { get; } + + /// + /// Gets the URL to which the post redirects. + /// + /// The URL to which the post redirects, or if the post does not redirect. + Uri? RedirectUrl { get; } + + /// + /// Gets the slug of the post. + /// + /// The slug of the post. + string Slug { get; } + + /// + /// Gets the title of the post. + /// + /// The title of the post. + string Title { get; } + + /// + /// Gets the date and time the post was last updated. + /// + /// The update date and time, or if the post has not been updated. + DateTimeOffset? Updated { get; } + + /// + /// Gets the Disqus identifier for the post. + /// + /// The Disqus identifier for the post. + string GetDisqusIdentifier(); + + /// + /// Gets the Disqus URL for the post. + /// + /// The Disqus URL for the post. + string GetDisqusUrl(); + + /// + /// Gets the Disqus post ID for the post. + /// + /// The Disqus post ID for the post. + string GetDisqusPostId(); +} diff --git a/OliverBooth.Blog/Data/IUser.cs b/OliverBooth.Blog/Data/IUser.cs new file mode 100644 index 0000000..c9999dd --- /dev/null +++ b/OliverBooth.Blog/Data/IUser.cs @@ -0,0 +1,54 @@ +namespace OliverBooth.Blog.Data; + +/// +/// Represents a user which can log in to the blog. +/// +public interface IUser +{ + /// + /// Gets the URL of the user's avatar. + /// + /// The URL of the user's avatar. + Uri AvatarUrl { get; } + + /// + /// Gets the email address of the user. + /// + /// The email address of the user. + string EmailAddress { get; } + + /// + /// Gets the display name of the author. + /// + /// The display name of the author. + string DisplayName { get; } + + /// + /// Gets the unique identifier of the user. + /// + /// The unique identifier of the user. + Guid Id { get; } + + /// + /// Gets the date and time the user registered. + /// + /// The registration date and time. + DateTimeOffset Registered { get; } + + /// + /// Gets the URL of the user's avatar. + /// + /// The size of the avatar. + /// The URL of the user's avatar. + Uri GetAvatarUrl(int size = 28); + + /// + /// Returns a value indicating whether the specified password is valid for the user. + /// + /// The password to test. + /// + /// if the specified password is valid for the user; otherwise, + /// . + /// + bool TestCredentials(string password); +} diff --git a/OliverBooth/Data/Rss/AtomLink.cs b/OliverBooth.Blog/Data/Rss/AtomLink.cs similarity index 100% rename from OliverBooth/Data/Rss/AtomLink.cs rename to OliverBooth.Blog/Data/Rss/AtomLink.cs diff --git a/OliverBooth/Data/Rss/BlogChannel.cs b/OliverBooth.Blog/Data/Rss/BlogChannel.cs similarity index 100% rename from OliverBooth/Data/Rss/BlogChannel.cs rename to OliverBooth.Blog/Data/Rss/BlogChannel.cs diff --git a/OliverBooth/Data/Rss/BlogItem.cs b/OliverBooth.Blog/Data/Rss/BlogItem.cs similarity index 100% rename from OliverBooth/Data/Rss/BlogItem.cs rename to OliverBooth.Blog/Data/Rss/BlogItem.cs diff --git a/OliverBooth/Data/Rss/BlogRoot.cs b/OliverBooth.Blog/Data/Rss/BlogRoot.cs similarity index 100% rename from OliverBooth/Data/Rss/BlogRoot.cs rename to OliverBooth.Blog/Data/Rss/BlogRoot.cs diff --git a/OliverBooth.Blog/Data/User.cs b/OliverBooth.Blog/Data/User.cs new file mode 100644 index 0000000..c374982 --- /dev/null +++ b/OliverBooth.Blog/Data/User.cs @@ -0,0 +1,73 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Security.Cryptography; +using System.Text; +using Cysharp.Text; + +namespace OliverBooth.Blog.Data; + +/// +/// Represents a user. +/// +internal sealed class User : IUser, IBlogAuthor +{ + /// + [NotMapped] + public Uri AvatarUrl => GetAvatarUrl(); + + /// + public string EmailAddress { get; set; } = string.Empty; + + /// + public string DisplayName { get; set; } = string.Empty; + + /// + public Guid Id { get; private set; } = Guid.NewGuid(); + + /// + public DateTimeOffset Registered { get; private set; } = DateTimeOffset.UtcNow; + + /// + /// Gets or sets the password hash. + /// + /// The password hash. + internal string Password { get; set; } = string.Empty; + + /// + /// Gets or sets the salt used to hash the password. + /// + /// The salt used to hash the password. + internal string Salt { get; set; } = string.Empty; + + /// + public Uri GetAvatarUrl(int size = 28) + { + if (string.IsNullOrWhiteSpace(EmailAddress)) + { + return new Uri($"https://www.gravatar.com/avatar/0?size={size}"); + } + + ReadOnlySpan span = EmailAddress.AsSpan(); + int byteCount = Encoding.UTF8.GetByteCount(span); + Span bytes = stackalloc byte[byteCount]; + Encoding.UTF8.GetBytes(span, bytes); + + Span hash = stackalloc byte[16]; + MD5.TryHashData(bytes, hash, out _); + + using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder(); + Span hex = stackalloc char[2]; + for (var index = 0; index < hash.Length; index++) + { + if (hash[index].TryFormat(hex, out _, "x2")) builder.Append(hex); + else builder.Append("00"); + } + + return new Uri($"https://www.gravatar.com/avatar/{builder}?size={size}"); + } + + /// + public bool TestCredentials(string password) + { + return false; + } +} diff --git a/OliverBooth/Middleware/RssEndpointExtensions.cs b/OliverBooth.Blog/Middleware/RssEndpointExtensions.cs similarity index 90% rename from OliverBooth/Middleware/RssEndpointExtensions.cs rename to OliverBooth.Blog/Middleware/RssEndpointExtensions.cs index 8b0d501..412657a 100644 --- a/OliverBooth/Middleware/RssEndpointExtensions.cs +++ b/OliverBooth.Blog/Middleware/RssEndpointExtensions.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Middleware; +namespace OliverBooth.Blog.Middleware; internal static class RssEndpointExtensions { diff --git a/OliverBooth/Middleware/RssMiddleware.cs b/OliverBooth.Blog/Middleware/RssMiddleware.cs similarity index 53% rename from OliverBooth/Middleware/RssMiddleware.cs rename to OliverBooth.Blog/Middleware/RssMiddleware.cs index 9e6ac6b..57a52ff 100644 --- a/OliverBooth/Middleware/RssMiddleware.cs +++ b/OliverBooth.Blog/Middleware/RssMiddleware.cs @@ -1,35 +1,18 @@ using System.Diagnostics.CodeAnalysis; using System.Xml.Serialization; -using OliverBooth.Data.Blog; +using OliverBooth.Blog.Data; +using OliverBooth.Blog.Services; using OliverBooth.Data.Rss; -using OliverBooth.Services; -namespace OliverBooth.Middleware; +namespace OliverBooth.Blog.Middleware; -/// -/// Represents the RSS middleware. -/// internal sealed class RssMiddleware { - private readonly BlogService _blogService; - private readonly BlogUserService _userService; - private readonly ConfigurationService _configurationService; + private readonly IBlogPostService _blogPostService; - /// - /// Initializes a new instance of the class. - /// - /// The request delegate. - /// The blog service. - /// The user service. - /// The configuration service. - public RssMiddleware(RequestDelegate _, - BlogService blogService, - BlogUserService userService, - ConfigurationService configurationService) + public RssMiddleware(RequestDelegate _, IBlogPostService blogPostService) { - _blogService = blogService; - _userService = userService; - _configurationService = configurationService; + _blogPostService = blogPostService; } [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Middleware")] @@ -40,28 +23,25 @@ internal sealed class RssMiddleware var baseUrl = $"https://{context.Request.Host}/blog"; var blogItems = new List(); - foreach (BlogPost blogPost in _blogService.AllPosts.OrderByDescending(p => p.Published)) + foreach (IBlogPost post in _blogPostService.GetAllBlogPosts()) { - var url = $"{baseUrl}/{blogPost.Published:yyyy/MM/dd}/{blogPost.Slug}"; - string excerpt = _blogService.GetExcerpt(blogPost, out _); + var url = $"{baseUrl}/{post.Published:yyyy/MM/dd}/{post.Slug}"; + string excerpt = _blogPostService.RenderExcerpt(post, out _); var description = $"{excerpt}

Read more...

"; - _userService.TryGetUser(blogPost.AuthorId, out User? author); - var item = new BlogItem { - Title = blogPost.Title, + Title = post.Title, Link = url, Comments = $"{url}#disqus_thread", - Creator = author?.DisplayName ?? string.Empty, - PubDate = blogPost.Published.ToString("R"), - Guid = $"{baseUrl}?pid={blogPost.Id}", + Creator = post.Author.DisplayName, + PubDate = post.Published.ToString("R"), + Guid = $"{baseUrl}?pid={post.Id}", Description = description }; blogItems.Add(item); } - string siteTitle = _configurationService.GetSiteConfiguration("SiteTitle") ?? string.Empty; var rss = new BlogRoot { Channel = new BlogChannel @@ -73,7 +53,7 @@ internal sealed class RssMiddleware Description = $"{baseUrl}/", LastBuildDate = DateTimeOffset.UtcNow.ToString("R"), Link = $"{baseUrl}/", - Title = siteTitle, + Title = "Oliver Booth", Generator = $"{baseUrl}/", Items = blogItems } @@ -90,6 +70,5 @@ internal sealed class RssMiddleware await using var writer = new StreamWriter(context.Response.BodyWriter.AsStream()); serializer.Serialize(writer, rss, xmlNamespaces); - // await context.Response.WriteAsync(document.OuterXml); } } diff --git a/OliverBooth.Blog/OliverBooth.Blog.csproj b/OliverBooth.Blog/OliverBooth.Blog.csproj new file mode 100644 index 0000000..fd902ff --- /dev/null +++ b/OliverBooth.Blog/OliverBooth.Blog.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/OliverBooth/Areas/Blog/Pages/Article.cshtml b/OliverBooth.Blog/Pages/Article.cshtml similarity index 79% rename from OliverBooth/Areas/Blog/Pages/Article.cshtml rename to OliverBooth.Blog/Pages/Article.cshtml index b693710..e1480c4 100644 --- a/OliverBooth/Areas/Blog/Pages/Article.cshtml +++ b/OliverBooth.Blog/Pages/Article.cshtml @@ -1,9 +1,7 @@ -@page "/blog/{year:int}/{month:int}/{day:int}/{slug}" +@page "/{year:int}/{month:int}/{day:int}/{slug}" @using Humanizer -@using OliverBooth.Data.Blog -@using OliverBooth.Services -@model OliverBooth.Areas.Blog.Pages.Article -@inject BlogService BlogService +@using OliverBooth.Blog.Data +@model Article @if (Model.Post is not { } post) { @@ -12,14 +10,14 @@ @{ ViewData["Title"] = post.Title; - User author = Model.Author; + IBlogAuthor author = post.Author; DateTimeOffset published = post.Published; }