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.

Comments

  1. Martin S.'s avatar

    Martin S. 2 years ago

    Man, you're on such a run now! Keep it up and thanks for sharing!

  2. Jacob's avatar

    Jacob 2 years 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;        
            }
        }
    
  3. Rafael's avatar

    Rafael 2 years 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.

  4. Top site's avatar

    Top site 1 years 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.

  5. Sergejs Kravcenko's avatar

    Sergejs Kravcenko 1 years 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
    }
    


  6. Sergejs Kravcenko's avatar

    Sergejs Kravcenko 1 years 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;
            }
    
  7. Mattias's avatar

    Mattias 1 years ago

    Sergejs:
    I am new to ASP.NET and MVC 2, but i have a question about the diffrence between Joels example and yours.

    Joels example uses a custom ActionResult to directly output an XML string to the response, and as i understand it this dosen´t require a View (XmlSitemap.aspx).

    If I use the ActionFilter approach I need the View to output the result.

    Is this correct?

  8. Sergejs Kravcenko's avatar

    Sergejs Kravcenko 1 years ago

    Hi Mattis, no, actually View relly on the ActionResult naming. ActionFilter it modifies the logic of the response of our server to a client, in that way you can change the type of data to be sent to the client, whether it is an html/text type or xml type. Joel's example is also good, but I preffer to implement the logic of the response in the ActionFilter, not creating new ActionResult class based on it. In my example, just tag with the brackets [XmlActionResult] you ActionResult view, which you would like to have as XML output, than generate your urls, using the static class XmlSiteMap , use the method Create which is generic, where ListOfUrls it is a List, you have to generate this list before calling Create method, something like that:

    IList listOfUrls = Amhico.Data.XmlSiteMap.BuildXmlSiteMap(Url, _repository);
    
                var XmlSiteMapFeed = Amhico.Data.XmlSiteMap.Create(
                    listOfUrls, x => x.url, x => x.lastModified, x => x.changeFrequency, x => x.priority);
    
    return View(XmlSiteMapFeed);
    


    where BuildXmlSiteMap - this is your own method which generates links accordingly to your rule, i can post the example of mine:

     public static IList BuildXmlSiteMap(this UrlHelper urlHelper, ICommerceRepository _repository)
            {
                IList listOfUrls = new List();
    
                var pageSize = AmhicoSection.Current.Departments.PageSize;
                var pfPageSize = AmhicoSection.Current.ProductFamilies.PageSize;
    
                //adding Home Index
    
                listOfUrls.Add(new XmlSiteMapUrls
                {
                    url = String.Format("http://{0}{1}", HttpContext.Current.Request.Url.Authority, urlHelper.Action("Index", "Home")),
                    priority = 0.9,
                    lastModified = DateTime.Today, //? which date to show
                    changeFrequency = EnumsChangeFreq.daily.ToString(),
                    linkName = "Home"
                });
    
                var departments = _repository.TableDepartments().GetPaginatedDepartments(1,pageSize);
    
                for (int i = 1; i 


    That was a long time ago, try to search for solutions in google, there could be more, now I'm implementing instead of ActionFilter, using the FileEntity as a response.

    Good luck, and keep going! :)
  9. Mattias's avatar

    Mattias 1 years ago

    Thanks for the quick reply. :)

    I will keep going.

  10. As i told, i changed somethings in that example, can u look and if u want use it...

    http://www.rafaelalmeida.net/POST/mvc_sitemap_facil_e_eficiente

  11. Rory McCrossan's avatar

    Rory McCrossan 5 months ago

    Hi, many thanks for posting this, however I am having an issue. When I navigate to the sitemap at mydomain.com/sitemap.xml, the browser brings up a download open/save box, rather than rendering it in the browser. I believe this to be an issue with IIS7, but am unsure how to fix it. If I save the file, then upload it directly to the server and view it in the browser it is served correctly.

    Does anyone know what I can do to fix this problem?

    Regards

Follow me on Twitter

  1. "FindBananWithCriteria" #postdemofridayhumor 22 hours ago
  2. On the subject of things I wish I didn't have to know: http://t.co/Hdu9tCsw 2 days ago
  3. @erickdennel thanks! 2 days ago
follow me

Latest comments

  1. Berra S wrote "Read your post at http://joelabrahamsson.com/entry/using-xfo..." on PageData objects not returned as typed when using Page Type Builder and FindPagesWithCriteria
  2. Linus wrote "1 up for behaviour being as close as expected as possible!" on A common problem with Page Type Builder and UniqueValuePerLanguage set to false
  3. Joel Abrahamsson wrote "Hi Hans, Could it be that you previously didn't have Page..." on Page Type Builder 2.0 released

About this site

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