Wednesday, June 29, 2016

Sitecore handling content URLs that change between environments

In some Sitecore implementations there can be various integration points to third party (or other internal web sites) which will have URLs (in content) which change based on environment (development, test or production).

An example of this would be the main content web site (in Sitecore) which has a number of links to an online application web site. In production the URL might be app.mydomain.com, and in test app-test.mydomain.com. The problem here is that as content is synced from production to test, the production links will be present in the test environment. This is not ideal as it can lead to test data in production which testers/content users may not notice - it also doesn't allow for fair end to end testing of the integrated systems.

The way I solved this issue was to create test content management as a publishing target on the production content management server. This means that authorised users are able to publish/refresh content on test via production. This of course could be replaced with other syncing methods such as Sitecore packages, TDS or Razl.

I then created a command (which was added to the ribbon) which would recursively loop through content in the tree and do a replacement on links (based on configurable from and to values).

namespace MyProject.Commands
{
    public class LinkChanger : Command
    {
        private static readonly string GeneralLinkExternal = @"linktype=""external""";
        private static readonly ID ConfigItemId = ID.Parse("{6B21822A-3383-4CC8-A32F-2BFEEE28EB79}"); // ID of content item
        private static readonly ID LinkChangeTemplate = ID.Parse("{5E4F0DF4-6FA2-4C36-A965-1895FE527B8D}"); // ID of template
        private Config config = new Config();

        public override void Execute(CommandContext context)
        {
            GetConfig();

            if (config == new Config() || !config.Enabled || string.IsNullOrEmpty(config.ContentPath))
            {
                Sitecore.Context.ClientPage.ClientResponse.Alert("Error changing links!");
                return; // Unable to get config or not enabled or no content path
            }

            var contentItem = Factory.GetDatabase("master").GetItem(config.ContentPath);

            if (contentItem != null)
            {
                GetItem(contentItem);
            }
            else
            {
                Sitecore.Context.ClientPage.ClientResponse.Alert("Error changing links!");
            }

            Sitecore.Context.ClientPage.ClientResponse.Alert("Link change process complete!");
        }

        private Item GetItem(Item mainItem)
        {
            ProcessChanges(mainItem);

            if (mainItem.HasChildren)
            {
                foreach (var myInnerItem in mainItem.Children.ToList())
                {
                    GetItem(myInnerItem);
                }
            }

            return null;
        }

        private void ProcessChanges(Item itemBeingSaved)
        {
            var richTextFields = itemBeingSaved.Fields.Where(x => !x.Name.StartsWith("_") && x.Type == "Rich Text");
            var linkFields = itemBeingSaved.Fields.Where(x => !x.Name.StartsWith("_") && x.Type == "General Link");

            try
            {
                // Rich Text
                foreach (var field in richTextFields ?? new List<Field> { null })
                {
                    foreach (var replacement in config.Replacements)
                    {
                        if (field.Value.IndexOf(replacement.From, 0, StringComparison.CurrentCultureIgnoreCase) > -1)
                        {
                            using (new Sitecore.Data.Items.EditContext(itemBeingSaved))
                            {
                                field.Value = CaseReplace(field.Value, replacement.From, replacement.To, StringComparison.CurrentCultureIgnoreCase);
                            }
                        }
                    }
                }

                // General Link
                foreach (var field in linkFields ?? new List<Field> { null })
                {
                    foreach (var replacement in config.Replacements)
                    {
                        if (field.Value.Contains(GeneralLinkExternal) && field.Value.IndexOf(replacement.From, 0, StringComparison.CurrentCultureIgnoreCase) > -1)
                        {
                            using (new Sitecore.Data.Items.EditContext(itemBeingSaved))
                            {
                                field.Value = CaseReplace(field.Value, replacement.From, replacement.To, StringComparison.CurrentCultureIgnoreCase);
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Log.Error(string.Format("Error in LinkChanger: {0}", ex.ToString()), "");
            }
        }

        private void GetConfig()
        {
            // Get and add to cache
            var configItem = Factory.GetDatabase("master").GetItem(ConfigItemId);

            if (configItem == null)
            {
                Log.Error("Error in Link Changer: Unable to get config item.", "");
                return;
            }

            config = new Config
            {
                Replacements = new List<LinkReplacement>(),
                Enabled = configItem.Fields["Enabled"].Value == "1" ? true : false,
                ContentPath = configItem.Fields["Content Path"].Value
            };

            // Get link changes
            var changes = configItem.Children.Where(x => x.TemplateID == LinkChangeTemplate);

            foreach(var child in changes ?? new List<Item> { null })
            {
                config.Replacements.Add(new LinkReplacement
                {
                    From = child.Fields["From"].Value,
                    To = child.Fields["To"].Value
                });
            }
        }

        private static string CaseReplace(string source, string oldValue, string newValue, StringComparison comparisonType)
        {
            if (source.Length == 0 || oldValue.Length == 0)
                return source;

            var result = new System.Text.StringBuilder();
            int startingPos = 0;
            int nextMatch;
            while ((nextMatch = source.IndexOf(oldValue, startingPos, comparisonType)) > -1)
            {
                result.Append(source, startingPos, nextMatch - startingPos);
                result.Append(newValue);
                startingPos = nextMatch + oldValue.Length;
            }
            result.Append(source, startingPos, source.Length - startingPos);

            return result.ToString();
        }

        private struct LinkReplacement
        {
            public string From { get; set; }
            public string To { get; set; }
        }

        private class Config
        {
            public List<LinkReplacement> Replacements { get; set; }
            public bool Enabled { get; set; }
            public string ContentPath { get; set; }
        }
    }
}

You will also need to include the following config patch file (updating namespace and assembly of course):
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="tools:changelinks" type="MyProject.Commands.LinkChanger,MyProject" />
    </commands>
  </sitecore>
</configuration>

Some general notes are:
  • This example will parse over all (non system) rich text and general link fields (external links)
  • It will handle both the href="" and text content of a given link
  • I don't include http:// or https:// in the replacements in order to handle both
  • The function for replacements will handle matches ignoring the case
  • Be careful when using this and consider logging replacements whilst testing. With full domain names, it should not replace non-URL content.
  • The command needs to be hooked up to the ribbon in the core database.
The two configuration templates are - the IDs will need to be changed in code to match your IDs:


Which then are configured as follows:


No comments:

Post a Comment