Error resolving SpeakUI scripts in Sitecore

While working with Speak UI in Sitecore, I added a new Speak component rendering to my solution.

2017-03-31_152916

Once I added this rendering to my speak page, it started throwing a javascript error:

Script error for: /-/speak/v1/client/

2017-03-31_153237

After some poking around, I realized that this issue was occurring because of the space in my rendering name!

So while this url was giving a 404: http://redirectmanager.local/-/speak/v1/client/Redirects_Listing.js, the url with the space was working just fine:

2017-03-31_153530.png

So short of customizing the ResolveScript processor (see below screenshot) which is responsible for resolving the rendering script from your folder to the expected url with the set prefix (see below RequireJsCustomHandler setting), the work around would be to avoid spaces in your rendering names for this version and use underscores if you have to!

<!--   SPEAK HTML REQUIRE JS CUSTOM HANDLER             Specifies the prefix that should trigger the HTTP request customer handler (see the customHandlers section in the web.config file).             Default: /-/speak/v1/        -->
<setting name="Speak.Html.RequireJsCustomHandler" value="/-/speak/v1/" patch:source="Sitecore.Speak.config"/>

2017-03-31_153809.png

Hope this helps!

Advertisements

Sitecore Image Lazy Load Module

The package and code for this module can be found at: Github – aceanindita

The bLazy.js plugin – A lazyload image script can be used to enable lazyload on your site, to reduce initial page load times drastically, especially if your site is image heavy.

While you can directly integrate your site with the bLazy plugin, you will need to ensure that your images are not loaded on page load, for which you will need to transform your html accordingly. This can also bring with it experience editor compatibility concerns.

This module will make all your images rendered from Sitecore Image and Rich Text fields, lazy loaded automatically without you having to make ANY html updates at all.

For this, we are usign the following config which does 2 things

  • Processor added to mvc.renderRendering – to include and initialize the bLazy plugin
  • Processor added to renderField – to transform html for all images rendered from the Sitecore Image field and Rich Text field, to move the src url to a different attribute recognized by the plugin, and also add a selector to tell the plugin this html needs to be transformed to lazy load the image.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.renderRendering>
        <processor type="ImageLazyLoadModule.Pipelines.MVC.InsertbLazyInit, ImageLazyLoadModule"             patch:before="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc']" />
      </mvc.renderRendering>
      <renderField>
        <processor type="ImageLazyLoadModule.Pipelines.RenderField.ImageLazyLoad, ImageLazyLoadModule" />
      </renderField>
    </pipelines>
    <settings>
      <setting name="ImageLazyLoadModule.Offset" value="200" />
      <setting name="ImageLazyLoadModule.Selector" value="b-lazy" />
    </settings>
  </sitecore>
</configuration>

You can see 2 configurable settings above, which are used when the bLazy plugin is initialized.

  • The offset controls how early you want the elements to be loaded before they’re visible.
  • The selector is the class that will be added to the img tags dynamically, which will be used by the plugin to lazy load images.

The code for mvc.renderRendering for including the script and plugin initialization

using Sitecore.Configuration;
using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.Mvc.Presentation;

namespace ImageLazyLoadModule.Pipelines.MVC
{
    public class InsertbLazyInit : RenderRenderingProcessor
    {
        public override void Process(RenderRenderingArgs args)
        {
            Renderer renderer = args.Rendering.Renderer;
            if (renderer == null)
                return;

            bool isLayout = renderer is ViewRenderer &&
                               ((ViewRenderer)renderer).Rendering.RenderingType == "Layout";

            if (isLayout)
            {
                args.Writer.Write("<script src=\"../../sitecore modules/Lazy Load/blazy.min.js\"></script>"
                                  + "<script>"
                                  + "var bLazy = new Blazy({ selector: '."
                                  + Settings.GetSetting("ImageLazyLoadModule.Selector", "b-lazy")
                                  + "', offset: " 
                                  + Settings.GetIntSetting("ImageLazyLoadModule.Offset", 200)
                                  + ", src: 'responsive-src'});"
                                  + "</script>");
            }
        }
    }
}

The code for renderField which will transform img tag html

using HtmlAgilityPack;
using Sitecore;
using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.RenderField;
using System;

namespace ImageLazyLoadModule.Pipelines.RenderField
{
    public class ImageLazyLoad
    {
        public void Process(RenderFieldArgs args)
        {
            try
            {
                if (args == null)
                    return;

                // Trigger the code to transform the img tags only for rich text and image fields
                if (!(args.FieldTypeKey != "rich text" || args.FieldTypeKey != "image") || string.IsNullOrEmpty(args.FieldValue) ||
                     !Context.PageMode.IsNormal)
                    return;

                if (!string.IsNullOrWhiteSpace(args.Result?.FirstPart))
                {
                    HtmlDocument doc = new HtmlDocument { OptionWriteEmptyNodes = true };
                    doc.LoadHtml(args.Result.FirstPart);

                    if (doc.DocumentNode != null)
                    {
                        // Search for all img tags
                        HtmlNodeCollection imgTag = doc.DocumentNode.SelectNodes("//img");
                        if (imgTag == null || imgTag.Count == 0)
                            return;

                        foreach (HtmlNode node in imgTag)
                        {
                            if (node.Attributes["src"] != null && node.ParentNode != null)
                            {
                                string imgUrl = node.Attributes["src"].Value;
                                node.Attributes.Add("responsive-src", imgUrl);
                                node.Attributes["src"].Value = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
                                string currentClass = node.Attributes["class"] != null ? node.Attributes["class"].Value : "";
                                node.Attributes.Remove("class");
                                node.Attributes.Add("class", (string.IsNullOrWhiteSpace(currentClass) ? "" : currentClass + " ")
                                    + Settings.GetSetting("ImageLazyLoadModule.Selector", "b-lazy"));
                                node.Attributes.Remove("width");
                                node.Attributes.Remove("height");
                            }
                        }

                        args.Result.FirstPart = doc.DocumentNode.OuterHtml;
                    }
                }
            }
            catch (Exception ex)
            {
                Log.Error("Error in ImageLazyLoadModule.ImageLazyLoad:" + ex.Message, ex);
            }
        }
    }
}

You can also choose to add image transitions using css as is specified in the bLazy Documentation

.b-lazy {
-webkit-transition: opacity 500ms ease-in-out;
-moz-transition: opacity 500ms ease-in-out;
-o-transition: opacity 500ms ease-in-out;
transition: opacity 500ms ease-in-out;
max-width: 100%;
opacity: 0;
}
.b-lazy.b-loaded {
opacity: 1;
}

Here’s a very simple example of this module in use (Rich text field example)

We have a simple content item, where an image has been embedded in the rich text field from the media library

The view rendering code, simply outputs the rich text field using the Sitecore renderer (observe there are no additional classes added either)

Now for the output! When you load up the page with this ViewRendering, you see the following

Observe that the selector ‘b-lazy’ was added dynamically by our RenderField customization, and it was further picked up by the bLazy plugin and transformed successfully, evident by the ‘b-loaded’ class.

Now to see the html loaded on page load

Here you can see that the bLazy script and its initialization was added dynamically by our mvc.renderRendering pipeline update, and the img tag on page load, has a different src! This src is the base64 code of a pixel. The actual image source has been moved to a different attribute (responsive-src) which is recognized by the plugin (supplied during plugin initialization)

Hence proved.

The package and code for this module can be found at: Github – aceanindita

Sitecore RenderingsMarkerModule

Note: This module has been created as a package and is available for download along with the complete source code at Git: aceanindita

Sitecore being component based as it is, adding start and end comments to your views can be immensely helpful while debugging issues, especially if they are being worked on someone who doesn’t know the project from Adam.

Hardcoding the start / end comments isn’t a great idea for obvious reasons like being prone to developer omission in some views, refactoring / file renaming concerns etc.

In an earlier post I had shown how we had partially resolved this by making the build up of the comment dynamic using a html helper method which used the WebPageExecutingBase.VirtualPath property from the System.Web.WebPages assembly.
You can refer the post here: Using dynamic view markers in Sitecore MVC

In this method, you would need to manually wrap each of your views within a call to the said html helper method, which would dynamically include the current view path in start and end comments around your view. While this did away with the potential issues that might come out of refactoring like changing folder structures / renaming files, it still didn’t deal with the dependency on having the developer(s) remember to add the call to this html helper in every single view.

Here’s a solution for this, we added a new processor in the mvc.rendering pipeline, before and after the call of Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer which renders the actual rendering html.

Here’s the config update made for this (patch):

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.renderRendering>
        <processor type="RenderingsMarkerModule.Pipelines.MVC.RenderingMarker.RenderMarkerStart, RenderingsMarkerModule"
            patch:before="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc']" />
        <processor type="RenderingsMarkerModule.Pipelines.MVC.RenderingMarker.RenderMarkerEnd, RenderingsMarkerModule"
            patch:after="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc']" />
      </mvc.renderRendering>
    </pipelines>
    <settings>
      <setting name="RenderingsMarkerModule.Enabled" value="true" />
      <setting name="RenderingsMarkerModule.ShowLayoutMarkers" value="false" />
    </settings>
  </sitecore>
</configuration>

Note we have 2 additional settings here. ‘RenderingsMarkerModule.Enabled’ lets you toggle the rendering of comments on and off – this could help you turn of this rather revealing feature on staging / production. Additionally you could toggle ‘RenderingsMarkerModule.ShowLayoutMarkers’ to show / hide comments before & after your layout. It is recommended that this remain off, since having your html response start with a html comment might not always be favorable.

Here’s the code we have in place:
RenderingsMarkerModule.Pipelines.MVC.RenderingMarker.RenderMarkerStart

using Sitecore.Configuration;
using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.Mvc.Presentation;

namespace RenderingsMarkerModule.Pipelines.MVC.RenderingMarker
{
    public class RenderMarkerStart : RenderRenderingProcessor
    {
        public override void Process(RenderRenderingArgs args)
        {
            if (Settings.GetBoolSetting("RenderingsMarkerModule.Enabled", false))
            {
                Renderer renderer = args.Rendering.Renderer;
                if (renderer == null)
                    return;

                bool isLayout = renderer is ViewRenderer &&
                                ((ViewRenderer) renderer).Rendering.RenderingType == "Layout";
                bool showLayoutMarkers = Settings.GetBoolSetting("RenderingsMarkerModule.ShowLayoutMarkers", false);

                if (isLayout && !showLayoutMarkers) return;

                args.Writer.Write("\n<!-- START: " + renderer + " -->\n");
            }
        }
    }
}

RenderingsMarkerModule.Pipelines.MVC.RenderingMarker.RenderMarkerEnd

using Sitecore.Configuration;
using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.Mvc.Presentation;

namespace RenderingsMarkerModule.Pipelines.MVC.RenderingMarker
{
    public class RenderMarkerEnd : RenderRenderingProcessor
    {
        public override void Process(RenderRenderingArgs args)
        {
            if (Settings.GetBoolSetting("RenderingsMarkerModule.Enabled", false))
            {
                Renderer renderer = args.Rendering.Renderer;
                if (renderer == null)
                    return;

                bool isLayout = renderer is ViewRenderer &&
                                ((ViewRenderer) renderer).Rendering.RenderingType == "Layout";
                bool showLayoutMarkers = Settings.GetBoolSetting("RenderingsMarkerModule.ShowLayoutMarkers", false);

                if (isLayout && !showLayoutMarkers) return;

                args.Writer.Write("\n<!-- END: " + renderer + " -->\n");
            }
        }
    }
}

Here’s a sample of the output

This module has been created as a package and is available for download along with the complete source code at Git: aceanindita

Resize and lazy load images in Rich Text fields in Sitecore

Often the imagery that is uploaded in Sitecore is of way higher resolution than what is required, in an attempt to produce high quality pages.
While these are uploaded onto image fields in sitecore, we can resize the images when we output them using suitable html helpers. Refer:Render Responsive Image Glass Html Helper with Sitecore Image Processor Module.

We also needed a way to achieve this for images inserted in the rich text field. Especially seeing as we had content authors adding images sized 3000×3000 in there, and page sizes soaring up to 80mb each at times!
We implemented a solution with the bLazy plugin.
The plugin documentation will tell you, that once we include the script and initialize it with the right parameters, all it takes is adding the right attributes and class into our html, to enable lazy load of images – resized if need be for each breakpoint as we determine.

Here’s what we implemented for this:

  • We added a patch to the renderField pipeline, which would transform all img tags as we needed – add the required class which is to be used as the selector for the blazy plugin, and also the breakpoint specific resized image url attributes. In addition, we also replaced the img tag source with a base64 encoded transparent gif so it wouldn’t do any extra requests.
  • We added a reference to the bLazy plugin, and initialized it with the parameters suitable to our requirements.

The pipeline code:

using HtmlAgilityPack;
using sbdshared._Classes.SBDShared.Helpers;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.RenderField;
using System;
using System.Web;

namespace MySite.Customizations.Customized_Sitecore
{
    public class ProcessRichTextImages
    {
        /// <summary>
        /// Processes the specified arguments.
        /// </summary>
        /// <param name="args">The arguments.</param>
        public void Process(RenderFieldArgs args)
        {
            try
            {
                if (args != null && (args.FieldTypeKey != "rich text" || string.IsNullOrEmpty(args.FieldValue) || !Context.PageMode.IsNormal))
                    return;

                if (args != null && args.Result != null && !string.IsNullOrWhiteSpace(args.Result.FirstPart))
                {
                    HtmlDocument doc = new HtmlDocument { OptionWriteEmptyNodes = true };
                    doc.LoadHtml(args.Result.FirstPart);

                    if (doc.DocumentNode != null)
                    {
                        // Search for all img tags
                        HtmlNodeCollection imgTag = doc.DocumentNode.SelectNodes("//img");
                        if (imgTag == null || imgTag.Count == 0)
                            return;

                        foreach (HtmlNode node in imgTag)
                        {
                            if (node.Attributes["src"] != null && node.ParentNode != null)
                            {
                                string imgUrl = node.Attributes["src"].Value;

                                node.Attributes.Add("data-responsive-bkg-desktop", ImageHelper.GetResizedExternalUrl(imgUrl, 580, 0));
                                node.Attributes.Add("data-responsive-bkg-tablet", ImageHelper.GetResizedExternalUrl(imgUrl, 350, 0));
                                node.Attributes.Add("data-responsive-bkg-mobile", ImageHelper.GetResizedExternalUrl(imgUrl, 200, 0));
                                node.Attributes["src"].Value = "/_Images/loader.gif";
                                string currentClass = node.Attributes["class"] != null ? node.Attributes["class"].Value : "";
                                node.Attributes.Remove("class");
                                node.Attributes.Add("class", (string.IsNullOrWhiteSpace(currentClass) ? "" : currentClass + " ") + "responsive-bkg");
                                node.Attributes.Remove("width");
                                node.Attributes.Remove("height");
                                if (node.Attributes["style"] != null && !string.IsNullOrWhiteSpace(node.Attributes["style"].Value)
                                    && (node.Attributes["style"].Value.Contains("width") || node.Attributes["style"].Value.Contains("height")))
                                {
                                    node.Attributes.Remove("style");
                                }
                            }
                        }

                        // Replace the Rich Text content with the modified content
                        args.Result.FirstPart = doc.DocumentNode.OuterHtml;
                    }
                }
            }
            catch (Exception ex)
            { Log.Error("Error in MySite.Customizations.Customized_Sitecore.ProcessRichTextImages:" + ex.Message, ex); }
        }
    }
}

We used the Sitecore Image Processor Module to get the resized sitecore images.

        public static string GetResizedSitecoreUrl(string imageUrl, int width, int height, bool centerCrop = true)
        {
            if (string.IsNullOrWhiteSpace(imageUrl)) return string.Empty;

            if (height > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&h=" : "?h=") + height;
            if (width > 0)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&w=" : "?w=") + width;
            if (centerCrop)
                imageUrl = imageUrl + (imageUrl.Contains('?') ? "&" : "?") + "usecustomfunctions=1&centercrop=1";

            return imageUrl;
        }

Additionally – we also removed any height / width attributes added to the images in the rich text field – since the design in our case allowed for it.
We added this to the renderField processor:

<configuration>
  <sitecore>
    <pipelines>
      <renderField>
        <processor type="MySite.Customizations.Customized_Sitecore.ProcessRichTextImages, MySite" />
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Now that we had the html the way we required it for the blazy plugin, all that was needed was to include the script and initialize it:

<script src="blazy.js"></script>

And the following needed to go into the $(document).Ready() method.

var bLazy = new Blazy({
	selector: '.responsive-bkg',
    offset: 200,
    breakpoints: [{
			width: 768,
			src: 'data-responsive-bkg-mobile'
		},
		{
		      width: 980
		    , src: 'data-responsive-bkg-tablet'
		}],
	src: 'data-responsive-bkg-desktop'
});

The images would now load in a lazy-load fashion based on the current viewport breakpoint, when the cursor was within 200px of the image iteself. (Refer bLazy Documentation)

Adding query support to datasources on renderings for a Sitecore multisite instance

Sitecore 8, I believe has inherent support for sitecore queries in the ‘Datasource Location’ field of renderings!
But for those of us waiting on client approvals to make the move from Sitecore 7.xx :(, here’s the way to add that support!
This will come in very handy especially while working with shared renderings to be used across multiple sites in your Sitecore instance.

2015-11-01_042131

We added the following code to the getRenderingDatasource pipeline:

using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetRenderingDatasource;

namespace MySite.Customizations.Customized_Sitecore
{
    public class GetMultisiteRenderingDatasource
    {
        public void Process(GetRenderingDatasourceArgs args)
        {
            Assert.IsNotNull(args, "args");

            string text = args.RenderingItem["Datasource Location"]; 
            if (!string.IsNullOrEmpty(text))
            {
                if (text.StartsWith("query:") && !string.IsNullOrEmpty(args.ContextItemPath))
                {
                    var contextItem = args.ContentDatabase.GetItem(args.ContextItemPath);

                    if (contextItem != null)
                    {
                        text = text.Remove(0, 6); 
                        var item = contextItem.Axes.SelectSingleItem(text);

                        if (item != null)
                        {
                            args.DatasourceRoots.Add(item);
                        }
                    }
                }
            }
        }  
    }
}

The following configuration added it to the desired pipeline

<sitecore>
  <configuration>
    <pipelines>
      <getRenderingDatasource>
        <processor type="MySite.Customizations.Customized_Sitecore.GetMultisiteRenderingDatasource,MySite"
                   patch:before="processor[@type='Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation, Sitecore.Kernel']"/>
      </getRenderingDatasource>
    </pipelines>
  </configuration>
</sitecore>

Setting site specific workflows to items using shared templates in a multisite Sitecore solution

In a multisite solution, we might come across the need to have site specific workflows.
And if we use glassmapper with codegen – we might want to reuse as many templates as we possibly can with the basic goal of reusing as much code as possible.

In the regular scenario, we would set the desired workflow for an item in the ‘Default Workflow’ field of the standard values item of the template. But since we are sharing templates across sites – this doesn’t really work for us!

To resolve this, we came up with a solution – which basically sets the workflow on an item depending on its location in the content tree. We also added an option to turn on / off setting workflow based on the template used to create the item.

We added a field to the system template ‘Template’ which basically indicates if a given template is marked to go into the site workflow or not:

2015-08-30_223537

So this field would now be available in all templates – and we could check it for templates, items of which need to be added to the site workflow.

Now to associate a site with a workflow, we added a field to the SiteNode (A parent item of each site in our multisite instance). You could also choose to add this to your home node itself.

2015-08-30_225228

2015-08-30_224409

So now that we have a way to determine which workflow we need to set on an item based on its location in the content tree and the flag that tells us whether or not it needs to be added to a given workflow, all we need to do is add the event handler which will add the workflow to the item conditionally – when the item is created.

Here’s the code we used in the event handler:

using Sitecore.Configuration;
using Sitecore.Data.Events;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.SecurityModel;
using System;
using System.Configuration;
using Sitecore.Workflows;
using SharedSource.Helpers;
using Event = Sitecore.Events.Event;

namespace shared._Classes.Shared.Customizations.Customized_Sitecore
{
    public class SetItemWorkflow
    {
        public void OnItemCreated(object sender, EventArgs args)
        {
            using (new SecurityDisabler())
            {
                var arg = Event.ExtractParameter(args, 0) as ItemCreatedEventArgs;
                if (arg == null || (Sitecore.Context.Site != null && Sitecore.Context.Site.Name != "shell")) return;

                try
                {
                    if (!SitecoreHelper.ItemRenderMethods.GetCheckBoxValueByFieldName("Use Site Workflow", arg.Item.Template)) return;

                    Item currentSiteNode = SitecoreHelper.ItemMethods.GetAncestorOrSelfByTemplateId(arg.Item, ConfigurationManager.AppSettings["TemplateGuid_SiteNode"]);
                    if (currentSiteNode == null) return;

                    string currentSiteNodeDefaultWorkflow = SitecoreHelper.ItemRenderMethods.GetRawValueByFieldName("Default Workflow", currentSiteNode, false);

                    if (string.IsNullOrWhiteSpace(currentSiteNodeDefaultWorkflow)) return;

                    using (new EditContext(arg.Item, SecurityCheck.Disable))
                    {
                        IWorkflow workflow = Factory.GetDatabase("master").WorkflowProvider.GetWorkflow(currentSiteNodeDefaultWorkflow);

                        if (workflow == null) return;
                        workflow.Start(arg.Item);
                    }
                }
                catch (Exception ex)
                {
                    Log.Error("Error on creating item (SetItemWorkflow):" + arg.Item.ID + Environment.NewLine + "StackTrace:" + ex.StackTrace, ex);
                }
            }
        }
    }
}

Some of the helper methods used here:

  • GetCheckBoxValueByFieldName: Gets the value from a checkbox field and casts it into a boolean value
  • GetAncestorOrSelfByTemplateId: Recursively goes up the content tree from the current item upto the content node, and returns the first item matched by template id passed in
  • GetRawValueByFieldName: Gets the raw value stored in the given field

Additionally, we add the configuration required to add this event handler to the item:created event:

<sitecore>
    <events>
      <event name="item:created">
        <handler type="shared._Classes.Shared.Customizations.Customized_Sitecore.SetItemWorkflow, shared" method="OnItemCreated" />
      </event>
    </events>
</sitecore>

Using Custom Routes in Sitecore MVC

Many a time, we come across the need to customize our urls in sitecore.
While doing this would definitely need overriding the item resolver in sitecore, additionally we use MVC routes to be able to define the route structure we need.

In our project, we found this especially useful for bucketable items and also certain special / listing pages – where we used the JS History API to account for paging / sorting filtering and the likes.

Consider the scenario of bucketable product items:

2015-06-01_012328

Now, we would definitely not want the product url correspond with the sitecore structure as shown above. It would be more likely that we would want the product url to perhaps embed a super category name, a category name, and may be a sku along with the product name itself. In this case, we will implement custom routes and also override the sitecore item resolver and link provide to make the circle complete.
This kind of implementation will also let you have separate presentations on every item if you wanted to, as opposed to the approach of using sitecore wildcard nodes where the presentation couldn’t be updated by the content author for individual items.

To achieve the url we want, the route would need to be defined:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
         routes.MapRoute("Products", "products/{supercategory}/{category}/{productname}/{sku}");
    }
}

Point to note: If you have multiple routes which are similar – in the sense that they start with the same sequence, you will want to add most specific to most generic route to your route table.

And in your global.asax.cs, you need to register the route(s) you just created:

public override void Application_Start()
{
     AreaRegistration.RegisterAllAreas();
     ...
     RouteConfig.RegisterRoutes(RouteTable.Routes);
}

Now you will need to add to the httpRequestBegin sitecore pipeline – a new itemresolver pipeline where you need to add the logic to identify the item from the route you just created above.
Basically, for any product, if you were to get the identifiers that were declared ({supercategory},{category},{productname},{sku} in this case), the code which fetches the corresponding product item from sitecore based on these identifiers, needs to be in the itemresolver code.

In this case, we will assume that the sku is a unique identifier for a product.
Note: If you are using an item name as an identifier, you will want to ensure that the item name is unique for the template you are searching for (in the location you are searching, if it is not the entire tree). It would be best to ensure that the item names are unique in this case, in the scope you will define for the item search. You could use this post, to ensure this and add a constraint to this effect: Unique item name constraint using index search in Sitecore.

namespace myassemblyname
{
    public class CustomItemResolver : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (Sitecore.Context.Item != null || Sitecore.Context.Database == null || args.Url.ItemPath.Length == 0) return;

            RouteBase route = RouteTable.Routes["Products"];

            if (route == null) return;

            RouteData routeData = route.GetRouteData(new HttpContextWrapper(HttpContext.Current));

            if (routeData == null) return;

            string productSku = (string)routeData.Values["sku"];

            if (string.IsNullOrWhiteSpace(productSku)) return;

            // Logic to fetch the product from sitecore / index based on the sku fetched from the url
            // you could optionally also use the other identifiers like the category / super category etc.
            Item product = ...

            if (product == null) return;

            Sitecore.Context.Item = product;
        }
    }
}

The config update here to add the above processor:

<pipelines>
    <httpRequestBegin>
        <processor type="myassemblyname.CustomItemResolver, myassemblyname" />
    </httpRequestBegin>
</pipelines>

In addition to this, to complete the circle here, you will also want to customize the your sitecore link provider here. This is so that the product urls when requested from the LinkManager are returned in the format you want it to, instead of the one corresponding to the sitecore folder structure (buckets in this case)

namespace myassemblyname
{
    public class CustomLinkProvider : LinkProvider
    {
        public override string GetItemUrl(Item item, UrlOptions urlOptions)
        {
            var holdUrlOptions = urlOptions;
            try
            {
                urlOptions.SiteResolving = Settings.Rendering.SiteResolving;

                string customUrl;

                // We want this to apply to only Products
                if (item.TemplateID.ToString() == "<Product template guid>")
                {
                    // Code to fetch the sku / category / super category of the product given that we have the product item itself in 'item'
                    string category = ...
                    string superCategory = ...

                    if (!string.IsNullOrWhiteSpace(category) && !string.IsNullOrWhiteSpace(superCategory))
                    {
                        // NOTE, if this is being called from an asynchronous tasks, context will be null
                        // in this case, we will need to set the url by replacing the identifiers with the corresponding values in the routes
                        if (HttpContext.Current != null)
                        {
                            var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext);
                            customUrl = urlHelper.RouteUrl("Products", new
                            {
                                supercategory,
                                category,
                                item.Name,
                                sku
                            });
                        }
                    }
                }
                catch
                {
                    urlOptions.SiteResolving = Settings.Rendering.SiteResolving;
                    return base.GetItemUrl(item, holdUrlOptions);
                }
            }
        }
    }
}

The linkprovider override will also need to be applied in the config:

<linkManager defaultProvider="custom">
    <providers>
        <add name="custom" type="myassemblyname.CustomLinkProvider, myassemblyname" addAspxExtension="false" alwaysIncludeServerUrl="false" encodeNames="true" languageEmbedding="never" languageLocation="filePath" lowercaseUrls="true" shortenUrls="true" useDisplayName="false" />
    </providers>
</linkManager>

So this should allow you to use product urls like the one below throughout your site.
Note: you are defining the custom route in a single place (in the RegisterRoutes method above), so this is a clean approach as well!

yourdomain.com/products/stationery/printer-cartridges/canon-cl-241/cl241

In addition to the above, you might also come across random scenarios where implementing custom routes may be a great idea. If you decide to use the history api to manage interactions like sorting / paging etc, to support maintaining listing states on page refresh, you will need custom routes implemented.

An example of this scenario, would be a product listing, accessed using yourdomain.com/products.
Say you were on page 2, had sorted by descending order of product name.
And the sameple resulting url would be:

yourdomain.com/products/params/2/ztoa

If you didnt have custom routes implemented, and refreshed the page, you would ideally land on a 404, since the route till ‘products’ corresponds to the sitecore tree, but the remaining don’t.

To get around this, you could use a route definition like:

     routes.MapRoute("ProductListing", "products/params/{*args}");

Having a separator string like ‘params’ would be advised here so that the routes don’t interfere with each other.
Also, {*args} will correspond to a variable number of arguments.
You could also fetch the entire string of these variable arguments in your controller action using the method signature:


public ActionResult CustomIndex(string args = null)
{
     // args will contain the string after params, in this example '2/ztoa'
     // this string could be split and used to return the page to its intended state if required!
}

Happy routing!

Wrapping Rich Text Value in paragraph tag in Sitecore

Often we find the situation where the front end requires all text coming in from rich text fields in sitecore to be wrapped in say a <p> tag, for styling purposes.
In most cases, it is a better option to achieve this through code, than leave it up to the content authors.

You could go with one of the following 2 approaches:

You could create a new pipeline event in the

  • <saveRichTextContent> pipeline – This could enable you to append the <p> tag when you hit save on the rich text editor in sitecore
  • <renderField> pipeline – This could on the fly wrap your text into <p></p> tag while rendering the page, if the <p> tag was not there in the original rtf text.

If you go for method 1: <saveRichTextContent>
You could add to the pipeline in web.config:

<processor type="Sitecore72.Classes.WrapRichTextInParagraphOnSave, Sitecore72" />

And you could use the following corresponding code:

    namespace Sitecore72.Classes
    {
        public class WrapRichTextInParagraphOnSave
        {
            public void Process(SaveRichTextContentArgs args)
            {
                if (!(args.Content.Trim().StartsWith("<p>") && args.Content.Trim().EndsWith("</p>")))
                    args.Content = "<p>" + args.Content + "</p>";
            }
        }
    }

Please note, that this pipeline gets triggered only when you use the Show Editor buttong of a rich text field:
2014-06-14_215418

If you go for method 2: <renderField>

To append to this pipeline you would use this config:

<processor type="Sitecore72.Classes.WrapRichTextInParagraphOnRender, Sitecore72" />

And you could use the following corresponding code:

    namespace Sitecore72.Classes
    {
        public class WrapRichTextInParagraphOnRender
        {
            public void Process(RenderFieldArgs args)
            {
                if (args.FieldTypeKey == "rich text" && !(args.Result.FirstPart.Trim().StartsWith("<p>") && args.Result.FirstPart.Trim().EndsWith("</p>")))
                    args.Result.FirstPart = "<p>" + args.Result.FirstPart + "<//p>";
            }
        }
    }

For both these, ensure you add reference to Sitecore.Kernel.dll.

ASP.Net Caching and Sitecore (HttpContext.Current.Cache vs. HttpRuntime.Cache)

This post is about using ASP.Net caching with sitecore, separate from the item caching that sitecore itself provides. (Sitecore Caching)

We are all possibly aware of the way to use the cache accessible using the HttpContext.Current.Cache property. In the scenario of Sitecore, we might also want to add the functionality to clear all cached data on any publish. Sitecore itself does clear all the data that it caches using the following event at the end of the publish process:


<event name="publish:end">
 <handler type="Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel" method="ClearCache">
 <sites hint="list">
 <site>website</site>
 </sites>
 </handler>
 </event>
 <event name="publish:end:remote">
 <handler type="Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel" method="ClearCache">
 <sites hint="list">
 <site>website</site>
 </sites>
 </handler>
 </event>

But this will only clear the keys which Sitecore itself created.

If there was any data which you have manually cached in your site, they will remain unaffected.

Say we used the following to insert data into the cache:

HttpContext.Current.Cache.Insert("Key", "Value");

And then to the publish pipeline we add the following:

<pipelines>
 <publishItem help="Processors should derive from Sitecore.Publishing.Pipelines.PublishItem.PublishItemProcessor">
 <processor type="MySite.Classes.ClearHttpCache, MySite" />
 </publishItem>
</pipelines>

Which would look something like this:

using Sitecore.Publishing.Pipelines.PublishItem;

namespace MySite.Classes
{
 public class ClearHttpCache : PublishItemProcessor
 {
 public override void Process(PublishItemContext context)
 {
 var cacheEnumerator = HttpContext.Current.Cache.GetEnumerator();
 while (cacheEnumerator.MoveNext())
 HttpContext.Current.Cache.Remove(cacheEnumerator.Key.ToString());
 }
 }
}

When we go ahead and implement this, you’ll see that this still doesn’t work!
Here comes the distinction between HttpContext.Current.Cache and HttpRuntime.Cache.

The cache is stored in the session established for the front end site, while we are attempting to clear this cache in a session established for the sitecore editor (a different session). And HttpContext.Current.Cache is not shared across instances. HttpContext.Current.Cache internally does use HttpRuntime.Cache which infact is shared across instances for a given app domain.

Using HttpRuntime.Cache is the key here which will enable us to achieve this functionality. You could define a prefix for the keys which you would want cleared out on publish if you wouldn’t want all the keys in cache cleared.

So you’d use the following:

// Insert
HttpRuntime.Cache.Insert("Key", "Value");

// Access
var cacheValue = HttpRuntime.Cache["Key"];

And to clear the cache – on publish:

using Sitecore.Publishing.Pipelines.PublishItem;

namespace MySite.Classes
{
 public class ClearHttpCache : PublishItemProcessor
 {
 public override void Process(PublishItemContext context)
 {
 var cacheEnumerator = HttpRuntime.Cache.GetEnumerator();
 while (cacheEnumerator.MoveNext())
 HttpRuntime.Cache.Remove(cacheEnumerator.Key.ToString());
 }
 }
}

Customizing rich text fields in Sitecore on accept action

Objective:

Post process rich text field values entered by content authors when the RTE ‘Accept’ action is triggered (Sitecore 6.5).

Introduction:

Sitecore provides a Rich Text Field which basically lets the content author use either the inbuilt rich text editor or pure HTML to insert rich text content in a given item.

In some scenarios we might want to have some level of control over the entered text programmatically. The following snippet gives you access to the text entered by the content author, when they click on accept from the rich text editor and lets you parse it and then replace the entered text with your processed text.

Please note, the pipeline in use here is not used if the content author uses the ‘Edit Html’ action from the RTF straight up!

2014-04-26_134449

But do note that you can edit Html even from the rich text editor itself!

2014-04-26_134619

Implementation:

Really the only important thing here is to use the right pipeline, provide accurate assembly / class names and use the right method signature in the class!

Here is the configuration update to add to the saveRichTextContent pipeline.


<pipelines>
	<saveRichTextContent>
        	<processor type="Sitecore.Shell.Controls.RichTextEditor.Pipelines.SaveRichTextContent.EmbedInParagraph, Sitecore.Client" />
        	<processor type="Mysite.Global.Classes.SaveRelativeLinks, Mysite" />
	</saveRichTextContent>
</pipelines>

Since the content of a rich text editor is primarily Html, you could use the HtmlAgilityPack to access html nodes with ease. In the below example, we are looping to every anchor tag in the rich text field and we could possibly transform it or even remove the anchor tag if the href url didn’t pass through certain filters!


    public class SaveRelativeLinks
    {
        public void Process(SaveRichTextContentArgs args)
        {
            // Load the HTML into the HtmlAgilityPack
            var doc = new HtmlDocument { OptionWriteEmptyNodes = true };
            doc.LoadHtml(args.Content);

            // Search for all links
            var aTags = doc.DocumentNode.SelectNodes("//a");
            if (aTags == null || aTags.Count == 0)
                return;

            foreach (var node in aTags)
            {
                if (node.Attributes["href"] != null
                    && !string.IsNullOrEmpty(node.Attributes["href"].Value))
                {
                    // perform any logic you'd want on the node here!
                    // ...
                    // ...
                }
            }
            // Replace the Rich Text content with the modified content
            args.Content = doc.DocumentNode.OuterHtml;
        }
    }