Spatial search with SOLR now ridiculously simple in Sitecore 9

Spatial search was introduced as a built in feature in SOLR with Sitecore from Sitecore 9. This made it extremely easy to implement geo spatial searches based on global coordinates.

We had to achieve a couple of targets in our project, which we easily achieved using Sitecore’s new simple integrated functionality

  • Get a list of retailers closest to the current geo ip, sorted by retailers closest to me
  • Filter the list of retailers, by a maximum distance radius from me

All you need to do to get started here, is ensure that the template you use – to store coordinates, must inherit from /sitecore/templates/System/Geospatial/Coordinate

Once you do this, sitecore search API updates for spatial search will be supported in your code.

For instance, to be able to filter your results based on distance from a given point, all you need to do is

IQueryable search = context.GetQueryable  ().WithinRadius(s => s.Location, new Coordinate(40.7127, -74.0059), 100)

This query was used to return the list of retailers within a 100 km radius for New York City.
Ps. Note – the radius parameter here accepts the distance in kilometers! Please ensure you convert your data if needed.

To be able to just bring back a sorted list of results, based on distance from a given point –

IQueryable search = context.GetQueryable()
    .OrderByDistance(s => s.Location, new Coordinate(40.7127, -74.0059));

On a side note, if you wanted to calculate the distance between 2 coordinates, you have an extension method to GeoCoordinate available in System.Device.Location. A point to note here – is that the distance is returned in meters only – so please keep conversion in mind if it applies to you!

private double GetDistance(GeoCoordinate src, GeoCoordinate dest, bool isMetric)
        {
            // GetDistanceTo returns distance in meters
            return isMetric
                ? Math.Round(src.GetDistanceTo(dest) / 1000, 2)
                : Math.Round(MetersToMiles(src.GetDistanceTo(dest)), 2);
        }
Advertisements

Parse Sitecore General Link field

While I am aware that rendering parameters ideally should not contain content, our component has the special need which created the need to add a general link field on the rendering parameters template of a rendering.

Question came though – when we needed to use the data from this parameter. As we know, rendering parameters are only available in the code as strings, while general link field information is stored in an xml.

We needed these links to only support internal links, so the code does just that, however, you can obviously build on this, or use Sitecore’s internal properties on link field renderer to extend this code.

Here’s how we did it:

  [XmlRoot("link")]
  [Serializable]
  public class BasicLink
  {
    [XmlAttribute]
    public string text { get; set; }

    [XmlAttribute]
    public string anchor { get; set; }

    [XmlAttribute]
    public string linktype { get; set; }

    [XmlAttribute]
    public string @class { get; set; }

    [XmlAttribute]
    public string title { get; set; }

    [XmlAttribute]
    public string target { get; set; }

    [XmlAttribute]
    public string querystring { get; set; }

    [XmlAttribute]
    public string id { get; set; }

    public string Url
    {
      get
      {
        if (this.linktype == LinkType.Internal.ToString() && !this.id.IsNullOrWhiteSpace() && this.id.IsGuid())
          return LinkManager.GetItemUrl(Sitecore.Context.Database.GetItem(new ID(this.id)));
        return string.Empty;
      }
    }
  }

We then added a HTML helper extension method to allow us to transform the raw general link field data into the above new class:

public static BasicLink ToLink(this string raw)
    {
      if (string.IsNullOrWhiteSpace(raw))
        return (BasicLink) null;
      return (BasicLink) new XmlSerializer(typeof (BasicLink), new XmlRootAttribute("link")).Deserialize((TextReader) new StringReader(raw));
    }

Here’s a sample of the usage:

BasicLink ViewMoreLink = RenderingContext.Current.Rendering.Parameters["View More Link].ToLink(),

Include templates dynamically based on Sitecore field values in your SOLR index

In one of our projects, we had the need to create a separate index only for items of a certain set of templates – and the content admin needed to have the flexibility to edit this list of templates.

If this was only a question of including / excluding templates in the search, it would be a much better solution to just allow the content author to select a set of templates on a multilist field on a settings item some place.
However, from a performance perspective – this was the solution we definitely needed considering our content structure, and content editing process.

Here’s how we did it –

We added a field on /sitecore/templates/System/Templates/Template called ‘Exclude From Search Index’
We created a custom crawler then, to check this field on the item template, before adding it to the index. Do ensure you use this crawler only on non-sitecore system indexes to avoid Sitecore responding in an unexpected way.

using Foundation.Search.Models.sitecore.templates.Foundation.Search;
using Sitecore.ContentSearch;
using Sitecore.Data.Items;

namespace Foundation.Search.Solr.Crawlers
{
    public class SearchIndexCrawler : SitecoreItemCrawler
    {
        protected override bool IsExcludedFromIndex(SitecoreIndexableItem indexable, bool checkLocation = false)
        {
            bool isExcluded = base.IsExcludedFromIndex(indexable, checkLocation);

            if (isExcluded)
                return true;

            Item item = indexable;
            return item.Template.InnerItem[I_Indexing_SettingsConstants.Exclude_From_Search_IndexFieldName] == "1";
        }

        protected override bool IndexUpdateNeedDelete(SitecoreIndexableItem indexable)
        {
            Item item = indexable;
            return item.Template.InnerItem[I_Indexing_SettingsConstants.Exclude_From_Search_IndexFieldName] == "1";
        }
    }
}

You can then use this crawler in your site index:



web
/sitecore


Working with the Sitecore Query Builder field using SOLR

Sitecore’s query builder can be a very versatile tool for content authors if setup right. It can give the content authors the option to virtually select any set of items to display on a given module.
This, paired with the right set of custom facet definitions, can work wonders to optimize the content author experience.

1

Please refer my previous post about Adding search facets on computed fields in Sitecore 9 with SOLR, this will give you an idea about creating facets in the content editor folders / item searches.

The question addressed here, is once you have the content editor built query with you in the back end – how do you run this against your search engine to resolve this into a set of matching items?

Here’s the code to use with SOLR:

using Feature.Articles.Models;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Linq;
using Sitecore.ContentSearch.Utilities;
using Sitecore.Data;
using Sitecore.Data.Items;
using System;
using System.Collections.Generic;
using System.Linq;
using SearchHelper = Foundation.Search.Solr.Helpers.SearchHelper;

namespace Feature.Articles.Services
{
    public class ArticleTilesService
	{
		public IEnumerable GetPagesByQuery(string query)
		{
		    ISearchIndex index = ContentSearchManager.GetIndex("sitecore_master_index");
            using (IProviderSearchContext context = index.CreateSearchContext())
			{
				List stringModel = SearchStringModel.ExtractSearchQuery(query);
				IQueryable result = LinqHelper.CreateQuery(context, stringModel);

				string contextLanguage = Sitecore.Context.Language.ToString();

				result = result
					.Where(r => r.Language == contextLanguage)
					.OrderByDescending(r => r.ArticleDate);

				SearchResults resultArticles = result.GetResults();
				return resultArticles.Select(r => r.Document).ToList();
			}
		}
     }
}

Please note – It is advisable to add the following constraints / filters to the search query from back end code, in addition to the query passed in from Sitecore, simply because it is a rather simple oversight to be made on the part of the content authors.

  • Search Location: This should typically at least be your site node or depending on the context, you could make it as specific as the context allows you to be.
  • Search Language: This is an easy oversight by the content author – do remember to add this filter by context language to get desired results.

Adding search facets on computed fields in Sitecore 9 with SOLR

Adding facets to help content authors in searching content is always a great idea, especially when you have rather large buckets with loads of items.

One thing I noticed while trying to set this up, was that I needed untokenized fields in the index to make this work as expected, while text fields by default are tokenized in solr.

To understand this better, lets take an example of a bucket of products – say jewelry. I have a field called Jewelry Type on my products. If I wanted to give the content author an option to find jewelry by this field, I could add a facet on the existing field.

I would expect to see facet values like these:

1

But instead, I would see separate facets for each word, like ‘Engagement’, ‘Rings’, ‘Right’, ‘Hand’ etc. That’s certainly not what we need.

So to do that – instead of changing your solr settings to treat all fields of the certain type as untokenized (which would open a different can of worms!), we created a computed field to populate the Jewelry Type values into an untokenized field. This would treat the whole field value as a complete string and not break it down.

This is a relatively straightforward computed field:

using Foundation.Search.Solr.Helpers;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;

namespace Foundation.Search.Solr.ComputedFields
{
	public class JewelryType : IComputedIndexField
	{
		public object ComputeFieldValue(IIndexable indexable)
		{
			SitecoreIndexableItem sitecoreIndexable = indexable as SitecoreIndexableItem;

			if (sitecoreIndexable == null) return null;

			if (sitecoreIndexable.Item.TemplateID.ToString() == Constants.ProductTemplateId)
			{
				return sitecoreIndexable.Item["JewelryType"];
			}

			return null;
		}

		public string FieldName { get; set; }
		public string ReturnType { get; set; }
	}
}

Now we set up the facet with this new computed field created: /sitecore/system/Settings/Buckets/Facets

Using the template: /sitecore/templates/System/Item Buckets/Facet

Where the field name – matches the name of the computed field you created.

2

You could choose to mark this facet as global or local.
If your facet is very specific to a particular bucketed item template, you could as well mark it as local, and then add it on the particular bucket item only.

3

 

Creating archive items in media library from media items in a Sitecore Scheduled Task

We had, in our project, documents uploaded pertaining to different document categories for each product in a product repository. On the product page – the user was to have the capability to download an archive / zip file of all documents pertaining to a product – for a given document category.

We had to dynamically generate the archive, based on the documents uploaded in media library and associated with the products.

We initially looked into generating the archive file on the fly when a user requested for a download. However this was not too efficient, especially when the files were larger in size. The wait time was far longer than acceptable.

So as a solution, we decided to create a scheduled task – which would

  • loop through the products, locate the media items associated with each product – each document category
  • create an archive of the group
  • save it as a new media item – with an indicator as to which product & document category it was associated with

In our implementation then, when a user requested for an archive download, we simply located the corresponding media library archive item based on the product & document category indicators, and output the download link!

    public class ProductDownloadsArchiveCreator
    {
        public void Execute(Item[] items, Sitecore.Tasks.CommandItem command, Sitecore.Tasks.ScheduleItem schedule)
        {
            try
            {
                Sitecore.Diagnostics.Log.Info("ProductDownloadsArchiveCreator :: Job Started", this);

                using (new BulkUpdateContext())
                {
                    IDocumentRepository _documentRepository = DocumentRepositoryFactory.Build();
                    Item archiveRootFolder =
                        SitecoreHelper.ItemMethods.GetItemFromGUIDInMaster(Constants.ItemGuids
                            .ProductDownloadsArchiveFolder);

                    archiveRootFolder.DeleteChildren();

                    List productDocuments = _documentRepository.GetAllProductDocuments();

                    List productsWithDocuments = GetProductGuidsWithDocuments(productDocuments);

                    if (!productsWithDocuments.Any()) return;

                    foreach (Guid productGuid in productsWithDocuments)
                    {
                        List currentProductDocuments =
                            productDocuments
                                .Where(d => d.MediaProducts != null && d.MediaProducts.Contains(productGuid)).ToList();

                        ProcessCurrentProductDocuments(currentProductDocuments, productGuid);
                    }
                }

                PublishArchiveFolder();

                Sitecore.Diagnostics.Log.Info("ProductDownloadsArchiveCreator :: Job Ended", this);
            }
            catch (Exception e)
            {
                Sitecore.Diagnostics.Log.Error("ProductDownloadsArchiveCreator :: Job Error", e, this);
            }
        }

        #region Private Methods

        private List GetProductGuidsWithDocuments(List productDocuments)
        {
            List productsWithDocuments = new List();

            foreach (DocumentResultItem document in productDocuments)
            {
                if (document.MediaProducts != null && document.MediaProducts.Any())
                {
                    productsWithDocuments.AddRange(document.MediaProducts);
                }
            }

            return productsWithDocuments.Distinct().ToList();
        }

        private void ProcessCurrentProductDocuments(List currentProfileDocuments, Guid productGuid)
        {
            try
            {
                List documentCategories = new List();
                foreach (DocumentResultItem document in currentProfileDocuments)
                {
                    if (document.MediaCategories != null && document.MediaCategories.Any())
                    {
                        documentCategories.AddRange(document.MediaCategories);
                    }
                }

                documentCategories = documentCategories.Distinct().ToList();

                foreach (Guid documentCategoryGuid in documentCategories)
                {
                    List currentProductCategoryDocuments =
                        currentProfileDocuments.Where(d =>
                            d.MediaCategories != null && d.MediaCategories.Contains(documentCategoryGuid)).ToList();

                    List currentProductCategoryClassifications =
                        currentProductCategoryDocuments.Select(d => d.DocumentClassification)
                            .Where(c => !string.IsNullOrWhiteSpace(c) && c.Trim() != string.Empty).Select(c => c.Trim())
                            .Distinct().ToList();

                    currentProductCategoryClassifications.Add(string.Empty);

                    foreach (string classification in currentProductCategoryClassifications)
                    {
                        List currentArchiveFiles = currentProductCategoryDocuments
                            .Where(d => string.IsNullOrWhiteSpace(classification)
                                ? string.IsNullOrWhiteSpace(d.DocumentClassification)
                                : d.DocumentClassification == classification).ToList();

                        CreateArchive(currentArchiveFiles, productGuid, documentCategoryGuid, classification);
                    }
                }
            }
            catch (Exception e)
            {
                Sitecore.Diagnostics.Log.Error
                    ("ProductDownloadsArchiveCreator :: ProcessCurrentProductDocuments :: Exception " +
                     $"{e.StackTrace} for Product '{productGuid}'", this);
            }
        }

        private void CreateArchive
            (List currentArchiveFiles, Guid productGuid, Guid documentCategoryGuid, string classification)
        {
            using (MemoryStream memoryStream = new MemoryStream())
            {
                using (ZipArchive archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
                {
                    foreach (DocumentResultItem currentArchiveFile in currentArchiveFiles)
                    {
                        Item mediaItem = SitecoreHelper.ItemMethods.GetItemFromGUIDInMaster(currentArchiveFile.ItemId.ToString());
                        if (mediaItem != null)
                        {
                            Media media = MediaManager.GetMedia(mediaItem);

                            Stream stream = media?.GetStream()?.Stream;

                            if (stream != null)
                            {
                                ZipArchiveEntry currentFile = archive
                                    .CreateEntry(mediaItem.Name + "." +
                                                 SitecoreHelper.ItemRenderMethods.GetRawValueByFieldName(
                                                     "Extension",
                                                     mediaItem, false));

                                using (Stream entryStream = currentFile.Open())
                                {
                                    stream.CopyTo(entryStream);
                                    entryStream.Flush();
                                }
                            }
                        }
                    }
                }

                if (memoryStream.Length == 0)
                {
                    Sitecore.Diagnostics.Log.Error
                        ($"ProductDownloadsArchiveCreator :: CreateArchive :: Steam empty for Product '{productGuid}" +
                         $"', DocumentCategory '{documentCategoryGuid}', Classification '{classification}'", this);
                }

                memoryStream.Seek(0, SeekOrigin.Begin);
                SaveToSitecoreMediaLibrary(memoryStream, currentArchiveFiles, productGuid, documentCategoryGuid, classification);
            }
        }

        private void SaveToSitecoreMediaLibrary
            (MemoryStream memoryStream, List currentArchiveFiles,
            Guid productGuid, Guid documentCategoryGuid, string classification)
        {
            Item productItem = SitecoreHelper.ItemMethods.GetItemFromGUIDInMaster(productGuid.ToString());
            Item documentCategoryItem = SitecoreHelper.ItemMethods.GetItemFromGUIDInMaster(documentCategoryGuid.ToString());
            string cleanProductCode = productItem.Fields["Product Code"].Value.ToAlphanumeric();

            string fileTitle = cleanProductCode.ToLower().Trim() + "_" + documentCategoryItem.Name.ToAlphanumeric()
                + (string.IsNullOrWhiteSpace(classification) ? "" : "_" + classification.ToAlphanumeric());
            Item productDownloadsArchiveFolder = SitecoreHelper.ItemMethods.GetItemFromGUIDInMaster(Constants.ItemGuids.ProductDownloadsArchiveFolder);

            MediaCreatorOptions options = new MediaCreatorOptions
            {
                FileBased = false,
                IncludeExtensionInItemName = false,
                OverwriteExisting = true,
                Versioned = false,
                Destination = productDownloadsArchiveFolder.Paths.FullPath + "/"
                                  + cleanProductCode.ToUpper()[0] + "/"
                                  + cleanProductCode.ToLower() + "/"
                                  + fileTitle,
                Database = Sitecore.Configuration.Factory.GetDatabase("master")
            };
            Item mediaItem = MediaManager.Creator.CreateFromStream(memoryStream, fileTitle, options);

            Sitecore.Diagnostics.Log.Info("ProductDownloadsArchiveCreator :: Archive Created - " + mediaItem.Paths.FullPath, this);

            mediaItem.Editing.BeginEdit();
            mediaItem["Document Classification"] = string.IsNullOrWhiteSpace(classification) ? "none" : classification;
            mediaItem["Description"] = productGuid.ToString("N") + "|" + documentCategoryGuid.ToString("N");
            string archiveFiles = "";
            foreach (DocumentResultItem file in currentArchiveFiles)
            {
                archiveFiles += file.ItemId + "|";
            }
            mediaItem["Keywords"] = archiveFiles.Trim('|');
            mediaItem["Extension"] = "zip";
            mediaItem.Editing.EndEdit();
        }

        private void PublishArchiveFolder()
        {
            Language language = Sitecore.Context.Language;
            DateTime publishedTime = DateTime.Now;

            Item archivesFolder = SitecoreHelper.ItemMethods.GetItemFromGUIDInMaster(Constants.ItemGuids.ProductDownloadsArchiveFolder);

            ItemList publishingTargets = PublishManager.GetPublishingTargets(archivesFolder.Database);

            foreach (Item publishingTarget in publishingTargets)
            {
                string targetDatabaseName = publishingTarget["Target database"];
                if (string.IsNullOrEmpty(targetDatabaseName))
                    continue;

                Sitecore.Diagnostics.Log.Info("ProductDownloadsArchiveCreator :: Publishing to " + targetDatabaseName, this);

                Database targetDatabase = Sitecore.Configuration.Factory.GetDatabase(targetDatabaseName);
                if (targetDatabase == null)
                    continue;

                PublishOptions publishOptions = new PublishOptions
                    (archivesFolder.Database, targetDatabase, PublishMode.Smart, language, publishedTime);
                Publisher publisher = new Publisher(publishOptions);
                publisher.Options.RootItem = archivesFolder;
                publisher.Options.Deep = true;
                publisher.Publish();
            }
        }

        #endregion
    }

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.

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!

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!

&lt;!--   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/        --&gt;
&lt;setting name="Speak.Html.RequireJsCustomHandler" value="/-/speak/v1/" patch:source="Sitecore.Speak.config"/&gt;

2017-03-31_153809.png

Hope this helps!

Sitecore rocks add component error

A quick post here, to troubleshoot an error I faced while trying to add a new speak component to my project using Sitecore Rocks.

While trying to add the component, I kept seeing this issue.
“Count not load file or assembly ‘Sitecore.Rocks, Version=2.0.0.0, Culture=neytral, PublicKeyToken=a3d60f56f71b90’ or one of its dependencies. The system cannot find the file specified.”

Even though my Sitecore Rocks extension was updated only today, the error persisted.
For some reason however, uninstalling the extension and reinstalling the same and then restarting Visual Studio resolved the issue for me.
Hope this helps!

Any additional information on the root cause of this issue is welcome.