Don’t speak I know just what you’re saying – Sitecore SPEAK – Customize General Link with NoFollow Checkbox

I recently had to customize a General Link SPEAK dialog to add a NoFollow checkbox so that we can add a rel=”nofollow” on certain links on the site. At first, I was scared of SPEAK and I did not SPEAK yet! 😉

I will say that SPEAK could be easier thank it is and it would help if it didn’t change for every release. Saying that, my hope is that SPEAK will either evolve or Sitecore will end up changing it entirely. I implemented it in Sitecore 8.2 Update 2 rev.161221.

I started searching on this topic and I did not get many hits on customizing an existing field like General Link. The ones I found useful are listed at the bottom of this post. It was made difficult by the changes to Sitecore in each release. Finally I was able to get a working solution. So here goes.

First and foremost, install Sitecore Rocks Visual Studio Plugin, it will make your life easy.

Connect Sitecore Rocks to your local Sitecore Site. Make sure you test the connection so that Sitecore Rocks can update the server components for its interaction.

Once connected, traverse through the tree to /sitecore/client/Applications/Dialogs/InsertLinkViaTreeDialog.

Open the Design Layout for that Dialog.

Once open you can see the different renderings which make up the Dialog.

WORD OF CAUTION: I hosed my install when I added SPEAK 2 renderings in this dialog. The Dialog displayed and acted differently. So the best way to make sure it will work is to duplicate the renderings currently in the Dialog and set the properties.

Open the properties of the PageCode rendering and note the location for the PageCodeScriptFileName attribute and the value for the PageCodeTypeName. Later on we will be modifying these to get our functionality working.

Next, lets duplicate one of the existing items like Anchor. Each of the fields usually comes with three renderings, one for Row, one for label and field. Lets call them, NoFollowRow, NoFollowTextLbl and NoFollowText.

Since we want a checkbox, we will add a new SPEAK version 1 checkbox by clicking on the Add rendering button up top. Lets call it NoFollowChk.


On NoFollowRow properties lets make sure the PlaceholderKey is set to NameContainer.Content and for the other three renderings, lets set the PlaceholderKey to NoFollowRow.Content.

Now please don’t be that fruit who complains about too much detail. I figured that there are a few steps which seem simple enough but difficult to find/perform if you are not a SPEAK expert.

Here are the property values for each of the renderings.

For Text’s you can either specify on the rendering or provide a datasource to where the text is stored.

Leave the text field for Checkbox empty as we want to use the pretty label in NoFollowTextLbl.

We are going to set the text box to be invisible. We will use it for storage of the checked value.

Now that we have everything setup on the dialog, save the InsertLinkViaTreeDialog.layout item.

Ok now for the dirty part. We need to replace Sitecore.Speak.Applications.InsertLinkDialogTree, Sitecore.Speak.Applications with our custom implementation. Unfortunately we have to go down this route ad the code in Sitecore.Speak.Applications.InsertLinkDialogTree, Sitecore.Speak.Applications cannot be extended or overwritten. Lets get to it.

Use your favorite decompiler to get the output for Sitecore.Speak.Applications.InsertLinkDialogTree, Sitecore.Speak.Applications and create a new class InsertLinkDialogTreeCustom. Here is the complete class from ReSharper (Modified lines are highlighted):

using Sitecore;
using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell;
using Sitecore.Speak.Applications;
using Sitecore.Web;
using Sitecore.Web.PageCodes;
using System;
using System.Net;
using System.Xml.Linq;

namespace CustomSpeak
{
    public class InsertLinkDialogTreeCustom : PageCodeBase
    {
        private string targetActiveBrowserItemId = "{C5FA4571-37B3-472C-BDA1-0FADC2D2EFA7}";
        private string targetNewBrowserItemId = "{02A6C72E-17BB-48C5-8D35-AF9C494ED6BA}";
        private string targetCustomItemId = "{07CF2A84-9C22-4E85-8F3F-C301AADF5218}";

        public Sitecore.Mvc.Presentation.Rendering TreeView { get; set; }

        public Sitecore.Mvc.Presentation.Rendering ListViewToggleButton { get; set; }

        public Sitecore.Mvc.Presentation.Rendering TreeViewToggleButton { get; set; }

        public Sitecore.Mvc.Presentation.Rendering InsertEmailButton { get; set; }

        public Sitecore.Mvc.Presentation.Rendering InsertAnchorButton { get; set; }

        public Sitecore.Mvc.Presentation.Rendering TextDescription { get; set; }

        public Sitecore.Mvc.Presentation.Rendering AnchorText { get; set; }

        public Sitecore.Mvc.Presentation.Rendering Target { get; set; }

        public Sitecore.Mvc.Presentation.Rendering AltText { get; set; }

        public Sitecore.Mvc.Presentation.Rendering QueryString { get; set; }

        public Sitecore.Mvc.Presentation.Rendering StyleClass { get; set; }

        public Sitecore.Mvc.Presentation.Rendering CustomUrl { get; set; }

        public Sitecore.Mvc.Presentation.Rendering TargetLoadedValue { get; set; }

        public Sitecore.Mvc.Presentation.Rendering NoFollowChk { get; set; }

        public Sitecore.Mvc.Presentation.Rendering NoFollowText { get; set; }

        public string TargetActiveBrowserItemId
        {
            get
            {
                return this.targetActiveBrowserItemId;
            }
            set
            {
                Assert.ArgumentNotNull((object)value, "value");
                this.targetActiveBrowserItemId = value;
            }
        }

        public string TargetNewBrowserItemId
        {
            get
            {
                return this.targetNewBrowserItemId;
            }
            set
            {
                Assert.ArgumentNotNull((object)value, "value");
                this.targetNewBrowserItemId = value;
            }
        }

        public string TargetCustomItemId
        {
            get
            {
                return this.targetCustomItemId;
            }
            set
            {
                Assert.ArgumentNotNull((object)value, "value");
                this.targetCustomItemId = value;
            }
        }

        public override void Initialize()
        {
            string setting = Settings.GetSetting("BucketConfiguration.ItemBucketsEnabled");
            this.ListViewToggleButton.Parameters["IsVisible"] = setting;
            this.TreeViewToggleButton.Parameters["IsVisible"] = setting;
            this.TreeView.Parameters["ShowHiddenItems"] = UserOptions.View.ShowHiddenItems.ToString();
            this.TreeView.Parameters["ContentLanguage"] = WebUtil.GetQueryString("la");
            this.ReadQueryParamsAndUpdatePlaceholders();
        }

        private static string GetXmlAttributeValue(XElement element, string attrName)
        {
            if (element.Attribute((XName)attrName) == null)
                return string.Empty;
            return element.Attribute((XName)attrName).Value;
        }

        private void ReadQueryParamsAndUpdatePlaceholders()
        {
            string queryString1 = WebUtil.GetQueryString("ro");
            string queryString2 = WebUtil.GetQueryString("hdl");
            if (!string.IsNullOrEmpty(queryString1) && queryString1 != "{0}")
                this.TreeView.Parameters["RootItem"] = queryString1;
            this.InsertAnchorButton.Parameters["Click"] = string.Format(this.InsertAnchorButton.Parameters["Click"], (object)WebUtility.UrlEncode(queryString1), (object)WebUtility.UrlEncode(queryString2));
            this.InsertEmailButton.Parameters["Click"] = string.Format(this.InsertEmailButton.Parameters["Click"], (object)WebUtility.UrlEncode(queryString1), (object)WebUtility.UrlEncode(queryString2));
            this.ListViewToggleButton.Parameters["Click"] = string.Format(this.ListViewToggleButton.Parameters["Click"], (object)WebUtility.UrlEncode(queryString1), (object)WebUtility.UrlEncode(queryString2));
            int num = queryString2 != string.Empty ? 1 : 0;
            string empty = string.Empty;
            if (num != 0)
                empty = UrlHandle.Get()["va"];
            if (!(empty != string.Empty))
                return;
            XElement xelement = XElement.Parse(empty);
            if (!(InsertLinkDialogTreeCustom.GetXmlAttributeValue(xelement, "linktype") == "internal"))
                return;
            string xmlAttributeValue = InsertLinkDialogTreeCustom.GetXmlAttributeValue(xelement, "id");
            if (string.IsNullOrWhiteSpace(xmlAttributeValue))
                return;
            Item obj = (string.IsNullOrEmpty(queryString1) ? (Item)null : ClientHost.Databases.ContentDatabase.GetItem(queryString1)) ?? ClientHost.Databases.ContentDatabase.GetRootItem();
            Item itemFromQueryString = SelectMediaDialog.GetMediaItemFromQueryString(xmlAttributeValue);
            if (obj != null && itemFromQueryString != null && itemFromQueryString.Paths.LongID.StartsWith(obj.Paths.LongID))
                this.TreeView.Parameters["PreLoadPath"] = obj.ID.ToString() + itemFromQueryString.Paths.LongID.Substring(obj.Paths.LongID.Length);
            this.TextDescription.Parameters["Text"] = InsertLinkDialogTreeCustom.GetXmlAttributeValue(xelement, "text");
            this.AltText.Parameters["Text"] = InsertLinkDialogTreeCustom.GetXmlAttributeValue(xelement, "title");
            this.StyleClass.Parameters["Text"] = InsertLinkDialogTreeCustom.GetXmlAttributeValue(xelement, "class");
            this.QueryString.Parameters["Text"] = InsertLinkDialogTreeCustom.GetXmlAttributeValue(xelement, "querystring");
            this.AnchorText.Parameters["Text"] = InsertLinkDialogTreeCustom.GetXmlAttributeValue(xelement, "anchor");
            this.NoFollowChk.Parameters["Text"] = InsertLinkDialogTreeCustom.GetXmlAttributeValue(xelement, "nofollowchk");

            this.NoFollowText.Parameters["Text"] = InsertLinkDialogTreeCustom.GetXmlAttributeValue(xelement, "nofollowtext");

            this.SetupTargetDropbox(xelement);
        }

        private void SetupTargetDropbox(XElement fieldContent)
        {
            string xmlAttributeValue = InsertLinkDialogTreeCustom.GetXmlAttributeValue(fieldContent, "target");
            string str;
            if (xmlAttributeValue.Equals("_blank", StringComparison.OrdinalIgnoreCase))
                str = this.TargetNewBrowserItemId;
            else if (string.IsNullOrWhiteSpace(xmlAttributeValue))
            {
                str = this.TargetActiveBrowserItemId;
            }
            else
            {
                str = this.TargetCustomItemId;
                this.CustomUrl.Parameters["Text"] = xmlAttributeValue;
            }
            this.TargetLoadedValue.Parameters["Text"] = str;
        }
    }
}

Add the two properties we need.

public Sitecore.Mvc.Presentation.Rendering NoFollowChk { get; set; }
public Sitecore.Mvc.Presentation.Rendering NoFollowText { get; set; }

In the ReadQueryParamsAndUpdatePlaceholders function add the following to send back information stored to the SPEAK component.

this.NoFollowChk.Parameters["Text"] = InsertLinkDialogTreeCustom.GetXmlAttributeValue(xelement, "nofollowchk");
this.NoFollowText.Parameters["Text"] = InsertLinkDialogTreeCustom.GetXmlAttributeValue(xelement, "nofollowtext");

The next part is to modify how the fields interact. Open Website\sitecore\shell\client\Applications\Dialogs\InsertLinkViaTreeDialog.js file.

Modify the initialized function to check for the text value in the text field and set the checkbox based on that. Here is the entire file with modifications.

define(["sitecore"], function(Sitecore) {
  var InsertLinkViaTreeDialog = Sitecore.Definitions.App.extend({
    initialized: function () {

      this.updateCustomUrl();

      if (this.NoFollowText.get("text") == "true")
      {
	      this.NoFollowChk.set("isChecked", true);
      }
	  
      this.Target.on("change", function () {
        this.updateCustomUrl();
      }, this);

      // work around an issue in combobox
      this.TargetsDataSource.on("change:items", function () {
        if (this.__firstTime) {
          this.__firstTime = false;
          this.Target.set("selectedValue", this.TargetLoadedValue.get("text"));
        }
      }, this);
    },
      
    updateCustomUrl: function () {

      var emptyOptionID = "{A3C9DB39-1D1B-4AA1-8C68-7B9674D055EE}";
      var customUrlOptionID = "{07CF2A84-9C22-4E85-8F3F-C301AADF5218}";

      var targetWindowValue = this.Target.get("selectedItem");

      if (!targetWindowValue || targetWindowValue.itemId === emptyOptionID) {
        this.CustomUrl.set("isEnabled", false);
        return;
      }

      if (targetWindowValue.itemId === customUrlOptionID) {
        this.CustomUrl.set("isEnabled", true);
      } else {
        this.CustomUrl.set("isEnabled", false);
      }
    },

    insertInternalLinkResult: function () {
      var targetDisplayTextID = this.TextDescription,
      targetPathID = this.TreeView,
      targetPathProperty = "$path",
      targetQueryID = this.QueryString,
      targetAltTextID = this.AltText,
      targetStyleID = this.StyleClass,
      targetWindowID = this.Target,
      customUrlID = this.CustomUrl,
      targetControlID = this.TreeView,
      anchor = this.AnchorText,
      nofollowtext = this.NoFollowText,

      nofollowchk = this.NoFollowChk,
      selectedItemsPropertyName = "selectedNode",
      template = ' querystring="<%=queryString%>" id="<%=itemId%>" nofollowtext="<%=nofollowtext%>" nofollowchk="<%=nofollowchk%>" />',
      targetWindowValue,
      path,
      emptyOptionID = "{A3C9DB39-1D1B-4AA1-8C68-7B9674D055EE}",
      htmlEncode = function (str) {
        return str.toString().replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
      };

      if (!targetDisplayTextID) {
        console.log("Provide at least display text for your link");
        return false;
      }

      targetWindowValue = targetWindowID.get("selectedItem");

      if (!targetWindowValue || targetWindowValue.itemId === emptyOptionID) {
        targetWindowValue = "";
      } else {
        var targetWindow = targetWindowValue.$displayName.trim();

        switch (targetWindow) {
          case 'Active Browser':
            targetWindow = "";
            break;
          case 'New Browser':
            targetWindow = "_blank";
            break;
          case "Custom":
            targetWindow = customUrlID.get("text");
            break;
          default:
            targetWindow = "";
        }

        targetWindowValue = "target=\"" + targetWindow + "\"";
      }

      if (targetPathID.get(selectedItemsPropertyName) &&
        "rawItem" in targetPathID.get(selectedItemsPropertyName) &&
        targetPathID.get(selectedItemsPropertyName).rawItem[targetPathProperty]) {
        path = targetPathID.get(selectedItemsPropertyName).rawItem[targetPathProperty];
      }

      var itemLink = _.template(template, {
        displayText: htmlEncode(targetDisplayTextID.get("text")),
        alternateText: htmlEncode(targetAltTextID.get("text")),
        itemId: targetControlID.get("selectedItemId"),
        queryString: htmlEncode(targetQueryID.get("text")),
        target: targetWindowValue,
        styleClass: htmlEncode(targetStyleID.get("text")),
        path: path,
        
        nofollowtext: nofollowchk.get("isChecked"),

        nofollowchk: htmlEncode(nofollowchk.get("text")),
        anchor: htmlEncode(anchor.get("text"))
      });

      return this.closeDialog(itemLink);
    },
        
    __firstTime: true

  });

  return InsertLinkViaTreeDialog;
});

In the insertInternalLinkResult function, add in the mapping from the Class to the variables UI will write as raw value of the General Link field. Also modify the template variable to add in the markup used to generate the output to be stored in the General Link field. Last but not the least is to store the correct Checkbox value in the text field. Boom, this concludes the customization. Now to check out the output.

if (this.NoFollowText.get("text") == "true")
{
      this.NoFollowChk.set("isChecked", true);
}
nofollowtext = this.NoFollowText,
nofollowchk = this.NoFollowChk,
selectedItemsPropertyName = "selectedNode",
template = ' querystring="<%=queryString%>" id="<%=itemId%>" nofollowtext="<%=nofollowtext%>" nofollowchk="<%=nofollowchk%>" />',
nofollowtext: nofollowchk.get("isChecked"),
nofollowchk: htmlEncode(nofollowchk.get("text")),

This only solves half the problem which is how do we get Sitecore backend to show and process the Checkbox for NoFollow. In the next blog post we will look at how to render it using GlassMapper. Here is a video of the functionality.


via GIPHY

 

References:
General Link Dialog InsertLinkViaTreeDialog misses InsertLinkRules after Upgrade to 8.2u2
Extending internal General Link dialog with Checkbox in SPEAK
Extending Sitecore 8.1 – Adding support for anchors in internal links

If you have any questions or concerns, please get in touch with me. (@akshaysura13 on twitter or on Slack).

2 thoughts on “Don’t speak I know just what you’re saying – Sitecore SPEAK – Customize General Link with NoFollow Checkbox”

  1. I cannot find the 2nd part: “This only solves half the problem which is how do we get Sitecore backend to show and process the Checkbox for NoFollow. In the next blog post we will look at how to render it using GlassMapper. ” A link would be so great!

  2. Hi Akshay,
    Can you post the second part to this post. I want to also know how to render the custom general link using glass mapper

Leave a Reply

Your email address will not be published. Required fields are marked *