Skip to content

Commit 7fd0050

Browse files
committed
Enhance SitemapSerializer to include default namespace and improve XML serialization
1 parent dec1a19 commit 7fd0050

File tree

3 files changed

+105
-73
lines changed

3 files changed

+105
-73
lines changed

src/X.Web.Sitemap/Serializers/SitemapSerializer.cs

Lines changed: 85 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,7 @@ public class SitemapSerializer : ISitemapSerializer
1919

2020
public SitemapSerializer()
2121
{
22-
_serializer = CreateSerializer();
23-
}
24-
25-
private static XmlSerializer CreateSerializer()
26-
{
27-
return new XmlSerializer(typeof(Sitemap));
22+
_serializer = new XmlSerializer(typeof(Sitemap));
2823
}
2924

3025
public string Serialize(ISitemap sitemap)
@@ -36,10 +31,19 @@ public string Serialize(ISitemap sitemap)
3631

3732
string xml;
3833

34+
var settings = new XmlWriterSettings { Indent = true };
35+
3936
using (var writer = new StringWriterUtf8())
4037
{
41-
_serializer.Serialize(writer, sitemap);
42-
38+
using (var xmlWriter = XmlWriter.Create(writer, settings))
39+
{
40+
var namespaces = new XmlSerializerNamespaces();
41+
// set default namespace to sitemap protocol
42+
namespaces.Add(string.Empty, "http://www.sitemaps.org/schemas/sitemap/0.9");
43+
44+
_serializer.Serialize(xmlWriter, sitemap, namespaces);
45+
}
46+
4347
xml = writer.ToString();
4448
}
4549

@@ -51,78 +55,94 @@ private static string XmlPostProcessing(string xml)
5155
// Post-process generated XML to remove xsi:nil="true" for <changefreq> elements.
5256
// This avoids changing the Url class while ensuring the output conforms to the
5357
// Sitemaps protocol (no nil attributes for optional elements).
54-
try
58+
59+
var doc = new XmlDocument();
60+
doc.LoadXml(xml);
61+
62+
var nodes = doc.GetElementsByTagName("changefreq");
63+
64+
const string xsiNs = "http://www.w3.org/2001/XMLSchema-instance";
65+
66+
// Ensure root has the sitemap default namespace and remove only the xsi namespace
67+
// declarations that are no longer needed (e.g. xmlns:xsi and xsi:schemaLocation).
68+
var root = doc.DocumentElement;
69+
70+
const string sitemapNs = "http://www.sitemaps.org/schemas/sitemap/0.9";
71+
72+
if (root is not null)
5573
{
56-
var doc = new XmlDocument();
57-
doc.LoadXml(xml);
74+
// Ensure default xmlns is present and correct
75+
root.SetAttribute("xmlns", sitemapNs);
5876

59-
var nodes = doc.GetElementsByTagName("changefreq");
60-
61-
const string xsiNs = "http://www.w3.org/2001/XMLSchema-instance";
77+
// Remove xmlns:xsi if present
78+
var xmlnsXsi = root.GetAttributeNode("xmlns:xsi");
6279

63-
// Ensure root has the sitemap default namespace and remove only the xsi namespace
64-
// declarations that are no longer needed (e.g. xmlns:xsi and xsi:schemaLocation).
65-
var root = doc.DocumentElement;
66-
67-
const string sitemapNs = "http://www.sitemaps.org/schemas/sitemap/0.9";
80+
if (xmlnsXsi is not null)
81+
{
82+
root.RemoveAttributeNode(xmlnsXsi);
83+
}
84+
85+
// Remove xsi:schemaLocation if present
86+
var schemaLoc = root.GetAttributeNode("schemaLocation", xsiNs);
6887

69-
if (root is not null)
88+
if (schemaLoc is not null)
7089
{
71-
// Ensure default xmlns is present and correct
72-
root.SetAttribute("xmlns", sitemapNs);
73-
74-
// Remove xmlns:xsi if present
75-
var xmlnsXsi = root.GetAttributeNode("xmlns:xsi");
76-
77-
if (xmlnsXsi is not null)
78-
{
79-
root.RemoveAttributeNode(xmlnsXsi);
80-
}
81-
82-
// Remove xsi:schemaLocation if present
83-
var schemaLoc = root.GetAttributeNode("schemaLocation", xsiNs);
84-
85-
if (schemaLoc is not null)
86-
{
87-
root.RemoveAttributeNode(schemaLoc);
88-
}
90+
root.RemoveAttributeNode(schemaLoc);
8991
}
92+
}
93+
94+
// Collect nodes first to avoid modifying the live XmlNodeList during iteration
95+
var list = new List<XmlElement>();
9096

91-
// Collect nodes first to avoid modifying the live XmlNodeList during iteration
92-
var list = new List<XmlElement>();
93-
94-
foreach (XmlNode node in nodes)
97+
foreach (XmlNode node in nodes)
98+
{
99+
if (node is XmlElement el)
95100
{
96-
if (node is XmlElement el)
97-
{
98-
list.Add(el);
99-
}
101+
list.Add(el);
100102
}
103+
}
101104

102-
foreach (var el in list)
105+
foreach (var el in list)
106+
{
107+
var attr = el.GetAttributeNode("nil", xsiNs);
108+
109+
if (attr != null && string.Equals(attr.Value, "true", StringComparison.OrdinalIgnoreCase))
103110
{
104-
var attr = el.GetAttributeNode("nil", xsiNs);
105-
106-
if (attr != null && string.Equals(attr.Value, "true", StringComparison.OrdinalIgnoreCase))
107-
{
108-
// remove the entire element to avoid deserializing an empty value into the enum
109-
var parent = el.ParentNode;
110-
111-
parent?.RemoveChild(el);
112-
}
111+
// remove the entire element to avoid deserializing an empty value into the enum
112+
var parent = el.ParentNode;
113+
114+
parent?.RemoveChild(el);
113115
}
116+
}
117+
118+
// Normalize priority values: ensure integer values serialize as one decimal (e.g. 1 -> 1.0)
119+
var priorityNodes = doc.GetElementsByTagName("priority");
120+
var priorityList = new List<XmlElement>();
114121

115-
using var writer = new StringWriterUtf8();
116-
117-
doc.Save(writer);
118-
119-
return writer.ToString();
122+
foreach (XmlNode node in priorityNodes)
123+
{
124+
if (node is XmlElement el)
125+
{
126+
priorityList.Add(el);
127+
}
120128
}
121-
catch
129+
130+
foreach (var p in priorityList)
122131
{
123-
// If anything goes wrong in post-processing, fall back to the original XML
124-
return xml;
132+
var text = p.InnerText?.Trim() ?? string.Empty;
133+
134+
// If the value is an integer (no decimal point) and a valid number, append .0
135+
if (!string.IsNullOrEmpty(text) && !text.Contains(".") && double.TryParse(text, out _))
136+
{
137+
p.InnerText = text + ".0";
138+
}
125139
}
140+
141+
using var writer = new StringWriterUtf8();
142+
143+
doc.Save(writer);
144+
145+
return writer.ToString();
126146
}
127147

128148
public Sitemap Deserialize(string xml)
@@ -133,7 +153,7 @@ public Sitemap Deserialize(string xml)
133153
}
134154

135155
using TextReader textReader = new StringReader(xml);
136-
156+
137157
var obj = _serializer.Deserialize(textReader);
138158

139159
if (obj is null)

tests/X.Web.Sitemap.Tests/UnitTests/SerializedXmlSaver/SerializeAndSaveTests.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public void It_Saves_The_XML_File_To_The_Correct_Directory_And_File_Name()
3434

3535
//--assert
3636
Assert.Contains("sitemapindex", result.FullName);
37-
37+
3838
Assert.Equal(directory.Name, result.Directory?.Name);
3939
Assert.Equal(fileName, result.Name);
4040
}
@@ -60,19 +60,19 @@ public void It_Returns_A_File_Info_For_The_File_That_Was_Created()
6060
Assert.Equal(expectedFileInfo.FullName, result.FullName);
6161
Assert.Equal(expectedFileInfo.Directory?.Name, result.Directory?.Name);
6262
}
63-
63+
6464
[Fact]
6565
public void Serialize_ValidInput_Succeeds()
6666
{
6767
//--arrange
68-
68+
6969
const string root = "https://www.example.com/";
70-
70+
7171
var sitemap = new X.Web.Sitemap.Sitemap
7272
{
7373
CreateUrl(root),
7474
CreateUrl($"{root}open-source", ChangeFrequency.Daily),
75-
CreateUrl($"{root}communities"),
75+
CreateUrl($"{root}communities", priority: 1),
7676
CreateUrl($"{root}contact-us"),
7777
CreateUrl($"{root}privacy-policy"),
7878
CreateUrl($"{root}code-of-conduct")
@@ -81,7 +81,7 @@ public void Serialize_ValidInput_Succeeds()
8181
var serializer = new SitemapSerializer();
8282

8383
var expectedFileInfo = new FileInfo("something/sitemap.xml");
84-
84+
8585
var xml = serializer.Serialize(sitemap);
8686

8787
var fileName = "sitemap.xml";
@@ -96,8 +96,8 @@ public void Serialize_ValidInput_Succeeds()
9696
Assert.Equal(expectedFileInfo.Directory?.Name, result.Directory?.Name);
9797
}
9898

99-
private Url CreateUrl(string url, ChangeFrequency? changeFrequency = null)
99+
private Url CreateUrl(string url, ChangeFrequency? changeFrequency = null, double? priority = null)
100100
{
101-
return Url.CreateUrl(url, DateTime.UtcNow.Date, changeFrequency: changeFrequency);
101+
return Url.CreateUrl(url, DateTime.UtcNow.Date, changeFrequency: changeFrequency, priority: priority ?? 0.5);
102102
}
103103
}

tests/X.Web.Sitemap.Tests/UnitTests/SitemapSerializerTests.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,16 @@ public void SerializeAndDeserialize_RoundTrip_Works()
4040
Assert.Single(deserialized);
4141
Assert.Equal("http://example.com/rt", deserialized[0].Location);
4242
}
43+
44+
[Fact]
45+
public void Serialize_RootHasDefaultSitemapNamespace()
46+
{
47+
var sitemap = new Sitemap { Url.CreateUrl("http://example.com/") };
48+
var serializer = new SitemapSerializer();
49+
50+
var xml = serializer.Serialize(sitemap);
51+
52+
// The root should start with the urlset element and default sitemap namespace
53+
Assert.Contains("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"", xml);
54+
}
4355
}

0 commit comments

Comments
 (0)