+1-888-365-2779
Try Now
More in this section
Categories
Bloggers
Blogs RSS feed

New Sitefinity 4.4 SDK Sample: Sitemap Module

by Josh Morales

The Sitefinity SDK for version 4.4 is now available for download. Today we'll take a look at one of the new samples: the Sitefinity Sitemap Module. Although the end result of generating an xml sitemap of your website is a simple one, it demonstrates several key concepts that developers might find useful.

The initial release of this module includes all sitemap pages, as well as News, Events, and Blog Posts, as these are the content types that most often have dedicated Urls to be indexed. However, by following the code sample, you should be able to easily extend this to include the remaining native and even custom content Urls.

How It Works

The Sitemap module is made up of a few key components working together, and the goal of this sample is to demonstrate how they can be used in a real-world scenario.

Custom HttpHandler

The job of generating and displaying the xml sitemap is handled by a custom HttpHandler that builds the XML and returns it to the user. It appends all published pages, as well as the supported content items (News, Blogs, Events) using the XmlTextWriter.

// prepare response type
var response = context.Response;
response.ContentType = "text/xml";

// begin xml response
var writer = new XmlTextWriter(response.OutputStream, Encoding.UTF8) { Formatting = Formatting.Indented };
writer.WriteStartDocument();
writer.WriteStartElement("urlset");
writer.WriteAttributeString("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
writer.WriteAttributeString("xsi:schemaLocation", "http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd");
writer.WriteAttributeString("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9");

// … 

 

Custom RouteHandler

Generally, handlers are saved as .ashx (or sometimes .aspx) files so that they can be automatically served by ASP.NET. However, we want to use the path ~/sitemap.xml, which is generally a static file that is served directly by IIS. To do this, we need to tell Sitefinity to register this custom route and assign it to our custom handler.

All that is required to make this work is to inherit from IRouteHandler, then return the handler we defined earlier.

/// <summary>
/// Route Handler for serving the ~/sitemap.xml file path
/// </summary>
public class SiteMapRouteHandler : IRouteHandler
{
    /// <summary>
    /// Provides the object that processes the request.
    /// </summary>
    /// <param name="requestContext">An object that encapsulates information about the request.</param>
    /// <returns>
    /// An object that processes the request.
    /// </returns>
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        // simply return the SiteMap Handler
        return new SiteMapHttpHandler();
    }

Finally, the route is registered when the module is installed:

/// <summary>
/// Module for generating an XML Sitemap, including all pages and Sitefinity content types (Currently only supports News, Events, Blogs)
/// </summary>
public class SiteMapModule : ModuleBase
{
    /// <summary>
    /// Initializes the service with specified settings.
    /// </summary>
    /// <param name="settings">The settings.</param>
    public override void Initialize(ModuleSettings settings)
    {
        base.Initialize(settings);
        Config.RegisterSection<SiteMapConfig>();

        // handle the sitemap path
        RouteTable.Routes.Add("SiteMap", new Route("sitemap.xml", new SiteMapRouteHandler()));
    }

    //  ...
}

Including Content Items

Although Content items in Sitefinity have unique Urls, these are used dynamically in conjunction with a Content View widget (such as NewsView or BlogsView) to display the list and details pages. Because these widgets can be placed literally anywhere on any page, we need a way to identify and persist the location of the Sitefinity page that contains the Content View control for each content type.

Custom Configuration

Fortunately, Sitefinity 4 has a built-in mechanism for persisting settings such as these: Configuration. This is the component that creates and utilizes the .config files in the App_Data folder to store Sitefinity settings. By inheriting your configuration from these classes, you gain instant access to this feature for your own custom modules, including access via the Advanced Settings of the Sitefinity Administration Dashboard.

Sitefinity-4-Custom-Configuration

We'll explore building this custom Configuration class in more detail in a future post. For now just be aware that this configuration is used to associate each Content Type with a Sitefinity page to build the content url. It also has a boolean flag to determine whether or not to include those content items in the Sitemap.

Locating Default Pages

The slick new Sitefinity 4 Fluent API makes locating pages a breeze, with an intuitive interface to search for pages based on various conditions. In this example, we are searching for all pages that contain a NewsView control.

// retrieve all pages with a NewsView on them
var NewsPages = App.WorkWith().Pages()
    .Where(p => p.Page != null && p.ShowInNavigation && p.Page.Controls.Where(c => c.ObjectType.StartsWith(typeof(NewsView).FullName)).Count() > 0)
    .Get();

Then we can check the control properties to make sure the control matches the module and provider we are looking for.

// retrieve the news view control from the page
var newsView = page.Page.Controls.Where(c => c.ObjectType.StartsWith(typeof(NewsView).FullName)).First();
if (newsView == null) continue;

// determine if there is a modified control definition
var controlDefinition = newsView.Properties.Where(p => p.Name == "ControlDefinition").FirstOrDefault();
if (controlDefinition == null)
    // control uses default provider
    providerName = NewsManager.GetDefaultProviderName();
else
{
    // search for modified provider name in the control
    var providerNameProperty = controlDefinition.ChildProperties.Where(p => p.Name == "ProviderName").FirstOrDefault();
    if (providerNameProperty == null)
        // control is modified, but still uses default provider
        providerName = NewsManager.GetDefaultProviderName();
    else
        // get selected provider name
        providerName = providerNameProperty.Value;
}

// make sure specified provider matches the current provider we are working with
if (providerName != provider.Name) continue;

// make sure the mode of the control is not Details-only, skip this page if it is
var displayModeProperty = newsView.Properties.Where(p => p.Name == "ContentViewDisplayMode").SingleOrDefault();
if (displayModeProperty != null && displayModeProperty.Value == Telerik.Sitefinity.Web.UI.ContentUI.Enums.ContentViewDisplayMode.Detail.ToString()) continue;

Once we find the page we are looking for, we use our new configuration class to store its location so that it can be persisted or modified as needed.

// save default news page for this provider to the config
var newsPage = new ContentPage(newsConfig.Pages);
newsPage.DefaultPageUrl = page.GetFullUrl();
newsPage.Name = providerName;
newsPage.Include = true;
newsPage.ProviderName = providerName;
newsConfig.Pages.Add(newsPage);

This is repeated for each module. Note, however, that the Blogs module doesn't need a search through the pages for a BlogsView control, since this property is part of the module itself.

Sitefinity-4-Blog-Settings

Retrieving Content, Multiple Providers, and the Fluent API

Generating the actual page nodes of the sitemap is also fairly straightforward, thanks to the Fluent API. We simply retrieve a list of published pages, then build the url and append it to the xml.

using (var api = App.WorkWith())
{
    // append pages
    var pages = api.Pages().ThatArePublished().Where(p => p.ShowInNavigation == true && !p.IsBackend && p.NodeType == Telerik.Sitefinity.Pages.Model.NodeType.Standard).Get();
    foreach (var page in pages)
    {
        // build host
        var protocol = page.Page.RequireSsl ? "https://" : "http://";
        port = string.Concat(":", vars["SERVER_PORT"]);
        if (port == "80" || port == "443") port = string.Empty;
        host = protocol + vars["SERVER_NAME"] + port;

        writer.WriteStartElement("url");
        writer.WriteElementString("loc", host + VirtualPathUtility.ToAbsolute(page.GetFullUrl()));
        writer.WriteElementString("lastmod", GetLastMod(page.Page.LastModified));
        writer.WriteElementString("changefreq", GetChangeFreq(page.Page.LastModified));
        writer.WriteElementString("priority", "0.5");
        writer.WriteEndElement();
    }

    // ...
}

Working with Content items is similar, and a snap again with the Fluent API. However, we need to use our configuration to retrieve the default Sitefinity page url and append it to the Content Url.

Notice that here we are supporting multiple providers with the simple use of the SetContentProvider method of the Fluent API.

// append events
foreach (var eventsProvider in EventsManager.ProvidersCollection)
{
    // check index settings for matching provider from configuration
    var eventsConfig = config.ContentTypes[EventsModule.ModuleName].Pages.Elements.Where(e => e.ProviderName == eventsProvider.Name).FirstOrDefault();
    if (eventsConfig == null || !eventsConfig.Include) continue;

    // retrieve events from provider
    var events = App.Prepare().SetContentProvider(eventsProvider.Name).WorkWith().Events().Publihed().Get();
    foreach (var eventItem in events)
    {
        // build url
        var fullUrl = string.Format("{0}{1}{2}", host, VirtualPathUtility.ToAbsolute(eventsConfig.DefaultPageUrl), eventItem.Urls.Where(u => u.RedirectToDefault == false).First().Url);

        // append to sitemap
        writer.WriteStartElement("url");
        writer.WriteElementString("loc", fullUrl);
        writer.WriteElementString("lastmod", eventItem.LastModified.ToString("yyyy-MM-ddThh:mm:sszzzz"));
        writer.WriteElementString("changefreq", "monthly");
        writer.WriteElementString("priority", "0.5");
        writer.WriteEndElement();
    }
}

After including all pages and supported content items, the handler is complete and the sitemap is returned to the user.

Sitefinity-4-Sitemap-Module

Download the SDK

The new Sitemap module sample is just one of many updates to the Sitefinity 4 SDK and demonstrates the use of several Sitefinity components in a real-world scenario.

Take a moment to download the SDK and try it out for yourself! Be sure to send me any feedback, comments or suggestions via the Sitefinity 4 SDK Discussion Forum.

5 comments

Leave a comment
  1. Mike Dec 22, 2011
    To me there seems to be a lot wrong with this. 

    First of all, this approach makes it impossible to create a generic sitemap module, you would have to customize this code for multiple content types. If you want to reuse this module I don't think that's possible. Because you can never know what other content types are available in the future or in other Sitefinity instances.

    Second of all, why do we have to dig into controls on a page for this? You (and I) have got a serious problem if URL inspection of the system depends on what controls are on certain pages! To think that we simply take the fist such control we find to base our sitemap on is really strange (the code sample on this page is a huge code smell for sure).

    Which leads me to believe that content items don't have canonical URLs. Is that correct? In which case I must seriously question your CMS architecture. That a website has news and that there are 5 news articles published should just be queryable through a simple API. Each content item should be addressable with a URI, welcome to the web!

    In fact, I would go as far as to say that any CMS should have a hierarchical list of published URLs ready at any time, without having to reach (literally) inside pages.

    What benefit is there in content items having multiple URLs based off the page that you just so happen to add a control to? It goes directly against SEO guidelines, it's almost 2012, we can never buy a CMS that will dilute page rank out-of-the-box, it's a no-brainer!

    Why would published content items cease to exist in a sitemap if there is currently no control that displays links to the content items? It's publication by sheer coincidence. How could any self respecting content outlet publish like that? 

    If publication ultimately depends on controls on a page (still can't really believe it), then what use is having workflow? "I said it should be published by noon, why is it not in our sitemap?" "Because we updated a control for listings and now we must update the sitemap module first." "What?!"

    Why is the fluent API so good, and yet we have to search for "contentviewdisplaymode" by string just to know if the control is actually displaying a list of content items in just the right way?

    Why?

    Either I must be mistaken or this CMS is a non-starter for any serious web publication.

    Gabe?
  2. Josh Dec 28, 2011

    Hi Mike, thank you for your feedback.

    This example primarily serves as an example project for the various components it uses (configuration, Fluent API, etc) rather than an out-of-the-box solution. Although it certainly can be used as such with some adjustments.

    The searching for controls on a page just serves to populate "default" locations automatically for the user. You could easily bypass this and specify them explicitly and manually.

    However, this is indeed a result of the way URLs work in Sitefinity. Urls for content items uniquely ideinfity a content item within the content type, not the direct url to the content item in the website.

    The reason for this is because content items do not live on a specific page, but rather are defined by the content view widget (NewsView, BlogsView, etc) and where you place them on a page. Rather than forcing you to use /news/url or /blogs/url you can say /about-us/news/url or /our-blogs/blog-a/url.

    This also allows you to "break up" your news into different sections, departments, sublevels, etc based on things like categories, tags, publication date, or other classification. This is the way Sitefinity has handled content items and urls for many versions (well before version 4).

    Each content item should indeed have its own "default" page, and inded the blogs module does this. I have already submitted this feedback to the team so that each item can indeed have a default landing page to ensure a safe SEO. For the most part this isn't a problem, since most content items live on a single page.

    I will keep watching this issue as it develops, and will keep the community posted with any changes or updates. Once again, I thank you again for your feedback!

  3. Ben Oct 19, 2012
    Here's some rough code to grab dynamic types as well if you've created any using Module Builder, call it using AppendDynamicType(config, host, writer, "Telerik.Sitefinity.DynamicTypes.Model.FeaturedProjects.FeaturedProject");

       private static void AppendDynamicType(SiteMapConfig config, string host, XmlTextWriter writer, string type)
       {
           DynamicModuleManager dynamicModuleManager = DynamicModuleManager.GetManager();
                var dynamicType = TypeResolutionService.ResolveType(type);

                ContentPage dynamicTypeConfig = null;

           try
           {
                    dynamicTypeConfig = config.ContentTypes[dynamicType.Name].Pages.Elements.FirstOrDefault();
           }
           catch (Exception ex)
           {
               Logger.Writer.Write(ex);
           }


                if (dynamicTypeConfig != null && dynamicTypeConfig.Include)
           {
                    var dyanamicTypes =
                        dynamicModuleManager.GetDataItems(dynamicType).Where(
                       p => p.Status == Telerik.Sitefinity.GenericContent.Model.ContentLifecycleStatus.Live);

                    foreach (var dyanamicType in dyanamicTypes)
               {
                   var fullUrl = string.Format("{0}{1}{2}", host,
                                                    VirtualPathUtility.ToAbsolute(dynamicTypeConfig.DefaultPageUrl),
                                               dyanamicType.SystemUrl);

                   // append to sitemap
                   writer.WriteStartElement("url");
                   writer.WriteElementString("loc", fullUrl);
                   writer.WriteElementString("lastmod", dyanamicType.LastModified.ToString("yyyy-MM-ddThh:mm:sszzzz"));
                   writer.WriteElementString("changefreq", "monthly");
                   writer.WriteElementString("priority", "0.5");
                   writer.WriteEndElement();
               }
           }
       }
  4. Ben Oct 19, 2012
    Here's some rough code to grab dynamic types as well if you've created any using Module Builder, call it using AppendDynamicType(config, host, writer, "Telerik.Sitefinity.DynamicTypes.Model.FeaturedProjects.FeaturedProject");

       private static void AppendDynamicType(SiteMapConfig config, string host, XmlTextWriter writer, string type)
       {
           DynamicModuleManager dynamicModuleManager = DynamicModuleManager.GetManager();
                var dynamicType = TypeResolutionService.ResolveType(type);

                ContentPage dynamicTypeConfig = null;

           try
           {
                    dynamicTypeConfig = config.ContentTypes[dynamicType.Name].Pages.Elements.FirstOrDefault();
           }
           catch (Exception ex)
           {
               Logger.Writer.Write(ex);
           }


                if (dynamicTypeConfig != null && dynamicTypeConfig.Include)
           {
                    var dyanamicTypes =
                        dynamicModuleManager.GetDataItems(dynamicType).Where(
                       p => p.Status == Telerik.Sitefinity.GenericContent.Model.ContentLifecycleStatus.Live);

                    foreach (var dyanamicType in dyanamicTypes)
               {
                   var fullUrl = string.Format("{0}{1}{2}", host,
                                                    VirtualPathUtility.ToAbsolute(dynamicTypeConfig.DefaultPageUrl),
                                               dyanamicType.SystemUrl);

                   // append to sitemap
                   writer.WriteStartElement("url");
                   writer.WriteElementString("loc", fullUrl);
                   writer.WriteElementString("lastmod", dyanamicType.LastModified.ToString("yyyy-MM-ddThh:mm:sszzzz"));
                   writer.WriteElementString("changefreq", "monthly");
                   writer.WriteElementString("priority", "0.5");
                   writer.WriteEndElement();
               }
           }
       }
  5. James Apr 17, 2013
    Josh, thanks for the example. Ben - I was needing this for module builder content - wasn't sure how to get the DefaultPageUrl. Thanks!

    Leave a comment