Loading rendering wise CSS / JS in Sitecore

For a client, we came across a request for enabling rendering wise CSS and JS assets. This is definitely something that should be used judicially to balance the performance benefit of not load up unnecessary css / js vs the performance hit to load up multiple files over multiple http requests.

None the less, this is how we decided to tackle this requirement.

  • New fields in Controller Rendering template
  • A controller / action to return the list of rendering assets for the given item
  • Partial views that use the data from the above method to output the script / link tags
  • Update the layout file to include a call to the controller action and the views built above

We added fields to the controller rendering template at /sitecore/templates/System/Layout/Renderings/Controller rendering to accept a pipe delimiited list of js / css filenames with relative paths.

We additionally also had a base template created with the same set of fields, which any module could inherit from if needed. So if you had modules for which you might want to change the look and feel / behavior based on datasource, you could use this provision.

The new controller action created here looped through the presentation of the current item, picked up the list of stylesheets and script files and returned the collection back.
This code also looks into rendering datasources, so see if they include assets as well.

    public class RenderingAssetsController : GlassController
    {
        public static Assets GetAssets()
        {
            LayoutDefinition layout = LayoutDefinition.Parse(LayoutField.GetFieldValue(Sitecore.Context.Item.Fields["__Renderings"]));
            Assets assets = new Assets { Scripts = new List<string>(), Stylesheets = new List<string>() };

            if (layout.Devices != null && layout.Devices.Count > 0)
            {
                DeviceDefinition dev = (DeviceDefinition)layout.Devices[0];
                ArrayList renderings = dev?.Renderings;
                if (renderings != null)
                {
                    IEnumerable<RenderingDefinition> renderingDefinitions = renderings.Cast<RenderingDefinition>();

                    foreach (RenderingDefinition renderingDefinition in renderingDefinitions)
                    {
                        if (renderingDefinition.ItemID != null)
                        {
                            Item renderingItem = SitecoreHelper.GetItemByID(renderingDefinition.ItemID);

                            string renderingScript =
                                renderingItem.GetRawValue(
                                    ItemTree.Templates.System.Layout.Renderings.Controller_Rendering.Assets
                                        .Rendering_Scripts.ItemID);

                            if (!string.IsNullOrWhiteSpace(renderingScript))
                            {
                                assets.Scripts.AddRange(
                                    renderingScript.Split(new[] {'|'}, StringSplitOptions.RemoveEmptyEntries)
                                        .Select(s => "/" + s.TrimStart('/')));
                            }

                            string renderingStylesheet =
                                renderingItem.GetRawValue(
                                    ItemTree.Templates.System.Layout.Renderings.Controller_Rendering.Assets
                                        .Rendering_Stylesheets.ItemID);

                            if (!string.IsNullOrWhiteSpace(renderingStylesheet))
                            {
                                assets.Stylesheets.AddRange(
                                    renderingStylesheet.Split(new[] {'|'}, StringSplitOptions.RemoveEmptyEntries)
                                        .Select(s => "/" + s.TrimStart('/')));
                            }

                            if (!string.IsNullOrWhiteSpace(renderingDefinition.Datasource))
                            {
                                Item datasource = SitecoreHelper.GetItemByID(renderingDefinition.Datasource);

                                if (datasource != null)
                                {
                                    Base_Rendering_Assets renderingAssets =
                                        ScService.Cast<Base_Rendering_Assets>(datasource);

                                    if (!string.IsNullOrWhiteSpace(renderingAssets.Rendering_Stylesheets))
                                    {
                                        assets.Stylesheets.AddRange(
                                            renderingAssets.Rendering_Stylesheets.Split(new[] {'|'},
                                                    StringSplitOptions.RemoveEmptyEntries)
                                                .Select(s => "/" + s.TrimStart('/')));
                                    }

                                    if (!string.IsNullOrWhiteSpace(renderingAssets.Rendering_Scripts))
                                    {
                                        assets.Scripts.AddRange(
                                            renderingAssets.Rendering_Scripts.Split(new[] {'|'},
                                                    StringSplitOptions.RemoveEmptyEntries)
                                                .Select(s => "/" + s.TrimStart('/')));
                                    }
                                }
                            }
                        }
                    }
                }
            }

            assets.Scripts = assets.Scripts.DistinctBy(s => s).ToList();
            assets.Stylesheets = assets.Stylesheets.DistinctBy(s => s).ToList();

            return assets;
        }
    }

The partials created would be simple – they just take in the list of assets and output them in the right format.

RenderingScripts Partial

@using System.Linq

@model List<string>

@if (Model != null && Model.Any())
{
    foreach (string script in Model)
    {
        <script type="text/javascript" src="@script"></script>
    }
}

RenderingStylesheets Partial

@using System.Linq

@model List<string>

@if (Model != null && Model.Any())
{
    foreach (string stylesheet in Model)
    {
        <link rel="stylesheet" href="@stylesheet" />
    }
}

In the layout code, you would invoke the controller action above and then embed the partials in the intended location in the html passing in the list of assets returned from the controller action call.

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

Glass html helpers for responsive images in Sitecore with bLazy

The bLazy Plugin provides us with a great way to implement serving up resized images based on the current viewport, and additionally it also allows us to lazy load images on our site, drastically reducing the page size and download time.

We used bLazy for img tags – and also for other tags (where it simply loads up the image as a background image).

We created a few glass HTML helpers here to help us maintain consistency and increase maintainability by having this html present in only 1 place in the site.

        public HtmlString RenderResponsiveLazyImage(Expression<Func<TModel, object>> field,
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions, 
            bool isEditable = true, NameValueCollection parameters = null)
        {
            return RenderResponsiveLazyImage(Model, field, mobileDimensions, tabletDimensions, desktopDimensions, isEditable, parameters);
        }

        public HtmlString RenderResponsiveLazyImage<T>(T model, Expression<Func<T, object>> field, 
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions,
            bool isEditable = true, NameValueCollection parameters = null)
        {
            Image imageField = field.Compile().Invoke(model) as Image;

            if (imageField != null)
            {
                StringBuilder responsiveImageHtml = new StringBuilder();
                StringBuilder parametersHtml = new StringBuilder();
                if (parameters != null)
                {
                    var items = parameters.AllKeys.SelectMany(parameters.GetValues, (k, v) => new { key = k, value = v });
                    foreach (var item in items)
                    {
                        parametersHtml.Append(" " + item.key + "=\"" + item.value + "\"");
                    }
                }

                if (Sitecore.Context.PageMode.IsPageEditor)
                {
                    responsiveImageHtml.Append(GlassHtml.RenderImage(model, field, null, isEditable));
                }
                else if (!Sitecore.Context.PageMode.IsPageEditor && imageField.IsValid())
                {
                    responsiveImageHtml.Append("<img" + (parametersHtml.Length == 0 ? "" : parametersHtml.ToString())
                        + " src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\""
                        + " data-lazily-respond-mobile=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, 
                            mobileDimensions != null ? mobileDimensions.Width : 0, mobileDimensions != null ? mobileDimensions.Height : 0) + "\""
                        + " data-lazily-respond-tablet=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, 
                            tabletDimensions != null ? tabletDimensions.Width : 0, tabletDimensions != null ? tabletDimensions.Height : 0) + "\""
                        + " data-lazily-respond-desktop=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, 
                            desktopDimensions != null ? desktopDimensions.Width : 0, desktopDimensions != null ? desktopDimensions.Height : 0) + "\""
                        + " alt=\"" + imageField.Alt + "\" />");
                }

                return new HtmlString(responsiveImageHtml.ToString());
            }

            return new HtmlString(string.Empty);
        }

        public string RenderResponsiveLazyImageAttributes(Expression<Func<TModel, object>> field,
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions)
        {
            return RenderResponsiveLazyImageAttributes(Model, field, mobileDimensions, tabletDimensions, desktopDimensions);
        }

        public string RenderResponsiveLazyImageAttributes<T>(T model, Expression<Func<T, object>> field,
            Dimensions mobileDimensions, Dimensions tabletDimensions, Dimensions desktopDimensions)
        {
            Image imageField = field.Compile().Invoke(model) as Image;

            if (imageField != null && imageField.IsValid())
            {
                StringBuilder responsiveLazyImageHtml = new StringBuilder();

                if (mobileDimensions != null)
                {
                    responsiveLazyImageHtml.Append("data-lazily-respond-mobile=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, mobileDimensions.Width, mobileDimensions.Height) + "\"");
                }

                if (tabletDimensions != null)
                {
                    responsiveLazyImageHtml.Append(" data-lazily-respond-tablet=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, tabletDimensions.Width, tabletDimensions.Height) + "\"");
                }

                if (desktopDimensions != null)
                {
                    responsiveLazyImageHtml.Append(" data-lazily-respond-desktop=\""
                        + ImageHelper.GetResizedSitecoreUrl(imageField.Src, desktopDimensions.Width, desktopDimensions.Height) + "\"");
                }

                return responsiveLazyImageHtml.ToString();
            }
            return string.Empty;
        }

        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;
        }

We used Sitecore Image Processor Module to be able to resize sitecore images here.

Where Dimensions is:

    public class Dimensions
    {
        public int ScreenSize { get; set; }
        public int Width { get; set; }
        public int Height { get; set; }
        public bool IsUrlBlank { get; set; }

        public Dimensions(int width, int height)
        {
            Width = width;
            Height = height;
        }

        public Dimensions()
        { }
    }

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>

Multisite configuration for shared Sitecore rendering parameters

On regular templates, we can use sitecore queries to accommodate for showing items in Sitecore link / list fields from respective site nodes in multi site instances.

For Eg:

query:./ancestor::*[@@templatename='Site Node']/Global/Components/Data/Tip Types/*

However, this will not apply to rendering parameter templates. Since while selecting the rendering parameter values in the presentation of an item, the context item is the rendering itself and not the item on which the presentation is being set.
So if we were using a common rendering parameters template for items in multiple sites, we need to rig something up, so that the options appearing in the link / list type rendering parameters show options from the respective site node.

As an example, consider a rendering parameter template, with a background color field.
2015-10-31_235421

This rendering parameter is set to a shared rendering which is used on multiple sites. However, each site has its own unique list of background color options.
To solve this issue, we needed to add support for the token like $sitenode in the example above.

The handler code we write, replaces this token with the right path, after identifying which item the presentation is being set on, and then identifying its sitenode ancestor.

using Sitecore.Data.Items;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Shell.Applications.ContentManager;
using Sitecore.Text;
using Sitecore.Web;
using System.Configuration;
using System.Linq;
using SharedSource.Helpers;

namespace MySite.Customizations.Customized_Sitecore
{
    public class MultisiteRenderingParamDroplink : LookupEx
    {
        public new string Source
        {
            get { return base.Source; }
            set
            {
                if (value.Contains("$sitenode"))
                {
                    Item contextItem = Sitecore.Context.ContentDatabase.Items[ItemID];

                    if (contextItem.Template.BaseTemplates
                        .Any(x => x.ID == ItemTree.Templates.System.Layout.Rendering_Parameters.Standard_Rendering_Parameters.ItemID))
                    {
                        string url = WebUtil.GetQueryString();
                        if (!string.IsNullOrWhiteSpace(url) && url.Contains("hdl"))
                        {
                            FieldEditorParameters parameters = FieldEditorOptions.Parse(new UrlString(url)).Parameters;
                            var currentItemId = parameters["contentitem"];
                            if (!string.IsNullOrEmpty(currentItemId))
                            {
                                Sitecore.Data.ItemUri contentItemUri = new Sitecore.Data.ItemUri(currentItemId);
                                contextItem = Sitecore.Data.Database.GetItem(contentItemUri);

                                Item siteNode = SitecoreHelper.GetAncestorOrSelfByTemplateId
                                    (contextItem, ConfigurationManager.AppSettings["TemplateGuid_SiteNode"]);

                                if (siteNode != null)
                                {
                                    base.Source = value.Replace("$sitenode", siteNode.Paths.FullPath);
                                    return;
                                }
                            }
                        }
                    }
                }
                base.Source = value;
            }
        }
    }
}

Helpers:

            public static Item GetAncestorOrSelfByTemplateId(Item item, string templateId)
            {
                if (item == null || string.IsNullOrEmpty(templateId))
                {
                    return null;
                }

                ID parsedTemplateId;
                return ID.TryParse(templateId, out parsedTemplateId)
                           ? GetAncestorOrSelfByTemplateId(item, parsedTemplateId)
                           : null;
            }

            public static Item GetAncestorOrSelfByTemplateId(Item item, ID templateId)
            {
                if (item == null || templateId == ID.Null || templateId == ID.Undefined)
                {
                    return null;
                }

                Item templateItem = item.Database.GetItem(templateId);
                return templateItem == null ? null : GetAncestorOrSelfByTemplateItem(item, templateItem);
            }

We then register an extension with this type in sitecore:

    <controlSources>
      <source mode="on" namespace="MySite.Customizations.Customized_Sitecore" assembly="MySite" prefix="contentExtension" />
    </controlSources>

This extension is then added to the regular Droplink field. The logic outlined above will kick in only when the current item has the template: /sitecore/templates/System/Layout/Rendering Parameters/Standard Rendering Parameters as its base template. Which would ideally apply to all rendering parameter templates.

2015-11-01_022532

Now the ‘$sitenode’ token would get translated as per the logic added in the above code to the respective site node.

Output Html / Encoded characters using glass html helpers in Sitecore

When a content author enters decoded symbols in single line / multiline sitecore text fields, the symbols don’t get encoded on their own on the site when we output them with @Editable (Glass method – supporting page editor mode).
This also applies to links rendered using @RenderLink (Glass method)
This would mean text like &reg; and &trade; getting output in place of ® and ™ respectively.

Sitecore text fields DO support these symbols being pasted on to them directly, but sometimes it is not a choice we can make, especially if we have items created and prefilled automatically through external code / a connector to a third party system etc.

To achieve this, we create a new class inheriting from the GlassView class, and create Editable / RenderLink methods which use the respective base methods, taking care of the HtmlDecode!

public abstract class MyGlassView<TModel> : GlassView<TModel>
    {
        // *********************************** Text ***********************************

        public new HtmlString Editable(Expression<Func<TModel, object>> field, object parameters = null)
        {
            return new HtmlString(HttpUtility.HtmlDecode(GlassHtml.Editable(Model, field, parameters)));
        }

        public new HtmlString Editable(Expression<Func<TModel, object>> field, Expression<Func<TModel, string>> standardOutput, object parameters = null)
        {
            return new HtmlString(HttpUtility.HtmlDecode(GlassHtml.Editable(Model, field, standardOutput, parameters)));
        }

        // *********************************** Links ***********************************

        public HtmlString RenderLink<T>(T model, Expression<Func<T, object>> field, object attributes = null,
            bool isEditable = false, string contents = null, bool isContentEmpty = false)
        {
            if (field == null)
                return new HtmlString(string.Empty);

            return new HtmlString(HttpUtility.HtmlDecode(GlassHtml.RenderLink(model, field, attributes, isEditable,
                GlobalHelper.GetLinkContents(field.Compile().Invoke(model) as Link, contents, isContentEmpty))));
        }

        public HtmlString RenderLink(Expression<Func<TModel, object>> field, object attributes = null,
            bool isEditable = false, string contents = null, bool isContentEmpty = false)
        {
            return RenderLink(Model, field, attributes, isEditable, contents, isContentEmpty);
        }
    }

All views will need to inherit from this new MyGlassView class to be able to use these new methods.

Using dynamic view markers in Sitecore MVC

While working with Sitecore, we add many renderings in the presentation and that makes up each page that is output for the corresponding item.

We are currently working with Sitecore MVC, and we have traditionally added start and end comments in views manually (copy pasting the path of the current view) to mark the start and end of each component on the page. This helped us debug things, made the output html more readable and maintainable, and lets face it, after a month or two, we find it hard to remember which module was called what!

Recently it was pointed out that this might be a potential security threat and we are exposing details of our folder structure in the production environment, which might not be the best idea.

So to ease the process, and remove this potential threat, we created a html helper extension method which would dynamically output the view path start and end comments subject to an appsetting value being toggled to true. This appsetting key would be toggled to false in any environment where we wouldn’t want the comments to be output!

        public static ViewMarker BeginViewMarker(this HtmlHelper htmlHelper, string filename)
        {
            htmlHelper.ViewContext.Writer.Write(RenderFilenameComment("START", filename));
            return new ViewMarker(htmlHelper.ViewContext, filename);
        }

        private static HtmlString RenderFilenameComment(string position, string filename)
        {
            if (ConfigurationManager.AppSettings["ShowViewMarkers"] == "true")
            {
                return new HtmlString(string.Format("<!-- {0}: {1} -->", position.ToUpper(CultureInfo.CurrentCulture), filename));
            }

            return new HtmlString("");
        }

        public class ViewMarker : IDisposable
        {
            private readonly TextWriter _writer;
            private readonly string _filename;
            public ViewMarker(ViewContext viewContext, string filename)
            {
                _writer = viewContext.Writer;
                _filename = filename;
            }

            public void Dispose()
            {
                _writer.Write(RenderFilenameComment("END", _filename));
            }
        }

And in the view, we would just enclose all the view html / razor within a call to this helper method:

@using (Html.BeginViewMarker(VirtualPath))
{
   ...
   ...
   ...
}

The output would look like:

2015-09-19_223640

Safely truncate Sitecore Rich Text / Html

We came across the need to be able to truncate text (html) entered in a rich text field to be able to display the same on compact modules / show article summaries etc.
There are many different approaches that can be taken here to be able to safely truncate html. But here’s the code that passed our testing and is currently live! We are parsing the html into XML to achieve this.

Please note:
This code truncates the text to the nearest word, and if the character limit is reached within the first word, the truncation is done mid word instead of returning no text at all. It will also allow you to optionally append ellipses to the truncated text.

        public static string TruncateHtml(string text, int charCount, bool appendEllipses = false)
        {
            text = HttpUtility.HtmlDecode(text);

            if (charCount <= 0) return text;

            try
            {
                // your data, probably comes from somewhere, or as params to a methodint 
                XmlDocument xml = new XmlDocument();
                xml.LoadXml("" + text + "");
                // create a navigator, this is our primary tool
                XPathNavigator navigator = xml.CreateNavigator();
                XPathNavigator breakPoint = null;

                // find the text node we need:
                while (navigator.MoveToFollowing(XPathNodeType.Text))
                {
                    int remainingCharacters = charCount;
                    charCount -= navigator.Value.Length;
                    if (charCount <= 0) { string lastText = TruncateText(navigator.Value, remainingCharacters) + (StripHtml(text).Length > charCount && appendEllipses ? "..." : "");
                        navigator.SetValue(lastText);
                        breakPoint = navigator.Clone();
                        break;
                    }
                }

                // first remove text nodes, because Microsoft unfortunately merges them without asking
                while (navigator.MoveToFollowing(XPathNodeType.Text))
                {
                    if (navigator.ComparePosition(breakPoint) == XmlNodeOrder.After)
                    {
                        navigator.DeleteSelf();
                    }
                }

                // moves to parent, then move the rest
                navigator.MoveTo(breakPoint);
                while (navigator.MoveToFollowing(XPathNodeType.Element))
                {
                    if (navigator.ComparePosition(breakPoint) == XmlNodeOrder.After)
                    {
                        navigator.DeleteSelf();
                    }
                }

                // moves to parent
                navigator.MoveToRoot();
                navigator.MoveToFollowing(XPathNodeType.Element);
                return navigator.InnerXml;
            }
            catch (Exception)
            {
                return text;
            }
        }

        private static string TruncateText(string str, int maxCharCount, bool appendEllipses = false)
        {
            if (string.IsNullOrWhiteSpace(str)) return string.Empty;

            str = HttpUtility.HtmlDecode(str);
            if (str.Length <= maxCharCount || maxCharCount == 0) return str; int originalStrLen = str.Length; if (str.Length > maxCharCount)
                str = str.Substring(0, maxCharCount + 1);

            int ellipsePos = str.LastIndexOfAny(new[] { ' ', '.', ',', ';', '!', '-', ']', '}', ')', '*' });
            if (ellipsePos != -1 && str.Length > ellipsePos && ellipsePos > 0)
                str = str.Substring(0, ellipsePos);

            if (ellipsePos == -1)
            {
                str = str.Substring(0, str.Length - 1);
            }

            if ((str.Length < originalStrLen) && appendEllipses)
            {
                str = str.TrimEnd('.').TrimEnd(',').TrimEnd(';').TrimEnd('-');
            }

            return str + ((str.Length < originalStrLen) && appendEllipses ? "..." : string.Empty);
        }

The TruncateText() method only truncates a given string to the nearest word, while the TruncateHtml() method is responsible for converting the passed in text into XML and safely truncating the same, taking care of maintaining valid XML and hence valid HTML.

We have also wrapped any passed in text into an external XML tag , to make sure that passed in free text also can be parsed in this way.

When it came to using this in Sitecore, here’s an example of how we have put this to use!

@Html.Raw(Editable(Model, m => m.Title, x => TextUtilityStatic.TruncateHtml(x.Title, 50, true)))

So this way, the field would still be editable in page editor mode, but output the truncated html on the page!