ASP.NET October 21, 2009

XML sitemap with ASP.NET MVC

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.

A custom ActionResult

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.

My implementation

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;        
    }
}

Download the source code

You are welcome to download the source code as a Visual Studio 2008 project here.

Using it in an action method

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;
    }
}

Conclusion and feedback

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.

Joel Abrahamsson

Joel Abrahamsson

I'm a passionate web developer and systems architect living in Stockholm, Sweden. I work as CTO for a large media site and enjoy developing with all technologies, especially .NET, Node.js, and ElasticSearch. Read more

Comments

comments powered by Disqus

More about ASP.NET