Creating XML Sitemap for the Helix solution
I am working on a solution that already has HTML sitemap as a part of Navigation feature. Now I got a request to add also a basic XML sitemap with common set requirements. Habitat ships with an interface template _Navigable, so let's extend this template by adding a checkbox field called
ShowInSitemap, stating whether a particular page will be shown in that sitemap:

In order to start, we need to create a handler. Having handlers in web.config is not the desired way of doing things, it will require also doing configuration transform for the deployments, so let's do things in a Sitecore way (Feature.Navigation.config file):
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<httpRequestBegin>
<processor type="Platform.Feature.Navigation.Pipelines.SitemapHandler, Platform.Feature.Navigation"
patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.CustomHandlers, Sitecore.Kernel']">
</processor>
</httpRequestBegin>
<preprocessRequest>
<processor type="Sitecore.Pipelines.PreprocessRequest.FilterUrlExtensions, Sitecore.Kernel">
<param desc="Allowed extensions">aspx, ashx, asmx, xml</param>
</processor>
</preprocessRequest>
</pipelines>
</sitecore>
</configuration>
We rely on httpRequestBegin pipeline and incline our new SitemapHandler from Navigation feature right before CustomHandlers processor.
SitemapHandler is an ordinary pipeline processor for httpRequestBegin pipeline, so is inherited from HttpRequestProcessor:
public class SitemapHandler : HttpRequestProcessor
{
const string sitemapHandler = "sitemap.xml";
private readonly INavigationRepository _navigationRepository;
public SitemapHandler()
{
_navigationRepository = new NavigationRepository(RootItem);
}
public override void Process(HttpRequestArgs args)
{
if (Context.Site == null
|| args == null
|| string.IsNullOrEmpty(Context.Site.RootPath.Trim())
|| Context.Page.FilePath.Length > 0
|| !args.Url.FilePath.Contains(sitemapHandler))
{
return;
}
Response.ClearHeaders();
Response.ClearContent();
Response.ContentType = "text/xml";
try
{
var navigationItems = _navigationRepository.GetSitemapItems(RootItem);
string xml = new XmlSitemapService().BuildSitemapXML(flatItems);
Response.Write(xml);
}
finally
{
Response.Flush();
Response.End();
}
}
private Item RootItem => Context.Site.GetRootItem();
private HttpResponse Response => HttpContext.Current.Response;
}
And XmlSitemapService code below:
public class XmlSitemapService
{
public string CreateSitemapXml(IEnumerable<NavigationItem> items)
{
var doc = new XmlDocument();
var declarationNode = doc.CreateXmlDeclaration("1.0", "UTF-8", null);
doc.AppendChild(declarationNode);
var urlsetNode = doc.CreateElement("urlset");
var xmlnsAttr = doc.CreateAttribute("xmlns");
xmlnsAttr.Value = "http://www.sitemaps.org/schemas/sitemap/0.9";
urlsetNode.Attributes.Append(xmlnsAttr);
doc.AppendChild(urlsetNode);
foreach (NavigationItem itm in items)
{
doc = CreateSitemapRecord(doc, itm);
}
return doc.OuterXml;
}
private XmlDocument CreateSitemapRecord(XmlDocument doc, NavigationItem item)
{
string link = item.Url;
string lastModified = HttpUtility
.HtmlEncode(item.Item.Statistics.Updated.ToString("yyyy-MM-ddTHH:mm:sszzz"));
XmlNode urlsetNode = doc.LastChild;
XmlNode url = doc.CreateElement("url");
urlsetNode.AppendChild(url);
XmlNode loc = doc.CreateElement("loc");
url.AppendChild(loc);
loc.AppendChild(doc.CreateTextNode(link));
XmlNode lastmod = doc.CreateElement("lastmod");
url.AppendChild(lastmod);
lastmod.AppendChild(doc.CreateTextNode(lastModified));
return doc;
}
}
Also, NavigationItem is a custom POCO:
public class NavigationItem
{
public Item Item { get; set; }
public string Title { get; set; }
public string Url { get; set; }
public bool IsActive { get; set; }
public int Level { get; set; }
public NavigationItems Children { get; set; }
public string Target { get; set; }
public bool ShowChildren { get; set; }
}





