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. 2 years ago
Man, you're on such a run now! Keep it up and thanks for sharing!
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; } }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.
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.
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 }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; }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?
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; iThat 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! :)
Mattias 1 years ago
Thanks for the quick reply. :)
I will keep going.
Rafael da Silva Almeida 1 years ago
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
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