Sitecore

Manipulating Sitecore Urls with Aliases by Content Editors

By September 3, 2015 No Comments

If you are reading this post, most probably you are familiar with aliases, and how they function.
If not, a good starting read regarding aliases can be found on Sitecore SDN site.

But what if we want or need to enhance the aliases functionality a little bit, and separate the alias part from the content editing one?

To make this possible, we can put a new field on our site base template, let’s call it “URL Value”, and create the corresponding alias item on the fly, if the field is filled in by a content editor.

Supposing we have a structure like this:

tree_structure

We will be able to call our “Smart Devices Application” product page by simply calling “http://{host-name}/smart-app“.

To do this, we would need to register a simple item:saved event in web.config:

<event name="item:saved">  
  ...
  <handler type="AliasFunctionality.AliasSaved, AliasFunctionality" method="OnItemSaved"/>
</event>  

And here’s the code for it:

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;  
using Sitecore.Events;  
using Sitecore.Data.Items;  
using Sitecore.Data.Fields;  
using Sitecore.SecurityModel;  
using Sitecore.Diagnostics;  
using Sitecore.Data;

namespace AliasFunctionality  
{
    public class AliasSaved
    {
        #region properties
        public const string ALIASES_ROOT_FOLDER = "/sitecore/system/Aliases/";
        public const string ALIAS_LINK_TYPE = "internal";
        public const string ALIAS_TEMPLATE_ID = "{54BCFFB7-8F46-4948-AE74-DA5B6B5AFA86}";    
        #endregion

        public void OnItemSaved(object sender, EventArgs e)
        {
            if (e == null)
                return;

            Item item = Event.ExtractParameter(e, 0) as Item;
            if (item != null && item.Database == Sitecore.Context.ContentDatabase) // proceed only for master db
            {

                string aliasValue = item["URL Value"];
                if (!string.IsNullOrEmpty(aliasValue))
                {
                    try
                    {
                        var aliasesSiteFolder = GetAliasRootItem(item);
                        if (aliasesSiteFolder != null && !AliasAlreadyExists(aliasesSiteFolder, aliasValue))
                        {
                            // create new alias, then populate link for it
                            Item newAliasItem = CreateItem(aliasesSiteFolder, item, aliasValue, ALIAS_TEMPLATE_ID);
                            PopulateLink(item, newAliasItem);
                        }
                    }
                    catch(Exception ex)
                    {
                        //log error if needed
                        Log.Error("Exception happened on alias Save: " + ex.ToString(), this);
                    }
                }
            }
        }

        private Item CreateItem(Item rootFolder, Item item, string itemName, string templateId)
        {
            using (new SecurityDisabler())
            {
                var aliasTemplateId = ID.Parse(templateId);
                Item result = rootFolder.Add(itemName, item.Database.GetTemplate(aliasTemplateId));
                Log.Audit("Created new item: " + result.Paths.FullPath + ", language: " + result.Language.Name, rootFolder);
                return result;
            }
        }

        private void PopulateLink(Item item, Item newAliasItem)
        {
            if (newAliasItem != null)
            {
                using (new SecurityDisabler())
                {
                    newAliasItem.Editing.BeginEdit();
                    try
                    {
                        LinkField linkedItem = newAliasItem.Fields["Linked item"];
                        //perform the editing
                        linkedItem.LinkType = ALIAS_LINK_TYPE;
                        linkedItem.Url = item.Paths.ContentPath;
                        linkedItem.QueryString = string.Empty;
                        linkedItem.Target = string.Empty;
                        linkedItem.TargetID = item.ID;
                    }
                    finally
                    {
                        //Close the editing state
                        newAliasItem.Editing.EndEdit();
                    }
                }
            }
        }

        private bool AliasAlreadyExists(Item root, string displayName)
        {
            if (root != null && root.HasChildren)
            {
                var alias = root.Children.FirstOrDefault(c => c.DisplayName == displayName);
                if (alias == null)
                {
                    return false;
                }
            }
            return true;
        }

        private Item GetAliasRootItem(Item item)
        {
            return item.Database.GetItem("/sitecore/system/aliases/");
        }
    }
}

I don’t think there’s anything very complicated that needs to be explained regarding the above code. The main method is the one specified inside web.config, we check if the “URL Value” field is not empty and create an alias item if one with that name doesn’t exist already.

We can also go one step further and on publishing the initial item, check for the corresponding alias and publish that one too, if exists.

To do this, we need a new handler in web.config:

<event name="publish:itemProcessed" help="Receives an argument of type ItemProcessedEventArgs (namespace: Sitecore.Publishing.Pipelines.PublishItem)">  
        <handler type="AliasFunctionality.AliasPublish, AliasFunctionality" method="OnPublishItemProcessed">
</event>  

And the code for the publish handler:

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;  
using Sitecore.Configuration;  
using Sitecore.Data;  
using Sitecore.Data.Items;  
using Sitecore.Diagnostics;  
using Sitecore.Publishing.Pipelines.PublishItem;

namespace AliasFunctionality  
{
    public class AliasPublish
    {
        #region properties
        public const string MASTER_DB = "master";
        public const string WEB_DB = "web";
        #endregion

        public void OnPublishItemProcessed(object sender, EventArgs e)
        {
            ItemProcessedEventArgs args = e as ItemProcessedEventArgs;

            if (args != null)
            {
                Item item = args.Context.PublishHelper.GetTargetItem(args.Context.ItemId);
                if (item != null)
                {
                    string aliasName = item["URL Value"];
                    if (!string.IsNullOrEmpty(aliasName))
                    {
                        Item aliasItem = GetContentDbAlias(aliasName);
                        if (aliasItem != null)
                        {
                            PublishItem(aliasItem);
                        }
                    }
                }
            }
        }

        private void PublishItem(Item item)
        {
            // Create publishOptions (specify source and target database,
            // the publish mode and language, and the publish date)
            Sitecore.Publishing.PublishOptions publishOptions =
              new Sitecore.Publishing.PublishOptions(item.Database,
                                                     Database.GetDatabase(WEB_DB),
                                                     Sitecore.Publishing.PublishMode.SingleItem,
                                                     item.Language,
                                                     System.DateTime.Now);  // Create a publisher with the publishoptions
            Sitecore.Publishing.Publisher publisher = new Sitecore.Publishing.Publisher(publishOptions);

            // Choose where to publish from
            publisher.Options.RootItem = item;

            // Don't publish children
            publisher.Options.Deep = false;

            // Do the publish!
            publisher.Publish();
            Log.Audit("Publish item: " + item.Paths.FullPath + ", language: " + item.Language.Name, this);
        }

        private Item GetContentDbAlias(string aliasName)
        {
            return Factory.GetDatabase(MASTER_DB).GetItem(AliasSaved.ALIASES_ROOT_FOLDER + aliasName);
        }
    }
}

Inside the main method, which is OnPublishItemProcessed, we check if the item that was published had an alias, and if so,
we try to find it from Master database, and we publish it.

That would be it, we can now rely only on the content editors to add aliases only by changing “URL Value” field, and by publishing the original item, the alias also gets live!

Of course, there are some things worth mentioning here: the alias logic can be further enhanced to support multi-site solution. A very good article can be found here. Also, with the current implementation, there’s not an easy way to get rid of an old alias, if something needs to be changed, this will need to be done manually.

As Sitecore doesn’t encourage very much the usage of aliases, I think there are cases when the client demands to be able to have a custom URL independent of the tree-structure, and this is a pretty way to do it.

Adrian Iorgu

Author Adrian Iorgu

Results-oriented and self-motivated lead engineer with a focus on delivering high-quality code and products in high traffic environments. Sitecore MVP 2016 & 2017

More posts by Adrian Iorgu

Leave a Reply