Multi-languages (alternate links) Sitemap Generator for Sitecore

Introduction

I needed to create a Sitemap generator for a Sitecore project to output a Sitemap.xml after publish operations. The site was multilingual and so should the sitemap.xml be.

One of the requirements were to include only content pages that are based on a base template that includes an ‘Indexable’ field, the other requirement was to incorporate the multilingual aspect of the site in the ‘alternate links’ format as shown in this google sitemap specification.

I researched Sitecore Marketplace if I could find a module that does this. Unfortunately the existing modules only use the included and excluded templates to retrieve Sitecore items in addition that they do not use the above google’s sitemap format. Therefor I had to write my own sitemap generator.

Retrieving Items

private List<Item> GetSitemapItems()
{
    var homeItem = database.GetItem(Sitecore.Context.Site.ContentStartPath);
    var results = new List<Item>();
    results.Add(homeItem);
    results.AddRange(homeItem.Axes.GetDescendants()
       .Where(i => i.IsDerived(I_BaseConstants.TemplateId)));
    results = results
       .Where(w => w.Fields[I_BaseConstants.IndexableFieldName].Value == "1");
    return results.ToList();
}

//Extension method that should be placed in an appropriate location
public static bool IsDerived(this Item item, ID templateId)
{
   if (item == null)
   {
      return false;
   }

   return !templateId.IsNull && item.IsDerived(item.Database.Templates[templateId]);
}

Not much to explain above, however in case your site contains thousands of items you could use ContentSearch and search using indices.

Generating Sitemap’s Xml String

The following code speaks for itself,

  private readonly XNamespace nsXhtml = "http://www.w3.org/1999/xhtml";
  private readonly XNamespace nsSitemap = "http://www.sitemaps.org/schemas/sitemap/0.9";
  public XDocument Generate(List<Item> items)
  {
     var sitemap = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
     var urlSet = new XElement(nsSitemap + "urlset",
     new XAttribute("xmlns", nsSitemap),
     new XAttribute(XNamespace.Xmlns + "xhtml", nsXhtml),
     from node in items
     select GetNode(node));

     sitemap.Add(urlSet);
     return sitemap;
  }

  protected XElement GetNode(Item item)
  {
     XElement urlNode = null;

     //SiteContextSwitcher is important since this may be called from 
     //the publisher site context
     using (new SiteContextSwitcher(Sitecore.Sites.SiteContext.GetSite("website")))
     {
      Sitecore.Links.UrlOptions urlOptions = 
          LinkManager.Providers["sitecore"].GetDefaultUrlOptions();
      urlOptions.AlwaysIncludeServerUrl = true;
      urlOptions.SiteResolving = true;

      urlNode = new XElement(nsSitemap + "url",
      new XElement(nsSitemap + "loc", LinkManager.Providers["sitecore"].GetItemUrl(item, urlOptions)),
      new XElement(nsSitemap + "lastmod", item.Statistics.Updated.ToString("yyyy-MM-dd"))
      //Other sitemap fields could be added here
      );

      XElement linkNode = null;
      foreach (var lang in item.Languages)
      {
         var versionCount = ItemManager.GetVersions(item, lang).Count;
         if (versionCount == 0)
           continue;
        urlOptions.Language = lang;
        linkNode = new XElement(nsXhtml + "link",
        new XAttribute("rel", "alternate"),
        new XAttribute("hreflang", lang.Name),
        new XAttribute("href", LinkManager.Providers["sitecore"].GetItemUrl(item, urlOptions)));
        urlNode.Add(linkNode);
      }
      return urlNode;
    }
  }

A couple of notes to note here is that this code may be run under the publisher site context, so the LinkManager may not resolve the correct site, therefore the SiteSwitcherContext is needed to switch to the main site context.

Another point is that LinkManager.GetItemUrl has been overriden in this project for generating a customzied  URL for the site. However the Sitemap needed to include the ServerUrl which was not part of the overriden implementation. To do this I have to use the original Sitecore link provider as seen above.

The above code would generate an xml string similar to the below:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <url>
    <loc>http://www.website.com/en/</loc>
    <xhtml:link rel="alternate" hreflang="de" href="http://www.website.com/de/" />
    <xhtml:link rel="alternate" hreflang="ar-JO" href="http://www.website.com/ar-JO/" />
    <xhtml:link rel="alternate" hreflang="es" href="http://www.website.com/es/"/>
  </url>

Finally, the above string would be written to a file using a simple statement as the below:

public void WriteToFile(string sitemapString)
{
   string path = Path.Combine(System.Web.Hosting.HostingEnvironment.ApplicationPhysicalPath, @"sitemap.xml"); 
   File.WriteAllText(path, sitemapString);
}

Calling this Sitemap generator

At this point, our code is ready but needs to be triggered at some event. One of the common events is the publish:completed event. This gets invoked after a publish operation has completed. In a production site, publishing should not be frequently started so it is not a big overhead if it takes several extra seconds after publishing to generate the Sitemap.xml. Plus, publishing usually means new content thus an updated Sitemap.xml is needed.

Sitecore Configuration

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
 <sitecore>
  <events>
   <event name="publish:complete">
    <handler type="Assemble.namespace.PublishCompletedHandler, Assembly" method="Execute" />
   </event>
  </events>
 </sitecore>
</configuration>

Event Handler

public class PublishCompletedHandler
{
  public void Execute(object sender, EventArgs e)
  {
   try
   {
     var sitemap = new Sitemap();
     var items = sitemap.GetSitemapItems();
     var sitemapXml = sitemap.GenerateXmlString(items);
     sitemap.WriteToFile(sitemapXml.ToString());
   }
   catch (Exception ex)
   {
     //Log exception
   }
  }
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s