diff --git a/DysonNetwork.Zone/DysonNetwork.Zone.csproj b/DysonNetwork.Zone/DysonNetwork.Zone.csproj index 986f5ec..54f17db 100644 --- a/DysonNetwork.Zone/DysonNetwork.Zone.csproj +++ b/DysonNetwork.Zone/DysonNetwork.Zone.csproj @@ -19,11 +19,13 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + + diff --git a/DysonNetwork.Zone/Pages/Shared/_Layout.cshtml b/DysonNetwork.Zone/Pages/Shared/_Layout.cshtml index d403cc4..a28962e 100644 --- a/DysonNetwork.Zone/Pages/Shared/_Layout.cshtml +++ b/DysonNetwork.Zone/Pages/Shared/_Layout.cshtml @@ -16,6 +16,8 @@ + + @await RenderSectionAsync("Head", required: false) diff --git a/DysonNetwork.Zone/SEO/RssController.cs b/DysonNetwork.Zone/SEO/RssController.cs new file mode 100644 index 0000000..68bdf8f --- /dev/null +++ b/DysonNetwork.Zone/SEO/RssController.cs @@ -0,0 +1,71 @@ +using System.ServiceModel.Syndication; +using System.Xml; +using DysonNetwork.Shared.Models; +using DysonNetwork.Shared.Proto; +using Markdig; +using Microsoft.AspNetCore.Mvc; + +namespace DysonNetwork.Zone.SEO; + +[ApiController] +public class RssController(PostService.PostServiceClient postClient) : ControllerBase +{ + [HttpGet("rss")] + [Produces("application/rss+xml")] + public async Task Rss() + { + var feed = new SyndicationFeed( + "Solar Network Posts", + "Latest posts from Solar Network", + new Uri($"{Request.Scheme}://{Request.Host}/") + ); + + var items = new List(); + + // Fetch posts - similar to SitemapController, but for RSS we usually only want recent ones + // For simplicity, let's fetch the first page of posts + var request = new ListPostsRequest + { + OrderBy = "date", + OrderDesc = true, + PageSize = 20 // Get top 20 recent posts + }; + + var response = await postClient.ListPostsAsync(request); + + if (response?.Posts != null) + { + foreach (var protoPost in response.Posts) + { + var post = SnPost.FromProtoValue(protoPost); + var postUrl = post.AsUrl(Request.Host.ToString(), Request.Scheme); // Using the extension method + + var item = new SyndicationItem( + post.Title, + post.Content is not null ? Markdown.ToHtml(post.Content!) : "No content", // Convert Markdown to HTML + new Uri(postUrl), + post.Id.ToString(), + post.EditedAt?.ToDateTimeOffset() ?? + post.PublishedAt?.ToDateTimeOffset() ?? post.CreatedAt.ToDateTimeOffset() + ) + { + PublishDate = post.PublishedAt?.ToDateTimeOffset() ?? + post.CreatedAt.ToDateTimeOffset() // Use CreatedAt for publish date + }; + + items.Add(item); + } + } + + feed.Items = items; + + await using var sw = new StringWriter(); + await using var reader = XmlWriter.Create(sw, new XmlWriterSettings { Indent = true, Async = true }); + + var formatter = new Rss20FeedFormatter(feed); + formatter.WriteTo(reader); + await reader.FlushAsync(); + + return Content(sw.ToString(), "application/rss+xml"); + } +} \ No newline at end of file diff --git a/DysonNetwork.Zone/SEO/SitemapController.cs b/DysonNetwork.Zone/SEO/SitemapController.cs new file mode 100644 index 0000000..8902b90 --- /dev/null +++ b/DysonNetwork.Zone/SEO/SitemapController.cs @@ -0,0 +1,74 @@ +using DysonNetwork.Shared.Models; +using DysonNetwork.Shared.Proto; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using SimpleMvcSitemap; + +namespace DysonNetwork.Zone.SEO; + +[ApiController] +public class SitemapController( + AppDatabase db, + PostService.PostServiceClient postClient +) + : ControllerBase +{ + [HttpGet("sitemap.xml")] + public async Task Sitemap() + { + var nodes = new List + { + // Add static pages + new("/") + { ChangeFrequency = ChangeFrequency.Weekly, Priority = 1.0m }, + new("/about") + { ChangeFrequency = ChangeFrequency.Monthly, Priority = 0.8m }, + new("/posts") + { ChangeFrequency = ChangeFrequency.Daily, Priority = 0.9m } + }; + + // Add dynamic posts + var allPosts = await GetAllPosts(); + nodes.AddRange(allPosts.Select(post => + { + var uri = post.AsUrl(Request.Host.ToString(), Request.Scheme); + return new SitemapNode(uri) + { + LastModificationDate = post.EditedAt?.ToDateTimeUtc() ?? post.CreatedAt.ToDateTimeUtc(), + ChangeFrequency = ChangeFrequency.Monthly, Priority = 0.7m + }; + })); + + return new SitemapProvider().CreateSitemap(new SitemapModel(nodes)); + } + + private async Task> GetAllPosts() + { + var allPosts = new List(); + string? pageToken = null; + const int pageSize = 100; // Fetch in batches + + while (true) + { + var request = new ListPostsRequest + { + OrderBy = "date", + OrderDesc = true, + PageSize = pageSize, + PageToken = pageToken ?? string.Empty + }; + + var response = await postClient.ListPostsAsync(request); + + if (response?.Posts != null) + allPosts.AddRange(response.Posts.Select(SnPost.FromProtoValue)); + + if (string.IsNullOrEmpty(response?.NextPageToken)) + break; + + pageToken = response.NextPageToken; + } + + return allPosts; + } +} \ No newline at end of file diff --git a/DysonNetwork.Zone/SEO/SnPostExtensions.cs b/DysonNetwork.Zone/SEO/SnPostExtensions.cs new file mode 100644 index 0000000..8f23d56 --- /dev/null +++ b/DysonNetwork.Zone/SEO/SnPostExtensions.cs @@ -0,0 +1,12 @@ +using DysonNetwork.Shared.Models; +using Microsoft.AspNetCore.Mvc; + +namespace DysonNetwork.Zone.SEO; + +public static class SnPostExtensions +{ + public static string AsUrl(this SnPost post, string host, string scheme) + { + return $"{scheme}://{host}/p/{post.Slug ?? post.Id.ToString()}"; + } +}