I have previously written about the benefits of, and how to create a XML sitemap with ASP.NET. That time I used a custom HTTP handler to generate the sitemap. As I was building this site using ASP.NET MVC I decided to seek another solution where the sitemap was generated by a controller. I could of course let the action method in my controller return a content result with the XML sitemap as a string. That would however require me to do a lot of text parsing if I wanted to write tests for the action.
Luckily I stumbled upon this post by Keyvan Nayyeri where he describes how he created a custom action result for sitemaps. I really liked that idea as it meant that my action method could return the data that should be rendered in the sitemap, not the actual sitemap, making the action much easier to test and providing much better separation of concerns.
What Keyvan did was that he let his action method return an instance of a custom class inheriting from ActionResult. That custom class expected a set of blog posts as a constructor parameter. In the ExecuteResult method, which a descendant of ActionResult must implement, he created a XML sitemap for the blog posts using an XmlWriter which he finally wrote to the response.
I really liked the idea of a custom ActionResult but I wasn’t crazy about the implementation that Keyvan described for my site. First of all I didn’t want to limit myself to only one specific class for which to render URLs in the sitemap. I also thought that it is the controllers job to determine what priority each sitemap URL should have. Finally I also thought that the actual creation of the XML could be done in a slightly more elegant way. All in all, this was enough to overcome my laziness and I set out to create my own implementation.
First of all I created an enum for change frequencies (namespaces and using statements omitted for brevity).
public enum ChangeFrequency
{
Always,
Hourly,
Daily,
Weekly,
Monthly,
Yearly,
Never
}
Then I created an interface for sitemap items, ISitemapItem.
public interface ISitemapItem
{
string Url { get; }
DateTime? LastModified { get; }
ChangeFrequency? ChangeFrequency { get; }
float? Priority { get; }
}
I also created an implementation of that interface, SitemapItem. By creating both an interface and this standard implementation we have both the option to let our model objects be sitemap items themselves or map them to the standard implementation in the controller.
public class SitemapItem : ISitemapItem
{
public SitemapItem(string url)
{
Url = url;
}
public string Url { get; set; }
public DateTime? LastModified { get; set; }
public ChangeFrequency? ChangeFrequency { get; set; }
public float? Priority { get; set; }
}
Finally I created the ActionResult class, XmlSitemapResult. As you can see below it expects a set of ISitemapItems as a constructor parameter. The ExecuteResult method generates the XML using the (wonderful) XDocument class and writes it to the response.
public class XmlSitemapResult : ActionResult
{
private IEnumerable<ISitemapItem> _items;
public XmlSitemapResult(IEnumerable<ISitemapItem> items)
{
_items = items;
}
public override void ExecuteResult(ControllerContext context)
{
string encoding = context.HttpContext.Response.ContentEncoding.WebName;
XDocument sitemap = new XDocument(new XDeclaration("1.0", encoding, "yes"),
new XElement("urlset", XNamespace.Get("http://www.sitemaps.org/schemas/sitemap/0.9"),
from item in _items
select CreateItemElement(item)
)
);
context.HttpContext.Response.ContentType = "application/rss+xml";
context.HttpContext.Response.Flush();
context.HttpContext.Response.Write(sitemap.Declaration + sitemap.ToString());
}
private XElement CreateItemElement(ISitemapItem item)
{
XElement itemElement = new XElement("url", new XElement("loc", item.Url.ToLower()));
if(item.LastModified.HasValue)
itemElement.Add(new XElement("lastmod", item.LastModified.Value.ToString("yyyy-MM-dd")));
if(item.ChangeFrequency.HasValue)
itemElement.Add(new XElement("changefreq", item.ChangeFrequency.Value.ToString().ToLower()));
if(item.Priority.HasValue)
itemElement.Add(new XElement("priority", item.Priority.Value.ToString(CultureInfo.InvariantCulture)));
return itemElement;
}
}
You are welcome to download the source code as a Visual Studio 2008 project here.
To use the XmlSitemapResult class in an action method all you have to do is create an IEnumerable<ISitemapItem> and return a new instance of XmlSitemapResult using your set of items as constructor parameter. As an example, my SitemapController class for this site looks something like this (I’ve omitted the AddPages method, the namespace and using statements for brevity):
public class SitemapController : Controller
{
ExtendedBlogHandlerFacade _blogHandlerFacade;
Blog _blog;
List<ISitemapItem> _items;
public SitemapController(ExtendedBlogHandlerFacade blogHandlerFacade)
{
_blogHandlerFacade = blogHandlerFacade;
}
public XmlSitemapResult Xml(Blog blog)
{
_blog = blog;
_items = new List<ISitemapItem>();
AddEntries();
AddPages();
return new XmlSitemapResult(_items);
}
private void AddEntries()
{
int totalItems;
EntryCollection entries = _blogHandlerFacade.GetLatestEntries(_blog, 1, int.MaxValue - 1, out totalItems);
foreach (Entry entry in entries)
{
_items.Add(
new SitemapItem(entry.GetUrl(ControllerContext.RequestContext))
{
LastModified = GetEntryLastModified(entry),
Priority = 1
}
);
}
}
private DateTime GetEntryLastModified(Entry entry)
{
if (entry.Updated > entry.PublicationStart)
return entry.Updated;
return entry.PublicationStart;
}
}
While I like this solution, much thanks to it providing good testability for application logic, I’m sure that there are even better ways to generate XML sitemaps with ASP.NET MVC. If you know of one, don’t hesitate to tell me. Of course any other type of feedback is also very welcome!
PS. For updates about new posts, sites I find useful and the occasional rant you can follow me on Twitter. You are also most welcome to subscribe to the RSS-feed.
This blog is built with EPiServer Community, EPiServer CMS, ASP.NET MVC and a bunch of other great products. The source code is available for download at the projects page, where you also can read more about this site and my other projects.
read more
Comments
Martin S. 10 months ago
Man, you're on such a run now! Keep it up and thanks for sharing!
Jacob 7 months ago
Just wanted to say thanks for this. I had to change the ActionResult a little bit to fix a few errors for google.
public class XmlSitemapResult : ActionResult { private static readonly XNamespace xmlns = "http://www.sitemaps.org/schemas/sitemap/0.9"; private IEnumerable _items; public XmlSitemapResult(IEnumerable items) { _items = items; } public override void ExecuteResult(ControllerContext context) { string encoding = context.HttpContext.Response.ContentEncoding.WebName; XDocument sitemap = new XDocument(new XDeclaration("1.0", encoding, "yes"), new XElement(xmlns + "urlset", from item in _items select CreateItemElement(item) ) ); context.HttpContext.Response.ContentType = "application/rss+xml"; context.HttpContext.Response.Flush(); context.HttpContext.Response.Write(sitemap.Declaration + sitemap.ToString()); } private XElement CreateItemElement(ISitemapItem item) { XElement itemElement = new XElement(xmlns + "url", new XElement(xmlns + "loc", item.Url.ToLower())); if(item.LastModified.HasValue) itemElement.Add(new XElement(xmlns + "lastmod", item.LastModified.Value.ToString("yyyy-MM-dd"))); if(item.ChangeFrequency.HasValue) itemElement.Add(new XElement(xmlns + "changefreq", item.ChangeFrequency.Value.ToString().ToLower())); if(item.Priority.HasValue) itemElement.Add(new XElement(xmlns + "priority", item.Priority.Value.ToString(CultureInfo.InvariantCulture))); return itemElement; } }Rafael 6 months ago
I liked it, and did some emprovement, i am a braziliam guy and my blog is in portuguese, but the code i am sure u can understand. If u have some problem about i do it, plz let me know.
thx for your code and u were wellcome to replay my comment there.
Top site 6 months ago
How does your sitemap script generate the URLs, does it crawl the site or is it based on some feature in the server? I ask because I'm looking for a xml sitemap solution for a large dynamic site that has millions of pages created on the fly.
Sergejs Kravcenko 4 months ago
Good solution, but I would suggest more generic solution: also you should include xsi SchemaLocationUrls, and xsi namespaces.
public static class XmlSiteMap { private const string Url = "url"; private const string UrlSet = "urlset"; private const string UrlSetSchemaLocation = "schemaLocation"; private const string UrlXsi = "xsi"; private const string UrlSetSchemaLocationUrl = "http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"; private static readonly XNamespace xmlns = "http://www.sitemaps.org/schemas/sitemap/0.9"; private static readonly XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance"; public static string Create(IList items, Func url, Func lastModified, Func changeFrequency, Func priority) { XDocument xDoc = new XDocument(new XDeclaration("1.0", HttpContext.Current.Response.ContentEncoding.WebName, "yes"), new XElement(xmlns + UrlSet, new XAttribute(XNamespace.Xmlns + UrlXsi, xsi), new XAttribute(xsi + UrlSetSchemaLocation, UrlSetSchemaLocationUrl), from local in items select CreateUrlNode(local, url(local), lastModified(local), changeFrequency(local), priority(local)) ) ); return (xDoc.Declaration + xDoc.ToString()); } private static XElement CreateUrlNode(T item, string url, DateTime? lastModified, string changeFrequency, double priority) { XElement itemElement = new XElement(xmlns + "url"); itemElement.Add(new XElement(xmlns + "loc", url.ToLower())); itemElement.Add(new XElement(xmlns + "lastmod", CreateXmlDate(lastModified))); itemElement.Add(new XElement(xmlns + "changefreq", changeFrequency)); itemElement.Add(new XElement(xmlns + "priority", priority.ToString(CultureInfo.InvariantCulture))); return itemElement; } private static string CreateXmlDate(DateTime? date) { string item = string.Empty; if (date.HasValue) { item = string.Format("{0}-{1}-{2}T{3}:{4}:{5}+00:00", new object[] { date.Value.Year, date.Value.Month.ToString("00"), date.Value.Day.ToString("00"), date.Value.Hour.ToString("00"), date.Value.Minute.ToString("00"), date.Value.Second.ToString("00") }); } return item; }XmlSiteMapUrls class
public class XmlSiteMapUrls { public string url { get; set; } public DateTime? lastModified { get; set; } public string changeFrequency { get; set; } public double priority { get; set; } }Iterating in IList of urls:
var xmlString = XmlSiteMap.Create( listOfUrls, x => x.url, x => x.lastModified, x => x.changeFrequency, x => x.priority);where listOfUrls is a IList.
and instead writing inheritted class of new ActionResult, you can write ActionFilterAttribute
something like this
public class XmlActionResult : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext filterContext) { if (filterContext.Result is ViewResult) { var result = filterContext.Result as ViewResult; if (!String.IsNullOrEmpty(result.ViewName)) { filterContext.Result = new ContentResult { Content = result.ViewName.ToString(), ContentType = "text/xml", ContentEncoding = System.Text.Encoding.UTF8 }; } } } }finally you map your ActionResult with created FilterAttribute
[XmlActionResult] public ActionResult XmlSiteMap() { return View(stringXml); //pass here you stringXML output }Sergejs Kravcenko 4 months ago
Sorry I forgot to write lt & gt tags, so method in static class will be:
public static string Create<T>(IList<T> items, Func<T, string> url, Func<T, DateTime?> lastModified, Func<T, string> changeFrequency, Func<T, double> priority) { XDocument xDoc = new XDocument(new XDeclaration("1.0", HttpContext.Current.Response.ContentEncoding.WebName, "yes"), new XElement(xmlns + UrlSet, new XAttribute(XNamespace.Xmlns + UrlXsi, xsi), new XAttribute(xsi + UrlSetSchemaLocation, UrlSetSchemaLocationUrl), from local in items select CreateUrlNode<T>(local, url(local), lastModified(local), changeFrequency(local), priority(local)) ) ); return (xDoc.Declaration + xDoc.ToString()); } private static XElement CreateUrlNode<T>(T item, string url, DateTime? lastModified, string changeFrequency, double priority) { XElement itemElement = new XElement(xmlns + "url"); itemElement.Add(new XElement(xmlns + "loc", url.ToLower())); itemElement.Add(new XElement(xmlns + "lastmod", CreateXmlDate(lastModified))); itemElement.Add(new XElement(xmlns + "changefreq", changeFrequency)); itemElement.Add(new XElement(xmlns + "priority", priority.ToString(CultureInfo.InvariantCulture))); return itemElement; }