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

Searching on module content on pages in Sitecore

When developing search implementation for a Sitecore site, we usually do an index search on the internal _content field which is an accumulation of all the string fields on the current item. Often the pages have a lot of important data on module datasources as opposed to be on the context item itself. We also have reference fields on the context item which might warrant updating the search results based on the business importance of such data.

To ensure that your search functionality also takes into account this additional data while retrieving results, we added a couple of computed fields to account for this data.

  • ModuleContent – This computed field accesses the presentation of an item and finds the renderings with datasources on them. It then adds all the string content on these datasource items to the computed field.
  • ReferenceContent – This computed field accesses all the fields on the context item which are reference fields and iterates through the referenced items to add into the index, the string fields of all these referenced items.
  • Now, while we can create new computed fields, we could also append to _content, which will let the search code unchanged. Writing to the _content field from the computed index field code simply appends the data into the existing field, thus extending it.

    public class ModuleContent : IComputedIndexField
        {
            public string FieldName { get; set; }
            public string ReturnType { get; set; }
    
            public object ComputeFieldValue(IIndexable indexable)
            {
                Item obj = indexable as SitecoreIndexableItem;
                if (obj?.Template == null)
                    return null;
    
                if (obj.Paths.IsContentItem)
                {
                    try
                    {
                        LayoutDefinition layout = LayoutDefinition.Parse(LayoutField.GetFieldValue(obj.Fields["__Renderings"]));
    
                        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)
                                    {
                                        if (!string.IsNullOrWhiteSpace(renderingDefinition.Datasource))
                                        {
                                            Item datasource = SitecoreHelper.GetItemByID(renderingDefinition.Datasource);
    
                                            if (datasource != null)
                                            {
                                                return datasource.Fields.Where(f => !f.Name.StartsWith("__")
                                                    && f.Name != IBase_Rendering_AssetsConstants.Rendering_ScriptsFieldName
                                                    && f.Name != IBase_Rendering_AssetsConstants.Rendering_StylesheetsFieldName
                                                    && (f.Type == "Single-Line Text" || f.Type == "Multi-Line Text" || f.Type == "Rich Text"))
                                                    .Select(f => datasource.Fields[f.ID]?.Value).Where(v => !string.IsNullOrWhiteSpace(v));
                                            }
                                        }
                                    }
                                }
                            }
                        }
    
                        return new List<string>();
                    }
                    catch (Exception ex)
                    {
                        Log.Error("Site.Project._classes.Computed_Fields.ModuleContent", ex, this);
                    }
                }
    
                return string.Empty;
            }
        }
    

    In the configuration, we include the computed field:

    <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
      <sitecore>
        <contentSearch>
          <indexConfigurations>
            <defaultLuceneIndexConfiguration type="Sitecore.ContentSearch.LuceneProvider.LuceneIndexConfiguration, Sitecore.ContentSearch.LuceneProvider">
              <fields hint="raw:AddComputedIndexField">
                <field fieldName="_content" returnType="stringCollection" storageType="YES">
                  Site.Domain._classes.Computed_Fields.ModuleContent,Site.Domain
                </field>
              </fields>
            </defaultLuceneIndexConfiguration>
          </indexConfigurations>
        </contentSearch>
      </sitecore>
    </configuration>
    

    In a similar manner, we could include the reference item string fields, except we would filter by link / list field tyes like Multilist / Treelist / Droptree / Droplink etc in the code, to get the guids, then items and then string fields on them, to include in the index for the current item.

    When time comes for the search, we would simply do a free text index search on the _content field to get results.

    Hope this helps!