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>
|
/// <value>The set of article templates.</value>
|
||||||
public DbSet<ArticleTemplate> ArticleTemplates { get; private set; } = null!;
|
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 />
|
/// <inheritdoc />
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
@ -37,5 +43,6 @@ public sealed class WebContext : DbContext
|
|||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.ApplyConfiguration(new ArticleTemplateConfiguration());
|
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 NLog.Extensions.Logging;
|
||||||
using OliverBooth.Data;
|
using OliverBooth.Data;
|
||||||
using OliverBooth.Markdown;
|
using OliverBooth.Markdown;
|
||||||
|
using OliverBooth.Middleware;
|
||||||
using OliverBooth.Services;
|
using OliverBooth.Services;
|
||||||
using X10D.Hosting.DependencyInjection;
|
using X10D.Hosting.DependencyInjection;
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ builder.Configuration.AddTomlFile("data/config.toml", true, true);
|
|||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Logging.AddNLog();
|
builder.Logging.AddNLog();
|
||||||
builder.Services.AddHostedSingleton<LoggingService>();
|
builder.Services.AddHostedSingleton<LoggingService>();
|
||||||
|
builder.Services.AddSingleton<ConfigurationService>();
|
||||||
builder.Services.AddSingleton<TemplateService>();
|
builder.Services.AddSingleton<TemplateService>();
|
||||||
|
|
||||||
builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
|
builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
|
||||||
@ -53,5 +55,6 @@ app.UseAuthorization();
|
|||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
|
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
|
||||||
app.MapRazorPages();
|
app.MapRazorPages();
|
||||||
|
app.MapRssFeed("/blog/feed");
|
||||||
|
|
||||||
app.Run();
|
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