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()}";
+ }
+}