Dynamic Sitemaps in ASP.NET MVC3 (Part 2)

If you aren’t familiar with the schema for a sitemap yet, take a moment to review the basic tags and their definitions.  After you have refreshed your memory, it should be clear that we can use a simple class to represent possible URLs in a sitemap:

/// <summary>
/// Holds a single url for the sitemap.
/// </summary>
public class SiteLink {
   public string Location { get; set; }
   public DateTime? LastModifed { get; set; }
    public ChangeFrequency? ChangeFrequency { get; set; }
    public double ?Priority { get; set; }
}

Notice that have omitted the serialization code.  In this case it is a simple matter of implementing IXmlSerializable and overriding WriteXml.  While you might wish to use the default XML serialization be aware of the custom tag names required, along with the custom formatting required for each of the values.

Collection of Links

Now that we have an individual representation of a link, we can represent a collection of links.  One way is to create a strongly-typed collection class and override its serialization to generate the proper XML:

/// <summary>
/// Holds a list of urls to be displayed in the sitemap.
/// </summary>
[XmlRootAttribute("urlset", Namespace = "http://www.sitemaps.org/schemas/sitemap/0.9", IsNullable = false)]
public class SitemapLinks : List<SiteLink>, IXmlSerializable {
    /// <summary>
    /// Serialize this list into xml.
    /// </summary>
    /// <param name="writer">XML Writer stream to serialize to.</param>
    public void WriteXml(System.Xml.XmlWriter writer) {
        foreach (var url in this) {
            writer.WriteStartElement("url");
            url.WriteXml(writer);
            writer.WriteEndElement();
        }
    }
}

Note that we are implementing the IXmlSerializable interface.  For brevity, I removed the ReadXml and GetSchema methods since they are not necessary for our purposes.  All the WriteXml method does is write each URL in its collection to the XML stream.

SitemapResult

Now that we have a serializable collection of links, it would be nice if we could easily create the sitemap without having to create a view for each sitemap action.

Fortunately, ASP.NET MVC3 provides us with the ability to create custom ActionResult classes and return them inside our controller’s action.  So all we need to do is create a special SitemapResult class that will be responsible for directly serializing the collection of links into the response.  This will eliminate the need to create a view.

public class SitemapResult : ActionResult {
 
    /// <summary>
    /// Get the list of sitemap urls.
    /// </summary>
    public SitemapLinks SitemapUrls { get; private set; }
 
    /// <summary>
    /// Construct this instance.
    /// </summary>
    /// <param name="sitemapUrls">List of urls in this sitemap.</param>
    public SitemapResult(SitemapLinks sitemapUrls) {
        this.SitemapUrls = sitemapUrls;
    }
    /// <summary>
    /// Writes the result to the output by serializing the list of urls 
      /// into the required
    /// sitemap format.
    /// </summary>
    /// <param name="context">Controller context.</param>
    public override void ExecuteResult(ControllerContext context) {
        // Send this all at once:
        context.HttpContext.Response.Buffer = true;
        // Clear any existing content and headers:
        context.HttpContext.Response.Clear();
        // We are sending xml content:
        context.HttpContext.Response.ContentType = "text/xml";
 
        // Serialize the list of urls into xml here:
        XmlSerializer xml = new XmlSerializer(typeof(SitemapLinks));
        xml.Serialize(context.HttpContext.Response.Output, this.SitemapUrls);
    }
}

All the action takes place in the overridden ExecuteResult method.  All we are doing is clearing any existing response, and setting the appropriate content-type.  We then use the default XML serialization to serialize our collection of links directly to the response as an XML file.  That is all there is to it.

Creating a Sitemap Action

Now all we need to do to generate a sitemap is create a sitemap action and return a SitemapResult, passing in a collection of sitemap links.  For example, here is the code I use to create the sitemap on my Home controller:

/// <summary>
/// Creates and returns an xml sitemap for this controller.
/// </summary>
/// <returns>XML Sitemap</returns>
public SitemapResult Sitemap() {
    SitemapLinks urls = new SitemapLinks();
    urls.Add(new SiteLink {
        Location = this.Url.Action("Index",null,null,Request.Url.Scheme)
    });
    urls.Add(new SiteLink {
        Location = this.Url.Action("Contact",null,null,Request.Url.Scheme)
    });
    urls.Add(new SiteLink {
        Location = this.Url.Action("About",null,null,Request.Url.Scheme)
    });
    urls.Add(new SiteLink {
        Location = this.Url.Action("TermsOfService",null,null,Request.Url.Scheme)
    });
    urls.Add(new SiteLink {
        Location = this.Url.Action("Privacy",null,null,Request.Url.Scheme)
    });
    return new SitemapResult(urls);
}

This is very similar to the manner in which we created the dynamic sitemap before, only now we are creating these special SiteLink classes which we can set different properties on.  Then we just return the SitemapResult and we don’t need to create a view.  The SitemapResult class takes care of writing to the response stream.

The other nice benefit of this approach is that if the protocol or format changes, it only needs to be updated in one place – the SitemapResult class, and all your existing code will automatically get the new updated format.