December 2014 - Posts

One of the SEO best practices everyone should trying to implement on their sites relates to URLs with spaces. I’m sure you’ve seen a URL with an encoded space before - %20. Yuck. Not only does it look bad to your site’s visitors, but search engines don’t particularly like it either. From what I’ve read (e.g. http://goo.gl/mwNpgP), crawlers like Google definitely prefer to see dashes where humans like to see spaces.

I’ve seen a few recommendations for how to deal with this in Sitecore before. If you’re currently using encodeNameReplacements, InvalidItemNameChars, and/or ItemNameValidation - I think I have discovered a better way...

Sitecore generates URLs for items using the name of the item (and its parent items in the content tree), therefore what we really need is for items to be named something like “my-blog-post” instead of “My Blog Post”. Having content authors follow a naming convention is probably too much to ask, and what if they forget? Plus, it’s not particularly friendly for them to look at in the Sitecore content tree either.

What if we could let them name the item however they want and let them continue to see the name they entered in the content tree, but actually have the item named in an SEO friendly way in the database? What mechanism does Sitecore give us to allow an item to be named one thing, but display using a different name in the content tree? Perhaps you’ve seen or used the Display Name before?


Display Name in the Sitecore Ribbon

The essence of my solution to the spaces in URLs problem is to handle the Sitecore events for an item being added or renamed, detect items with a space in their name, rename them to use a dash in place of a space, and copy the original name into the Display Name so that content authors don’t see what we’ve done.

Here’s the complete code for a solution that I’m using on my current project. It makes use of a technique that I mentioned in a previous blog post (see Subscribing to Sitecore Events in a PreApplicationStartMethod) to subscribe to the item:added and item:renamed events, and then performs the above logic. I also threw in a .ToLower() on the item name while I’m at it because apparently search engines prefer to see all lowercase URLs (note - I’m also using the lowercaseUrls attribute on the linkManager, so this shouldn’t really be necessary - up to you if you want to include it in your solution or not).

Oh - that regex you see in there takes care of cases where an item was named with spaces surrounding a dash. Otherwise “Account - Change Password” would become “account---change-password”.


using System;
using System.Text.RegularExpressions;
using System.Web;

using Sitecore.Data.Items;
using Sitecore.Events;

[assembly: PreApplicationStartMethod(typeof([Namespace].[Class]), "Start")]

namespace [Namespace]
{
    /// 
    /// Sitecore item:added and item:renamed event handler that ensures
    /// item names are created with dashes instead of spaces and that
    /// the original name (with spaces) is stored in Display Name.
    /// 
    public class [Class]
    {
        /// 
        /// Start method that registers the Sitecore event handlers.
        /// 
        public static void Start()
        {
            var handler = new [Class]();
            Event.Subscribe("item:added", new EventHandler(handler.OnEvent));
            Event.Subscribe("item:renamed", new EventHandler(handler.OnEvent));
        }

        /// 
        /// Called when a Sitecore item is saved or renamed.
        /// 
        public void OnEvent(object sender, EventArgs args)
        {
            // Pull the item from the args.
            var item = (Item)Event.ExtractParameter(args, 0);

            // Do nothing if the item isn't in the master database.
            if (item.Database.Name != "master") return;

            // We're only concerned with items under /sitecore/content
            // (so not Layouts or Templates or whatever).
            if (!item.Paths.Path.StartsWith("/sitecore/content")) return;

            // If spaces are found in the name, replace them.
            string newName;
            if (item.Name == (newName = Regex.Replace(item.Name.ToLower().Replace(' ', '-'), "-{2,}", "-")))
            {
                // If no spaces were found, do nothing.
                return;
            }

            // Edit the item that was just saved.
            item.Editing.BeginEdit();
            try
            {
                // Set the display name to the original name (with spaces).
                item.Appearance.DisplayName = item.Name;

                // Set the item name to the new name (with dashes).
                item.Name = newName;
            }
            finally
            {
                item.Editing.EndEdit();
            }
        }
    }
}

I can’t take full credit for this solution as one of my Hanson Dodge Creative co-workers originally proposed the idea (and I think similar code was already in use on another client’s site), but this particular implementation is mostly mine. In the future, I would like to improve this solution in some ways - perhaps implement it as a rules engine action to plug into something like John West mentions in his post here: http://goo.gl/7UAm5W. If you have any suggestions, please pass them along! I can be reached on Twitter @williamsk000 or you can leave a comment here.

Filed under:

PreApplicationStartMethod is a neat .NET 4.0 trick that I only recently discovered. It allows you to have code in your assembly run during application startup. As Sitecore / ASP.NET developers, we can think of this like adding code to Application_Start in global.asax, only it’s in your assembly (no global.asax modifications needed). One thing this is commonly used for is to perform dependency injection, for example.

On the same day that I discovered PreApplicationStartMethod, I also found out that Sitecore events can be subscribed to at runtime rather than having to configure them via a .config file. When you combine these two techniques, you get a way to ensure that event handlers in your assemblies get wired up without requiring anything more than dropping your DLL in the bin folder.

Here’s a more concrete example… Let’s say I wanted to distribute a DLL that performs some action whenever an item is saved - maybe update a cache or write to an audit log or something. Your class might look something like this:

namespace [Namespace]
{
  public class [Class]
  {
    public void OnItemSaved(object sender, EventArgs args)
    {
      // Extract the item.
      var item = Sitecore.Events.Event.ExtractParameter(args, 0) as Item;

      // Do something with the item...
      // cache.Update(item);
      // Log.Audit(“Item updated: “ + item.ID);
    }
  }
}

Then, you ordinarily would throw something like this into a .config file in App_Config\Include:

<events>
  <event name=”item:saved”>
    <handler type=”[Namespace].[Class], [Assembly]” method=”OnItemSaved” />
  </event>
</events>

But with the addition of a PreApplicationStartMethod, you can avoid the .config file altogether. In order to activate your event handler, you’d just throw it in the bin folder of your Sitecore site. How is this done? First, throw an application level attribute into your class’ source file:

[assembly: PreApplicationStartMethod(typeof([Namespace].[Class]), "OnStart")]

Then, add the “OnStart” method to your class:

    public static void OnStart()
    {
      var handler = new [Class]();
      Sitecore.Events.Event.Subscribe(“item:saved”, new EventHandler(handler.OnItemSaved);
    }

And that’s it! Now if you compile and drop your DLL into the bin folder, your event handler will run each time an item is saved, and no further configuration is needed!

The PreApplicationStartMethod was introduced in .NET 4.0 and apparently had some limitations (only one of them could exist per assembly was the big one), but as far as I know those limitations have been entirely removed with .NET 4.5. If you’re using Sitecore 7+, you’re already on .NET 4.5, so… Run with it! If you have some other creative uses for either that, or runtime subscription to Sitecore events - let me know about it.