feat: add rss output at route /blog/feed
This commit is contained in:
parent
6743918f44
commit
8a1cd689ea
15
OliverBooth/Data/Rss/AtomLink.cs
Normal file
15
OliverBooth/Data/Rss/AtomLink.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace OliverBooth.Data.Rss;
|
||||
|
||||
public sealed class AtomLink
|
||||
{
|
||||
[XmlAttribute("href")]
|
||||
public string Href { get; set; } = default!;
|
||||
|
||||
[XmlAttribute("rel")]
|
||||
public string Rel { get; set; } = "self";
|
||||
|
||||
[XmlAttribute("type")]
|
||||
public string Type { get; set; } = "application/rss+xml";
|
||||
}
|
33
OliverBooth/Data/Rss/BlogChannel.cs
Normal file
33
OliverBooth/Data/Rss/BlogChannel.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace OliverBooth.Data.Rss;
|
||||
|
||||
public sealed class BlogChannel
|
||||
{
|
||||
[XmlElement("title")]
|
||||
public string Title { get; set; } = default!;
|
||||
|
||||
[XmlElement("link", Namespace = "http://www.w3.org/2005/Atom")]
|
||||
public AtomLink AtomLink { get; set; } = default!;
|
||||
|
||||
[XmlElement("link")]
|
||||
public string Link { get; set; } = default!;
|
||||
|
||||
[XmlElement("description")]
|
||||
public string Description { get; set; } = default!;
|
||||
|
||||
[XmlElement("lastBuildDate")]
|
||||
public string LastBuildDate { get; set; } = default!;
|
||||
|
||||
[XmlElement("updatePeriod", Namespace = "http://purl.org/rss/1.0/modules/syndication/")]
|
||||
public string UpdatePeriod { get; set; } = "hourly";
|
||||
|
||||
[XmlElement("updateFrequency", Namespace = "http://purl.org/rss/1.0/modules/syndication/")]
|
||||
public string UpdateFrequency { get; set; } = "1";
|
||||
|
||||
[XmlElement("generator")]
|
||||
public string Generator { get; set; } = default!;
|
||||
|
||||
[XmlElement("item")]
|
||||
public List<BlogItem> Items { get; set; } = new();
|
||||
}
|
27
OliverBooth/Data/Rss/BlogItem.cs
Normal file
27
OliverBooth/Data/Rss/BlogItem.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace OliverBooth.Data.Rss;
|
||||
|
||||
public sealed class BlogItem
|
||||
{
|
||||
[XmlElement("title")]
|
||||
public string Title { get; set; } = default!;
|
||||
|
||||
[XmlElement("link")]
|
||||
public string Link { get; set; } = default!;
|
||||
|
||||
[XmlElement("comments")]
|
||||
public string Comments { get; set; } = default!;
|
||||
|
||||
[XmlElement("creator", Namespace = "http://purl.org/dc/elements/1.1/")]
|
||||
public string Creator { get; set; } = default!;
|
||||
|
||||
[XmlElement("pubDate")]
|
||||
public string PubDate { get; set; } = default!;
|
||||
|
||||
[XmlElement("guid")]
|
||||
public string Guid { get; set; } = default!;
|
||||
|
||||
[XmlElement("description")]
|
||||
public string Description { get; set; } = default!;
|
||||
}
|
13
OliverBooth/Data/Rss/BlogRoot.cs
Normal file
13
OliverBooth/Data/Rss/BlogRoot.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace OliverBooth.Data.Rss;
|
||||
|
||||
[XmlRoot("rss")]
|
||||
public sealed class BlogRoot
|
||||
{
|
||||
[XmlAttribute("version")]
|
||||
public string Version { get; set; } = default!;
|
||||
|
||||
[XmlElement("channel")]
|
||||
public BlogChannel Channel { get; set; } = default!;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace OliverBooth.Data.Web.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the configuration for the <see cref="SiteConfiguration" /> entity.
|
||||
/// </summary>
|
||||
internal sealed class SiteConfigurationConfiguration : IEntityTypeConfiguration<SiteConfiguration>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Configure(EntityTypeBuilder<SiteConfiguration> builder)
|
||||
{
|
||||
builder.ToTable("SiteConfig");
|
||||
builder.HasKey(x => x.Key);
|
||||
|
||||
builder.Property(x => x.Key).HasMaxLength(50).IsRequired();
|
||||
builder.Property(x => x.Value).HasMaxLength(1000);
|
||||
}
|
||||
}
|
80
OliverBooth/Data/Web/SiteConfiguration.cs
Normal file
80
OliverBooth/Data/Web/SiteConfiguration.cs
Normal file
@ -0,0 +1,80 @@
|
||||
namespace OliverBooth.Data.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a site configuration item.
|
||||
/// </summary>
|
||||
public sealed class SiteConfiguration : IEquatable<SiteConfiguration>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the configuration item.
|
||||
/// </summary>
|
||||
/// <value>The name of the configuration item.</value>
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the configuration item.
|
||||
/// </summary>
|
||||
/// <value>The value of the configuration item.</value>
|
||||
public string? Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether two instances of <see cref="SiteConfiguration" /> are equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The first instance of <see cref="SiteConfiguration" /> to compare.</param>
|
||||
/// <param name="right">The second instance of <see cref="SiteConfiguration" /> to compare.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if <paramref name="left" /> and <paramref name="right" /> are equal; otherwise,
|
||||
/// <see langword="false" />.
|
||||
/// </returns>
|
||||
public static bool operator ==(SiteConfiguration? left, SiteConfiguration? right) => Equals(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether two instances of <see cref="SiteConfiguration" /> are not equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The first instance of <see cref="SiteConfiguration" /> to compare.</param>
|
||||
/// <param name="right">The second instance of <see cref="SiteConfiguration" /> to compare.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if <paramref name="left" /> and <paramref name="right" /> are not equal; otherwise,
|
||||
/// <see langword="false" />.
|
||||
/// </returns>
|
||||
public static bool operator !=(SiteConfiguration? left, SiteConfiguration? right) => !(left == right);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether this instance of <see cref="SiteConfiguration" /> is equal to another
|
||||
/// instance.
|
||||
/// </summary>
|
||||
/// <param name="other">An instance to compare with this instance.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if <paramref name="other" /> is equal to this instance; otherwise,
|
||||
/// <see langword="false" />.
|
||||
/// </returns>
|
||||
public bool Equals(SiteConfiguration? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return Key == other.Key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether this instance is equal to a specified object.
|
||||
/// </summary>
|
||||
/// <param name="obj">An object to compare with this instance.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if <paramref name="obj" /> is an instance of <see cref="SiteConfiguration" /> and
|
||||
/// equals the value of this instance; otherwise, <see langword="false" />.
|
||||
/// </returns>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || obj is SiteConfiguration other && Equals(other);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hash code for this instance.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// ReSharper disable once NonReadonlyMemberInGetHashCode
|
||||
return Key.GetHashCode();
|
||||
}
|
||||
}
|
@ -26,6 +26,12 @@ public sealed class WebContext : DbContext
|
||||
/// <value>The set of article templates.</value>
|
||||
public DbSet<ArticleTemplate> ArticleTemplates { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the set of site configuration items.
|
||||
/// </summary>
|
||||
/// <value>The set of site configuration items.</value>
|
||||
public DbSet<SiteConfiguration> SiteConfiguration { get; private set; } = null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
@ -37,5 +43,6 @@ public sealed class WebContext : DbContext
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfiguration(new ArticleTemplateConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SiteConfigurationConfiguration());
|
||||
}
|
||||
}
|
||||
|
13
OliverBooth/Middleware/RssEndpointExtensions.cs
Normal file
13
OliverBooth/Middleware/RssEndpointExtensions.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace OliverBooth.Middleware;
|
||||
|
||||
internal static class RssEndpointExtensions
|
||||
{
|
||||
public static IEndpointConventionBuilder MapRssFeed(this IEndpointRouteBuilder endpoints, string pattern)
|
||||
{
|
||||
RequestDelegate pipeline = endpoints.CreateApplicationBuilder()
|
||||
.UseMiddleware<RssMiddleware>()
|
||||
.Build();
|
||||
|
||||
return endpoints.Map(pattern, pipeline).WithDisplayName("RSS Feed");
|
||||
}
|
||||
}
|
93
OliverBooth/Middleware/RssMiddleware.cs
Normal file
93
OliverBooth/Middleware/RssMiddleware.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Xml.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Data;
|
||||
using OliverBooth.Data.Blog;
|
||||
using OliverBooth.Data.Rss;
|
||||
using OliverBooth.Services;
|
||||
|
||||
namespace OliverBooth.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the RSS middleware.
|
||||
/// </summary>
|
||||
internal sealed class RssMiddleware
|
||||
{
|
||||
private readonly BlogService _blogService;
|
||||
private readonly ConfigurationService _configurationService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RssMiddleware" /> class.
|
||||
/// </summary>
|
||||
/// <param name="_">The request delegate.</param>
|
||||
/// <param name="blogService">The blog service.</param>
|
||||
/// <param name="configurationService">The configuration service.</param>
|
||||
public RssMiddleware(RequestDelegate _,
|
||||
BlogService blogService,
|
||||
ConfigurationService configurationService)
|
||||
{
|
||||
_blogService = blogService;
|
||||
_configurationService = configurationService;
|
||||
}
|
||||
|
||||
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Middleware")]
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
context.Response.ContentType = "application/rss+xml";
|
||||
|
||||
var baseUrl = $"https://{context.Request.Host}/blog";
|
||||
var blogItems = new List<BlogItem>();
|
||||
|
||||
foreach (BlogPost blogPost in _blogService.AllPosts.OrderByDescending(p => p.Published))
|
||||
{
|
||||
var url = $"{baseUrl}/{blogPost.Published:yyyy/MM/dd}/{blogPost.Slug}";
|
||||
string excerpt = _blogService.GetExcerpt(blogPost, out _);
|
||||
var description = $"{excerpt}<p><a href=\"{url}\">Read more...</a></p>";
|
||||
|
||||
_blogService.TryGetAuthor(blogPost, out Author? author);
|
||||
|
||||
var item = new BlogItem
|
||||
{
|
||||
Title = blogPost.Title,
|
||||
Link = url,
|
||||
Comments = $"{url}#disqus_thread",
|
||||
Creator = author?.Name ?? string.Empty,
|
||||
PubDate = blogPost.Published.ToString("R"),
|
||||
Guid = $"{baseUrl}?pid={blogPost.Id}",
|
||||
Description = description
|
||||
};
|
||||
blogItems.Add(item);
|
||||
}
|
||||
|
||||
string siteTitle = _configurationService.GetSiteConfiguration("SiteTitle") ?? string.Empty;
|
||||
var rss = new BlogRoot
|
||||
{
|
||||
Channel = new BlogChannel
|
||||
{
|
||||
AtomLink = new AtomLink
|
||||
{
|
||||
Href = $"{baseUrl}/feed/",
|
||||
},
|
||||
Description = $"{baseUrl}/",
|
||||
LastBuildDate = DateTimeOffset.UtcNow.ToString("R"),
|
||||
Link = $"{baseUrl}/",
|
||||
Title = siteTitle,
|
||||
Generator = $"{baseUrl}/",
|
||||
Items = blogItems
|
||||
}
|
||||
};
|
||||
|
||||
var serializer = new XmlSerializer(typeof(BlogRoot));
|
||||
var xmlNamespaces = new XmlSerializerNamespaces();
|
||||
xmlNamespaces.Add("content", "http://purl.org/rss/1.0/modules/content/");
|
||||
xmlNamespaces.Add("wfw", "http://wellformedweb.org/CommentAPI/");
|
||||
xmlNamespaces.Add("dc", "http://purl.org/dc/elements/1.1/");
|
||||
xmlNamespaces.Add("atom", "http://www.w3.org/2005/Atom");
|
||||
xmlNamespaces.Add("sy", "http://purl.org/rss/1.0/modules/syndication/");
|
||||
xmlNamespaces.Add("slash", "http://purl.org/rss/1.0/modules/slash/");
|
||||
|
||||
await using var writer = new StreamWriter(context.Response.BodyWriter.AsStream());
|
||||
serializer.Serialize(writer, rss, xmlNamespaces);
|
||||
// await context.Response.WriteAsync(document.OuterXml);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ using Markdig;
|
||||
using NLog.Extensions.Logging;
|
||||
using OliverBooth.Data;
|
||||
using OliverBooth.Markdown;
|
||||
using OliverBooth.Middleware;
|
||||
using OliverBooth.Services;
|
||||
using X10D.Hosting.DependencyInjection;
|
||||
|
||||
@ -11,6 +12,7 @@ builder.Configuration.AddTomlFile("data/config.toml", true, true);
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddNLog();
|
||||
builder.Services.AddHostedSingleton<LoggingService>();
|
||||
builder.Services.AddSingleton<ConfigurationService>();
|
||||
builder.Services.AddSingleton<TemplateService>();
|
||||
|
||||
builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
|
||||
@ -53,5 +55,6 @@ app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
|
||||
app.MapRazorPages();
|
||||
app.MapRssFeed("/blog/feed");
|
||||
|
||||
app.Run();
|
||||
|
34
OliverBooth/Services/ConfigurationService.cs
Normal file
34
OliverBooth/Services/ConfigurationService.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Data;
|
||||
|
||||
namespace OliverBooth.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a service for retrieving configuration values.
|
||||
/// </summary>
|
||||
public sealed class ConfigurationService
|
||||
{
|
||||
private readonly IDbContextFactory<WebContext> _webContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="webContextFactory">
|
||||
/// The <see cref="IDbContextFactory{TContext}" /> for the <see cref="WebContext" />.
|
||||
/// </param>
|
||||
public ConfigurationService(IDbContextFactory<WebContext> webContextFactory)
|
||||
{
|
||||
_webContextFactory = webContextFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of a site configuration key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the site configuration item.</param>
|
||||
/// <returns>The value of the site configuration item.</returns>
|
||||
public string? GetSiteConfiguration(string key)
|
||||
{
|
||||
using WebContext context = _webContextFactory.CreateDbContext();
|
||||
return context.SiteConfiguration.Find(key)?.Value;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user