Skip to content

Commit ef3da03

Browse files
McKnight, Eric D.Copilot
authored andcommitted
fix(rss): emit UTF-8 declaration and add standard channel metadata
The RSS/Atom feeds were written through a StringBuilder, so XmlWriter stamped the declaration as encoding="utf-16" even though OutputWriter persists the string as UTF-8. Strict parsers (System.Xml, Python ElementTree, many feed readers) reject the feed because the declared encoding disagrees with the on-disk bytes. Route the writer through a Utf8StringWriter that reports UTF-8 so the declaration matches. Also add the standard channel-level generator, language, and pubDate elements (parity with the mkdocs-rss feed) that readers expect. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 54bc693 commit ef3da03

2 files changed

Lines changed: 50 additions & 2 deletions

File tree

src/Netdocs.Plugins/RssPlugin.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private FeedItem ToItem(BlogPost post, string siteUrl)
107107
private string BuildRss(SiteContext site, string siteUrl, List<FeedItem> items)
108108
{
109109
var sb = new StringBuilder();
110-
using var writer = XmlWriter.Create(sb, XmlSettings());
110+
using var writer = XmlWriter.Create(new Utf8StringWriter(sb), XmlSettings());
111111

112112
writer.WriteStartDocument();
113113
writer.WriteStartElement("rss");
@@ -119,8 +119,14 @@ private string BuildRss(SiteContext site, string siteUrl, List<FeedItem> items)
119119
writer.WriteElementString("title", _feedTitle ?? site.Config.SiteName);
120120
writer.WriteElementString("link", siteUrl + "/");
121121
writer.WriteElementString("description", _feedDescription ?? site.Config.SiteDescription ?? site.Config.SiteName);
122+
if (!string.IsNullOrWhiteSpace(site.Config.Theme.Language))
123+
writer.WriteElementString("language", site.Config.Theme.Language);
122124
if (items.Count > 0)
125+
{
126+
writer.WriteElementString("pubDate", items[0].Date.ToString("r"));
123127
writer.WriteElementString("lastBuildDate", items[0].Date.ToString("r"));
128+
}
129+
writer.WriteElementString("generator", "Netdocs");
124130
if (_ttl > 0)
125131
writer.WriteElementString("ttl", _ttl.ToString());
126132

@@ -176,7 +182,7 @@ private string BuildRss(SiteContext site, string siteUrl, List<FeedItem> items)
176182
private string BuildAtom(SiteContext site, string siteUrl, List<FeedItem> items)
177183
{
178184
var sb = new StringBuilder();
179-
using var writer = XmlWriter.Create(sb, XmlSettings());
185+
using var writer = XmlWriter.Create(new Utf8StringWriter(sb), XmlSettings());
180186

181187
writer.WriteStartDocument();
182188
writer.WriteStartElement("feed", AtomNs);
@@ -234,6 +240,18 @@ private string BuildAtom(SiteContext site, string siteUrl, List<FeedItem> items)
234240
private static XmlWriterSettings XmlSettings() =>
235241
new() { Indent = true, Encoding = new UTF8Encoding(false) };
236242

243+
/// <summary>
244+
/// <see cref="XmlWriter"/> stamps the XML declaration with the writer's reported encoding.
245+
/// The default <see cref="StringWriter"/> reports UTF-16, so the declaration ends up as
246+
/// <c>encoding="utf-16"</c> even though the string is later persisted as UTF-8 — a mismatch
247+
/// that makes strict RSS parsers reject the feed. Reporting UTF-8 keeps the declaration
248+
/// consistent with the bytes actually written to disk.
249+
/// </summary>
250+
private sealed class Utf8StringWriter(StringBuilder sb) : StringWriter(sb)
251+
{
252+
public override Encoding Encoding => Encoding.UTF8;
253+
}
254+
237255
private static string? FrontMatterString(Page page, string key) =>
238256
page.FrontMatter.TryGetValue(key, out var v) ? v?.ToString() : null;
239257

tests/Netdocs.Core.Tests/RssPluginTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,36 @@ public async Task WritesRssFeedWithItemMetadata()
7373
Assert.Contains("rel=\"self\"", xml);
7474
}
7575

76+
[Fact]
77+
public async Task DeclaresUtf8AndParsesStrictly()
78+
{
79+
var site = Site(Post("Hello", "blog/hello", new DateTimeOffset(2024, 1, 2, 0, 0, 0, TimeSpan.Zero),
80+
categories: new[] { "News" }));
81+
await Run(new Dictionary<string, object?>(), site);
82+
83+
var path = Path.Combine(_root, "feed_rss_created.xml");
84+
var declaration = File.ReadLines(path).First();
85+
Assert.Contains("encoding=\"utf-8\"", declaration, StringComparison.OrdinalIgnoreCase);
86+
87+
// Parse from the raw bytes so the declared encoding is honored — this throws if the
88+
// declaration disagrees with the on-disk bytes.
89+
var doc = new System.Xml.XmlDocument();
90+
doc.Load(path);
91+
Assert.Equal("rss", doc.DocumentElement!.Name);
92+
}
93+
94+
[Fact]
95+
public async Task RssChannel_IncludesStandardMetadata()
96+
{
97+
var site = Site(Post("Hello", "blog/hello", new DateTimeOffset(2024, 1, 2, 0, 0, 0, TimeSpan.Zero)));
98+
await Run(new Dictionary<string, object?>(), site);
99+
100+
var xml = File.ReadAllText(Path.Combine(_root, "feed_rss_created.xml"));
101+
Assert.Contains("<generator>Netdocs</generator>", xml);
102+
Assert.Contains("<language>en</language>", xml);
103+
Assert.Contains("<pubDate>", xml);
104+
}
105+
76106
[Fact]
77107
public async Task PerPostTitleOverride_WinsOverPageTitle()
78108
{

0 commit comments

Comments
 (0)