diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index da73e5e3..992201f9 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -1,114 +1,114 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
- all
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/web/MZikmund.Web.Core/MZikmund.Web.Core.csproj b/src/web/MZikmund.Web.Core/MZikmund.Web.Core.csproj
index a3cc6035..633cf5db 100644
--- a/src/web/MZikmund.Web.Core/MZikmund.Web.Core.csproj
+++ b/src/web/MZikmund.Web.Core/MZikmund.Web.Core.csproj
@@ -13,6 +13,7 @@
+
diff --git a/src/web/MZikmund.Web.Core/Syndication/FeedEntry.cs b/src/web/MZikmund.Web.Core/Syndication/FeedEntry.cs
index 0fe76a48..e5c4ffed 100644
--- a/src/web/MZikmund.Web.Core/Syndication/FeedEntry.cs
+++ b/src/web/MZikmund.Web.Core/Syndication/FeedEntry.cs
@@ -4,7 +4,9 @@ public record FeedEntry
{
public string Id { get; set; } = "";
- public DateTimeOffset PubDate { get; set; }
+ public DateTimeOffset PublishedDate { get; set; }
+
+ public DateTimeOffset UpdatedDate { get; set; }
public string Title { get; set; } = "";
diff --git a/src/web/MZikmund.Web.Core/Syndication/FeedGenerator.cs b/src/web/MZikmund.Web.Core/Syndication/FeedGenerator.cs
index 762f7b80..52f2494a 100644
--- a/src/web/MZikmund.Web.Core/Syndication/FeedGenerator.cs
+++ b/src/web/MZikmund.Web.Core/Syndication/FeedGenerator.cs
@@ -1,11 +1,15 @@
// Based on https://github.com/EdiWang/Moonglade/blob/cf5571b0db09c7722b310ca9922cdcd542e93a51/src/Moonglade.Syndication/FeedGenerator.cs
-using Edi.SyndicationFeed.ReaderWriter;
using Edi.SyndicationFeed.ReaderWriter.Atom;
-using Edi.SyndicationFeed.ReaderWriter.Rss;
using Microsoft.AspNetCore.Http;
+using System.ServiceModel.Syndication;
+using System;
using System.Text;
using System.Xml;
+using EwSyndicationItem = Edi.SyndicationFeed.ReaderWriter.SyndicationItem;
+using EwSyndicationLink = Edi.SyndicationFeed.ReaderWriter.SyndicationLink;
+using EwSyndicationCategory = Edi.SyndicationFeed.ReaderWriter.SyndicationCategory;
+using EwSyndicationPerson = Edi.SyndicationFeed.ReaderWriter.SyndicationPerson;
namespace MZikmund.Web.Core.Syndication;
@@ -18,7 +22,7 @@ public FeedGenerator(IHttpContextAccessor httpContextAccessor)
HostUrl = baseUrl!;
HeadTitle = "Martin Zikmund";
HeadDescription = "Open-source enthusiast and Microsoft MVP. Passionate speaker, avid climber, and Lego aficionado.";
- Copyright = "©2023 Martin Zikmund";
+ Copyright = $"©{DateTimeOffset.UtcNow.Year} Martin Zikmund";
Generator = "MZikmund";
GeneratorVersion = "0.1";
TrackBackUrl = baseUrl!;
@@ -42,28 +46,29 @@ public FeedGenerator(IHttpContextAccessor httpContextAccessor)
public string Language { get; set; }
- public async Task GetRssAsync(IEnumerable feedEntries)
+ public async Task GetRssAsync(IEnumerable feedEntries, string id)
{
+ var feed = new SyndicationFeed(HeadTitle, HeadDescription, new Uri("https://mzikmund.dev/rss"), id, DateTimeOffset.UtcNow);
+ feed.Copyright = new TextSyndicationContent(Copyright);
+
+ var items = GetItemCollection(feedEntries);
+ feed.Items = items;
// TODO: Generate additional properties (image, etc.)
- var feed = GetItemCollection(feedEntries);
var sw = new StringWriterWithEncoding(Encoding.UTF8);
- await using (var xmlWriter = XmlWriter.Create(sw, new() { Async = true }))
- {
- var writer = new RssFeedWriter(xmlWriter);
- await writer.WriteTitle(HeadTitle);
- await writer.WriteDescription(HeadDescription);
- await writer.Write(new SyndicationLink(new(TrackBackUrl)));
- await writer.WritePubDate(DateTimeOffset.UtcNow);
- await writer.WriteCopyright(Copyright);
- await writer.WriteGenerator(Generator);
- await writer.WriteLanguage(new(Language));
+ var settings = new XmlWriterSettings
+ {
+ Encoding = Encoding.UTF8,
+ NewLineHandling = NewLineHandling.Entitize,
+ NewLineOnAttributes = true,
+ Indent = true
+ };
- foreach (var item in feed)
- {
- await writer.Write(item);
- }
+ await using (var xmlWriter = XmlWriter.Create(sw, new() { Async = true }))
+ {
+ var rssFormatter = new Rss20FeedFormatter(feed, false);
+ rssFormatter.WriteTo(xmlWriter);
await xmlWriter.FlushAsync();
xmlWriter.Close();
@@ -75,7 +80,7 @@ public async Task GetRssAsync(IEnumerable feedEntries)
public async Task GetAtomAsync(IEnumerable feedEntries)
{
- var feed = GetItemCollection(feedEntries);
+ var feed = GetEwItemCollection(feedEntries);
var sw = new StringWriterWithEncoding(Encoding.UTF8);
await using (var xmlWriter = XmlWriter.Create(sw, new() { Async = true }))
@@ -101,29 +106,29 @@ public async Task GetAtomAsync(IEnumerable feedEntries)
return xml;
}
- private static List GetItemCollection(IEnumerable itemCollection)
+ private static List GetEwItemCollection(IEnumerable itemCollection)
{
- var synItemCollection = new List();
+ var synItemCollection = new List();
if (null == itemCollection) return synItemCollection;
foreach (var item in itemCollection)
{
// create rss item
- var sItem = new SyndicationItem
+ var sItem = new EwSyndicationItem
{
Id = item.Id,
Title = item.Title,
Description = item.Description,
- LastUpdated = item.PubDate.ToUniversalTime(),
- Published = item.PubDate.ToUniversalTime()
+ LastUpdated = item.UpdatedDate.ToUniversalTime(),
+ Published = item.PublishedDate.ToUniversalTime()
};
- sItem.AddLink(new SyndicationLink(new(item.Link)));
+ sItem.AddLink(new EwSyndicationLink(new(item.Link)));
// add author
if (!string.IsNullOrWhiteSpace(item.Author) && !string.IsNullOrWhiteSpace(item.AuthorEmail))
{
- sItem.AddContributor(new SyndicationPerson(item.Author, item.AuthorEmail));
+ sItem.AddContributor(new EwSyndicationPerson(item.Author, item.AuthorEmail));
}
// add categories
@@ -131,13 +136,41 @@ private static List GetItemCollection(IEnumerable it
{
foreach (var itemCategory in item.Categories)
{
- sItem.AddCategory(new SyndicationCategory(itemCategory));
+ sItem.AddCategory(new EwSyndicationCategory(itemCategory));
}
}
synItemCollection.Add(sItem);
}
return synItemCollection;
}
+
+ private List GetItemCollection(IEnumerable itemCollection)
+ {
+ var syndicationItems = new List();
+ if (null == itemCollection)
+ {
+ return syndicationItems;
+ }
+
+ foreach (var item in itemCollection)
+ {
+ // create rss item
+ var syndicationItem = new SyndicationItem(item.Title, item.Description, new Uri(item.Link), item.Id, item.UpdatedDate);
+ syndicationItem.Authors.Add(new SyndicationPerson(item.AuthorEmail, item.Author, HostUrl));
+
+ // add categories
+ if (item.Categories is { Length: > 0 })
+ {
+ foreach (var itemCategory in item.Categories)
+ {
+ syndicationItem.Categories.Add(new SyndicationCategory(itemCategory));
+ }
+ }
+
+ syndicationItems.Add(syndicationItem);
+ }
+ return syndicationItems;
+ }
}
public class StringWriterWithEncoding : StringWriter
diff --git a/src/web/MZikmund.Web.Core/Syndication/GetRssHandler.cs b/src/web/MZikmund.Web.Core/Syndication/GetRssHandler.cs
index 1cd221e6..7386a64a 100644
--- a/src/web/MZikmund.Web.Core/Syndication/GetRssHandler.cs
+++ b/src/web/MZikmund.Web.Core/Syndication/GetRssHandler.cs
@@ -19,12 +19,12 @@ public GetRssHandler(
public async Task Handle(GetRssQuery request, CancellationToken ct)
{
- var data = await _syndicationDataSource.GetFeedDataAsync(request.CategoryName);
+ var data = await _syndicationDataSource.GetFeedDataAsync(request.CategoryName, request.TagName);
if (data is null)
{
return null;
}
- return await _feedGenerator.GetRssAsync(data);
+ return await _feedGenerator.GetRssAsync(data, request.CategoryName ?? "Default");
}
}
diff --git a/src/web/MZikmund.Web.Core/Syndication/GetRssQuery.cs b/src/web/MZikmund.Web.Core/Syndication/GetRssQuery.cs
index 9f30a645..9ec89500 100644
--- a/src/web/MZikmund.Web.Core/Syndication/GetRssQuery.cs
+++ b/src/web/MZikmund.Web.Core/Syndication/GetRssQuery.cs
@@ -2,4 +2,4 @@
namespace MZikmund.Web.Core.Syndication;
-public record GetRssQuery(string? CategoryName) : IRequest;
+public record GetRssQuery(string? CategoryName, string? TagName) : IRequest;
diff --git a/src/web/MZikmund.Web.Core/Syndication/IFeedGenerator.cs b/src/web/MZikmund.Web.Core/Syndication/IFeedGenerator.cs
index 74851e4f..41a1ba49 100644
--- a/src/web/MZikmund.Web.Core/Syndication/IFeedGenerator.cs
+++ b/src/web/MZikmund.Web.Core/Syndication/IFeedGenerator.cs
@@ -2,7 +2,7 @@
public interface IFeedGenerator
{
- Task GetRssAsync(IEnumerable feedEntries);
+ Task GetRssAsync(IEnumerable feedEntries, string id);
Task GetAtomAsync(IEnumerable feedEntries);
}
diff --git a/src/web/MZikmund.Web.Core/Syndication/ISyndicationDataSource.cs b/src/web/MZikmund.Web.Core/Syndication/ISyndicationDataSource.cs
index 1fac9558..c5f28fb2 100644
--- a/src/web/MZikmund.Web.Core/Syndication/ISyndicationDataSource.cs
+++ b/src/web/MZikmund.Web.Core/Syndication/ISyndicationDataSource.cs
@@ -4,5 +4,5 @@ namespace MZikmund.Web.Core.Syndication;
public interface ISyndicationDataSource
{
- Task?> GetFeedDataAsync(string? categoryRouteName = null);
+ Task?> GetFeedDataAsync(string? categoryRouteName = null, string? tagRouteName = null);
}
diff --git a/src/web/MZikmund.Web.Core/Syndication/SyndicationDataSource.cs b/src/web/MZikmund.Web.Core/Syndication/SyndicationDataSource.cs
index 528603e2..723810fe 100644
--- a/src/web/MZikmund.Web.Core/Syndication/SyndicationDataSource.cs
+++ b/src/web/MZikmund.Web.Core/Syndication/SyndicationDataSource.cs
@@ -1,8 +1,6 @@
// Based on https://github.com/EdiWang/Moonglade/blob/cf5571b0db09c7722b310ca9922cdcd542e93a51/src/Moonglade.Syndication/SyndicationDataSource.cs
using Microsoft.AspNetCore.Http;
-
-using Microsoft.Extensions.Configuration;
using MZikmund.Web.Configuration;
using MZikmund.Web.Core.Services;
using MZikmund.Web.Data.Entities;
@@ -15,6 +13,7 @@ public class SyndicationDataSource : ISyndicationDataSource
{
private readonly string _baseUrl;
private readonly IRepository _categoriesRepository;
+ private readonly IRepository _tagsRepository;
private readonly IRepository _postsRepository;
private readonly IPostContentProcessor _postContentProcessor;
private readonly ISiteConfiguration _siteConfiguration;
@@ -22,11 +21,13 @@ public class SyndicationDataSource : ISyndicationDataSource
public SyndicationDataSource(
IHttpContextAccessor httpContextAccessor,
IRepository categoriesRepository,
+ IRepository tagsRepository,
IRepository postsRepository,
IPostContentProcessor postContentProcessor,
ISiteConfiguration siteConfiguration)
{
_categoriesRepository = categoriesRepository;
+ _tagsRepository = tagsRepository;
_postsRepository = postsRepository;
_postContentProcessor = postContentProcessor;
_siteConfiguration = siteConfiguration;
@@ -34,18 +35,28 @@ public SyndicationDataSource(
_baseUrl = $"{acc.HttpContext.Request.Scheme}://{acc.HttpContext.Request.Host}";
}
- public async Task?> GetFeedDataAsync(string? categoryRouteName = null)
+ public async Task?> GetFeedDataAsync(string? categoryRouteName = null, string? tagRouteName = null)
{
IReadOnlyList itemCollection;
if (!string.IsNullOrWhiteSpace(categoryRouteName))
{
- var cat = await _categoriesRepository.GetAsync(c => c.RouteName == categoryRouteName);
- if (cat is null)
+ var category = await _categoriesRepository.GetAsync(c => c.RouteName == categoryRouteName);
+ if (category is null)
+ {
+ return null;
+ }
+
+ itemCollection = await GetFeedEntriesAsync(category.Id);
+ }
+ else if (!string.IsNullOrWhiteSpace(tagRouteName))
+ {
+ var tag = await _tagsRepository.GetAsync(c => c.RouteName == tagRouteName);
+ if (tag is null)
{
return null;
}
- itemCollection = await GetFeedEntriesAsync(cat.Id);
+ itemCollection = await GetFeedEntriesAsync(null, tag.Id);
}
else
{
@@ -55,15 +66,16 @@ public SyndicationDataSource(
return itemCollection;
}
- private async Task> GetFeedEntriesAsync(Guid? categoryId = null)
+ private async Task> GetFeedEntriesAsync(Guid? categoryId = null, Guid? tagId = null)
{
- var specification = new ListPostsSpecification(1, 15, categoryId, null);
+ var specification = new ListPostsSpecification(1, 15, categoryId, tagId);
var posts = await _postsRepository.SelectAsync(specification, post => new FeedEntry
{
Id = post.Id.ToString(),
Title = post.Title,
- PubDate = post.PublishedDate ?? DateTimeOffset.UtcNow,
- Description = post.Content, // TODO: Do we want full content here?
+ PublishedDate = post.PublishedDate ?? DateTimeOffset.UtcNow,
+ UpdatedDate = post.LastModifiedDate ?? DateTimeOffset.UtcNow,
+ Description = post.Content,
Link = $"{_baseUrl}/blog/{post.RouteName}",
Author = $"{_siteConfiguration.Author.FirstName} {_siteConfiguration.Author.LastName}",
AuthorEmail = _siteConfiguration.Author.Email,
diff --git a/src/web/MZikmund.Web/Controllers/SyndicationController.cs b/src/web/MZikmund.Web/Controllers/SyndicationController.cs
index 7d7f5229..de43dec3 100644
--- a/src/web/MZikmund.Web/Controllers/SyndicationController.cs
+++ b/src/web/MZikmund.Web/Controllers/SyndicationController.cs
@@ -30,7 +30,7 @@ public async Task Opml()
ContentInfo = categoryMap,
BlogUrl = $"{rootUrl}/blog",
RssUrl = $"{rootUrl}/rss",
- RssCategoryTemplate = $"{rootUrl}/rss/[catTitle]",
+ RssCategoryTemplate = $"{rootUrl}/rss/category/[catTitle]",
BlogCategoryTemplate = $"{rootUrl}/blog/categories/[catTitle]"
};
@@ -38,10 +38,21 @@ public async Task Opml()
return Content(xml, "text/xml");
}
- [HttpGet("rss/{categoryName?}")]
- public async Task Rss([MaxLength(64)] string? categoryName = null)
+ [HttpGet("rss")]
+ public async Task Rss() => await GenerateRssResponseAsync(new GetRssQuery(null, null));
+
+ [HttpGet("rss/category/{categoryName}")]
+ public async Task RssByCategory([MaxLength(64)] string categoryName) =>
+ await GenerateRssResponseAsync(new GetRssQuery(categoryName, null));
+
+ [HttpGet("rss/tag/{tagName}")]
+ public async Task RssByTag([MaxLength(64)] string tagName) =>
+ await GenerateRssResponseAsync(new GetRssQuery(null, tagName));
+
+ [HttpGet("atom/{categoryName?}")]
+ public async Task Atom([MaxLength(64)] string? categoryName = null)
{
- var xml = await _mediator.Send(new GetRssQuery(categoryName));
+ var xml = await _mediator.Send(new GetAtomQuery(categoryName));
if (string.IsNullOrWhiteSpace(xml))
{
return NotFound();
@@ -50,15 +61,14 @@ public async Task Rss([MaxLength(64)] string? categoryName = null
return Content(xml, "text/xml");
}
- [HttpGet("atom/{categoryName?}")]
- public async Task Atom([MaxLength(64)] string? categoryName = null)
+ private async Task GenerateRssResponseAsync(GetRssQuery query)
{
- var xml = await _mediator.Send(new GetAtomQuery(categoryName));
+ var xml = await _mediator.Send(query);
if (string.IsNullOrWhiteSpace(xml))
{
return NotFound();
}
- return Content(xml, "text/xml");
+ return Content(xml, "application/rss+xml; charset=utf-8");
}
}