Dynamic sitemaps with ASP.NET MVC

Thursday, October 16, 2008 7:40 PM

I read Jeff Atwood’s post on The Importance of Sitemaps and thought I'd utilize one from the get-go on a site I'm creating for a side project. Seemed simple enough... Well, it didn't turn out a quick 5 minute task like I hoped.

Basically there were two issues. How to return raw content and how to get the encoding right.

How to return raw content was actually pretty easy to find. There's a method on System.Web.Mvc.Controller called Content that accepts a string, the content type, and the encoding. Perfect.

Now, this is probably easy to most, but I've never had to worry about encoding so this one took me a while to get right. First when I tried to serialize my xml document to a string, I ended up with utf-16 in the xml declaration. I knew that's not right because the sitemap specification specifically says it needs to be utf-8.

Help me Google!

The Google gods provided a lot of stuff... and a lot of rabbits to chase and a lot of less than perfect solutions. It was insane how much junk I came across. Many of which provided solutions that basically hacked the phrase "uft-8" into the xml declaration even though the document was physically still utf-16.

In the end I obviously figured it out - or at least, I figured out a solution that works for me and one I feel is somewhat slick. The code:

public ActionResult Sitemap()
{
   XNamespace xmlns = "http://www.sitemaps.org/schemas/sitemap/0.9";
   var root = new XElement(xmlns + "urlset");

   var repository = IoC.Resolve<IRepository<Location>>();
   foreach (var location in repository.FindValid())
   {
      object routeValues =
         new
            {
               state = location.State,
               location = location.Name,
               controller = "Agenda",
               action = "List"
            };

      root.Add(
         new XElement("url",
            new XElement("loc", GetUrl(routeValues)),
            new XElement("changefreq", "daily")));
   }

   var memoryStream = new MemoryStream();
   using (var writer = new StreamWriter(memoryStream, Encoding.UTF8))
      root.Save(writer);

   return Content(
      Encoding.UTF8.GetString(memoryStream.ToArray()),
      "text/xml",
      Encoding.UTF8);
}

The main part that took me so long to figure out is the last part where I'm trying to return the XElement I created. The keys ended up being the use of MemoryStream, StreamWriter with the UTF8 argument, and finally using Encoding.UTF8 to get the string to pass to Content().

That's it for the problems that held me up. I would like to add a final note about a problem I'm still having though. Notice the line where I'm calling GetUrl(). That's a utility method I wrote to get a full url from within the controllers. It's easy as pie from within the views, but I haven't found a built in easy way to do it within the controllers - or anywhere else where you don't have an HtmlHelper to play with. Here is that method, but I am still hopeful that an easier built in method exists that I just haven't found yet.

protected string GetUrl(object routeValues)
{
   var values = new RouteValueDictionary(routeValues);
   var context = new RequestContext(HttpContext, RouteData);

   var url = RouteTable.Routes.GetVirtualPath(context, values).VirtualPath;

   return new Uri(Request.Url, url).AbsoluteUri;
}

Happy coding!

Tags: .net, mvc
Comments (1)
Jeremy Sharp Jeremy Sharp 10/19/2008 9:08 PM

Cant wait to dig into this. Philip has thrwarted me slightly from breaking the seal on the MVC project types... I have list of related articles waiting for me to dive in your is now officially on the top of the list!! thanks for sharing! Jeremy