Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
Experience Sitecore! | All posts tagged 'SXA'

Experience Sitecore!

Martin Miles on Sitecore

Implementing blogs index page with filters and paging: SXA walkthrough

Objective.

Initially I've had a template called Blog, and several pages of it which are actual blog posts. Now I have created a page called Blog Index, implementing template, partial and page designs of the same name. The obvious purpose of given page is to show the list of blog posts, being able to filter this out by certain criteria, including a new custom one - series. Content authors want to group these blogs posts into series, so that's a grouping by a logical criteria. They also want to to display the most recent higher, but give readers an option to select an order, as well as page size.


Implementation plan

  1. Switch to SXA "Live mode" (optional)
  2. Create taxonomy categories
  3. Create Series interface template
  4. Use interface template and update blog posts
  5. Create search scope for page template
  6. Create computed field
  7. Publish, redeploy and re-build indexes
  8. Create facets
  9. Create filter datasources
  10. Make rendering variant for search filters
  11. Make rendering variant for search results
  12. Place component to partial design
  13. Configuring Search Results component
  14. Enjoy result!


IMPLEMENTATION

1. Before start, switch web to master, this can be done at /sitecore/content/Tenant/Platform/Settings/Site Grouping/Platform at Database field by setting it to master (dont't forget to publish that particulat item however). Once done, it will use not only master database at published site, but also master indexes.


2. Firstly, let's create Series taxonomy folder under Taxonomy (/sitecore/content/Tenant/Platform/Data/Taxonomy) and populate it with actual series-categories that will be used for filtering:


3. Now I can create interface template to implement series selection. This template will be later used with not just Blogs but also few other page types, that's why I make it an interface and put into shared - /sitecore/templates/Project/Tenant/Platform/Interfaces/Shared/_Series.

Make sure the Source column has correctly set datasource, so that you later will be able to pick up right category under site's Data/Taxonomy/Series folder, as on example below:

Datasource=query:$site/*[@@name='Data']/Taxonomy/Series&IncludeTemplatesForDisplay=Taxonomy folder,Category&IncludeTemplatesForSelection=Category


4. Once done, add _Series interface template to actual page template (Blog in my case). Then one can go to existing blog posts and assign them into series (best with Sitecore PowerShell):

$rootItem = Get-Item master:/sitecore/content/Tenant/Platform;
$sourceTemplate = Get-Item "/sitecore/templates/Project/Tenant/Platform/Pages/Blog";  
$selectedSeries = "{1072C536-0EC2-4EAB-8D98-DC9BF441F30A}";

Get-ChildItem $rootItem.FullPath -Recurse | Where-Object { $_.TemplateName -eq $sourceTemplate.Name } | ForEach-Object {  
        $_.Editing.BeginEdit()
        $_.Fields["Series"].Value = $selectedSeries;
$_.Editing.EndEdit() }

Now selecting an item displays which series it belongs to:


5. Create scope under /sitecore/content/Tenant/Platform/Settings/Scopes, call it Blogs and set its field Scope Query to filter out by Blog template ID:


6. Create computed field contentseries at you project to store actual name of Series into index that's in addition to another field in index called series so that automatically indexed by template and stores GUID for series. This is how I implemented it in Platform.Website.ContentSearch.config:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <contentSearch>
      <indexConfigurations>
        <defaultSolrIndexConfiguration>
          <fieldMap>
            <fieldNames>
              <field fieldName="contentseries" returnType="stringCollection" patch:after="*[0]" />
            </fieldNames>
          </fieldMap>
          <documentOptions>
            <fields hint="raw:AddComputedIndexField">
              <field fieldId="{ID-of-Series-field-within-_Series_template}" fieldName="contentseries" returnType="stringCollection" patch:after="*[0]">
                Tenant.Site.Website.ComputedFields.CategoriesField,Tenant.Site.Website
              </field>
            </fields>
          </documentOptions>          
        </defaultSolrIndexConfiguration>
      </indexConfigurations>
    </contentSearch>
  </sitecore>
</configuration>


7. Publish this configuration into webfolder, then clean up (you can run PS script below) and rebuild indexes.

stop-service solrServiceName
get-childitem -path c:\PathTo\Solr\server\solr\Platform*\Data -recurse | remove-item -force -recurse
start-service solrServiceName
iisreset /stop
iisreset /start


8. When computed field comes into index, the next step sould be to create Series facets. Please note that facet name should be different from the field name as the one got by template and then facet overwrites it

  • For Series - under /sitecore/content/Tenant/Platform/Settings/Facets. The most important field here is Field Name, the value would be contentseries that matched name of field we've created at the previous step
  • Also create one for Published date that relies on already existing published_date_tdt field, which is a custom date field presenting in all of my content page templates.


9. Create new datasource items:

  • checklist filter called Series under /sitecore/content/Tenant/Platform/Data/Search/Checklist Filter folder and assign its first field to point Series facet created at previous step
  • an item for Publication Date under /sitecore/content/Tenant/Platform/Data/Search/Date Filter/Publication Date.


10. Implement Search Filter rendering variant that will contain actual filters. I create that under my custom component Search Content, make two columns and also component variant field into each of them. Assign Filter (Checklist) into first and Filter (Date) into second. Reference datasource items from previous step for each component correspondingly:


11. Implement Search Result rendering variant that will define presentation for each item shown/found:

Noticed Series reference field? That switches context to the item references by Series field, so that I can get a value of actual category under Taxonomy folder.


12. In partial design for Blog Index, drop the following renderings into the canvas: Search content, Sort Results, Page Size and Search Results.


13. Finally, for Search Results component, go to Edit component properties, under SearchCriteria section assign Search results signature to search-results and also select Search scope to match Blogs.

The result:

Uploading scheduled auto-backups of editors content for your Helix / SXA website into Azure Blob storage

INTRODUCTION

I assume you are following good practices and develop Helix-powered websites (or SXA, that follows Helix by definition). In that case, you do separate actual user-generated content items from definition items, normally shipped along with each release and created by developers (if not, then please refer to an article I've previously written on that), so you end up having a Unicorn configuration that stores all of your author-edited content:

Will are going to split the process into 3 major steps

  1. Re-serialize Unicorn configuration for content items
  2. Archive serialized content
  3. Upload an archive into Azure Blob Storage

IMPLEMENTATION

1. Re-serialize Unicorn configuration for content items. Luckily, Unicorn provides us with MicroCHAP.dll library helping to automate the sync process as a part of your deployment process (note that the DLL and corresponding PowerShell module Unicorn.psm1  should be referenced from your code). The good news is that any verb can be passed to it, not just sync, so one can use 'Reserialize'. That call will look like:

$ErrorActionPreference = 'Stop'
$ScriptPath = Split-Path $MyInvocation.MyCommand.Path

Import-Module $ScriptPath\Unicorn.psm1
Sync-Unicorn -Verb 'Reserialize' -Configurations @('Platform.Website.Content') -ControlPanelUrl 'https://platform.dev.local/unicorn.aspx' -SharedSecret '$ecReT!'

2. Archiving serialized content would be the next step. If you click Show Config button for Platform.Website.Content configuration in Unicorn configuration page, you may find all the relevant information about it, including the physical folder where items are serialized. We need this folder to be archived. The step comes as:

$resource = "Content_$(get-date -f yyyy.MM.dd)"
$archiveFile = "d:\$resource.7z"
$contentFolder = "C:\inetpub\wwwroot\Platform.dev.local\App_Data\serialization\Project\serialization\Content"

7z a -t7z -m0=lzma -mx=9 -mfb=64 -md=32m -ms=on $archiveFile $contentFolder
I am using 7zip as an archiver, as my content is slightly more than 4GBs and traditional zip cannot handle that. As a hidden bonus, I am getting the best compression ratio coming with 7zip. Also, It would be worth checking if a file with such name exists at target and deleting it before archiving, especially if you run the script more often than daily.

3. Uploading to Azure Blob Storage concludes given routine. To make this happen you should have a subscription, and ideally a connection string to your Storage account. Then you may use the following code to achieve the result:
$containerName = "qa-serialization"
$ctx = New-AzureStorageContext -ConnectionString "DefaultEndpointsProtocol=https;AccountName=lab1234;AccountKey=y9CFAKE6PQYYf/vVDSFAKEzxOgl/RFv03PwAgcj8K80mSfQFDojdnKfakeaLMva0S9DbrQTzNjDMdGCp7rseRw==;EndpointSuffix=core.windows.net"

Set-AzureStorageBlobContent -File $archiveFile -Container $containerName -Blob $resource -Force -Context $ctx

Also, it is assumed you already have blob container created, if not you need to create it upfront:
New-AzureStorageContainer -Name $containerName -Context $ctx -Permission blob
Optionally, you may want to delete the temporal archive, once it's uploaded to blob storage.


RUNNING

Here's an entire code that works for me:
# Step 1: re-serialize user-generated content
$ErrorActionPreference = 'Stop'
$ScriptPath = Split-Path $MyInvocation.MyCommand.Path

Import-Module $ScriptPath\Unicorn.psm1
Sync-Unicorn -ControlPanelUrl 'https://platform.dev.local/unicorn.aspx' -Configurations @('Platform.Website.Content') -Verb 'Reserialize' -SharedSecret '$ecReT!'
# Step 2: archive serialized user-generated content with 7zip using best compression $resource = "Content_$(get-date -f yyyy.MM.dd)" $archiveFile = "d:\$resource.7z" $contentFolder = "C:\inetpub\wwwroot\Platform.dev.local\App_Data\serialization\Project\serialization\Content" 7z a -t7z -m0=lzma -mx=9 -mfb=64 -md=32m -ms=on $archiveFile $contentFolder # Step 3: upload generated content into Azure Blob Storage $containerName = "qa-serialization" $ctx = New-AzureStorageContext -ConnectionString "DefaultEndpointsProtocol=https;AccountName=lab1234;AccountKey=y9CFAKE6PQYYf/vVDSFAKEzxOgl/RFv03PwAgcj8K80mSfQFDojdnKfakeaLMva0S9DbrQTzNjDMdGCp7rseRw==;EndpointSuffix=core.windows.net"
Set-AzureStorageBlobContent -File $archiveFile -Container $containerName -Blob $resource -Force -Context $ctx #Step 4: clean-up after yourself Remove-Item $archiveFile -Force
I run it on a daily basis by Windows Task Scheduler in order to get a daily snapshot of editors' activity. The script produces the following output:


As a result of running script, I get an archive appearing in the Azure Blob Storage:



RESTORING

There's no sense in making backups unless you confirm restoring the data out of it works well. For content items, download an archive, restore and substitute content serialization folder with what you've extracted, then sync content configuration. As simple as that!

Note that your content should be aligned with the definition items, or it may not work well!

Hope this post helps!

Yet another SXA rendering variant - Script Reference Tag coming to improve your SEO

Note! The code used in this post can be cloned from GitHib repository: SXA.Foundation.Variants

I previously wrote a post about having a rendering variant holding an inline JavaScript one might need along adding some basic JS functionality into your components. 

This is useful when you're early developing your pages and have no possibility or capacity of recompiling entire frontend and updating Creative Exchange package into your solution because of adding/changing few lines; however, given approach is not SEO-friendly as search engines penalize sites for excessive inline scripts and styles. So use it considering to be technical debt, that should be addressed prior to going to production.

The very minimal change one can do is to replace the inline script with a reference to that same script stored in Media Library - same that SXA does itself with themes. This blog post below кумуфдыthat approach:

Firstly, create a template:

Then reference given template IDs within <code>Constamts.cs</code> file:

using Sitecore.Data;

namespace Platform.Foundation.Variants.Pipelines.VariantFields.ScriptReferenceTag
{
    public static partial class Constants
    {
        public static partial class RenderingVariants
        {
            public static partial class Templates
            {
                public static ID ScriptReferenceTag { get; } = new ID("{0EC036D7-384D-4CF6-AD1F-FE949E96126A}");
            }

            public static partial class Fields
            {
                public static class ScriptReferenceTag
                {
                    public static ID ScriptMedia { get; } = new ID("{F1497AF9-7DD3-4B38-BE22-5F092007F929}");
                }
            }
        }
    }
}
Model class, having just one property that stores a GUID of a referenced script from Media Library 
using Sitecore.Data.Items;
using Sitecore.XA.Foundation.RenderingVariants.Fields;

namespace Platform.Foundation.Variants.Pipelines.VariantFields.ScriptReferenceTag
{
    public class VariantScriptReferenceTag : RenderingVariantFieldBase
    {
        public string ScriptMedia { get; set; }

        public VariantScriptReferenceTag(Item variantItem) : base(variantItem)
        {
        }
    }
}
Parser:
using Sitecore.Data;
using Sitecore.XA.Foundation.Variants.Abstractions.Pipelines.ParseVariantFields;

namespace Platform.Foundation.Variants.Pipelines.VariantFields.ScriptReferenceTag
{
    public class ParseScriptReferenceTag : ParseVariantFieldProcessor
    {
        public override ID SupportedTemplateId =>  Constants.RenderingVariants.Templates.ScriptReferenceTag;
        
        public override void TranslateField(ParseVariantFieldArgs args)
        {
            ParseVariantFieldArgs variantFieldArgs = args;

            var variantHtmlTag = new VariantScriptReferenceTag(args.VariantItem) { Tag = "script" };
            variantHtmlTag.ScriptMedia = args.VariantItem[Constants.RenderingVariants.Fields.ScriptReferenceTag.ScriptMedia];
            variantFieldArgs.TranslatedField = variantHtmlTag;
        }
    }
}
Renderer:
using System;
using Sitecore.Data;
using System.Web.UI.HtmlControls;
using Sitecore.XA.Foundation.RenderingVariants.Pipelines.RenderVariantField;
using Sitecore.XA.Foundation.Variants.Abstractions.Pipelines.RenderVariantField;
using Sitecore.Resources.Media;

namespace Platform.Foundation.Variants.Pipelines.VariantFields.ScriptReferenceTag
{
    public class RenderScriptReferenceTag : RenderVariantField
    {
        public override Type SupportedType => typeof(VariantScriptReferenceTag);

        public override void RenderField(RenderVariantFieldArgs args)
        {
            var variantField = args.VariantField as VariantScriptReferenceTag;
            if (variantField != null)
            {
                var id = variantField?.ScriptMedia;
                if (string.IsNullOrWhiteSpace(id))
                {
                    return;
                }

                var scriptItem = Context.Database.GetItem(new ID(id));
                if(scriptItem == null)
                {
                    return;
                }

                var url = MediaManager.GetMediaUrl(scriptItem);

                var tag = new HtmlGenericControl(variantField.Tag);
                tag.Attributes.Add("type", "text/javascript");
                tag.Attributes.Add("defer", String.Empty);
                tag.Attributes.Add("src", url);

                args.ResultControl = tag;
                args.Result = RenderControl(args.ResultControl);
            }
        }
    }
}

Example of usage:

This rendering variant field generates the following output:

<script src="/-/media/Project/Platform/Other/Scripts/Header-script.js" type="text/javascript" defer="" ></script>


This approach works perfectly well. But once again for a second, have you ever considered moving such scripts into a Theme along with related component (if any) instead of leaving it like that? Hope this helps!

Walktrough: creating a footer for SXA website implementing a precisely demanded front end markup

Note! This is a second walkthrough explaining an implementation of real-life scenario with Sitecore SXA.
It reveals best practices and several powerful techniques, such as:

  • structuring data for complex components to be both easy to maintain and editors-friendly
  • referencing other renderings using Component field, setting datasource and rendering variant
  • reusing existing built components and their templates by Clone rendering PowerShell script
  • nesting rendering variants and looping through them
  • using Query Variant field for accessing child items
  • restricting rendering variants by certain page templates

When starting working with SXA I faced some lack of good guidance and walkthrough (with an exception of excellent series by Adam Najmanowicz). Today I am going to eliminate this gap by adding my own walkthrough of implementing a footer, what could be simpler, I thought. After reading some official tutorials I expected it to be an exercise of dropping structure components (ie. splitters with rows and columns) and assigning link lists into them so that it could be later styled by front-end team. Wrong! Things appeared to be not as easy.

To start with, my strict front-end team came to me with quite a precise requirement for a footer, and below is they demand from me.

Requirements

They send me an image with all the blocks assigned:


It was accompanied by the HTML output expected for me to achieve:


Minimal and effective, nice job, front-enders! Now the ball is on my side in order to get all that implemented.

Implementation

1. As per Sitecore SXA recommendations, I create Footer partial design and open it in Experience Editor for editing. Footer component will be created for that partial design, which itself will be used to construct a resulting page.

2. Not to mess with any OOB components, I create Footer rendering by cloning one of the existing renderings with datasource, so that I ensured a copy of datasource template and folder created. Also make sure this rendering stays outside of Experience Accelerator folder, but on feature layer where serialization enabled.

3. Assign new rendering to Available Renderings (/sitecore/content/Tenant/Site/Presentation/Available Renderings/Module) so that it appears in my custom components sectionand also in Toolbox

4. Now it is a good time to adjust a Template for Footer,it was created automatically by cloning, but this is how I defined it:


What is important to explain here - Elements field is pointing to Footer Elements Folder. At a first glance, that folder contains just a collection of link lists. But that's not right! There are at least 3 types of footer blocks: first four blocks are indeed Link List blocks,however,followed by Rich Text blocks, and finally, there is Social Presence block and all three can be added intoFooter Elements Folder. These three types are defined along with Footer template, you may see on the screenshots below.

5. Elements templates. This is how I defined them, Link ist footer element:

Rich text footer element:

Social presence footer element:


6. Insert Options to be configured for Footer folder to accept both Footer items and Footer Elements Folder. Configure Footer Elements Folder to accept these 3 types. Now one can insert the data. Once done, that will be how the data folder looks like:


First 4 items (About Us, Hot topics, Other sites, Help and Support) reference corresponding Link Lists as defined as below:


Next two items point to reusable Rich Text items under /Data folder. And the last one is a reference to a Social Presence I have implemented previously.

After items of all three types are created, we can assign them into footer datasource item itself:



7. Rendering variants. In the previous steps, we have defined rendering, templates, folders and actual data. Now it is time to make it all together work to produce the output by creating rendering variants. This is the most tricky part of the current walkthrough


Default is the main and only rendering variant to be called or footer. Other three variants are "service" variants and designed to be called from Defaultinternally in a loop, being assigned to a component rendering field with personalisation applied (see image above).

Here's how these three other variants look like:


Link list footer element switches to a referenced Link List item and uses Query variant field to iterate its children.

Rich Text footer element simply references to a Rich Text (reusable) item under /Data folder in the same manner.

As for Social presence footer element rendering variant, it defines a component that references Social buttons rendering with Social presence rendering variant, that I have described in one of my previous posts.

Lastly, footer-info__copyright field renders copyright lines at the very bottom.

8. In order to avoid confusion for your editors, it makes sense to restrict rendering variant by selecting Allowed in templates field leaving only Default variant since other three variants are internally called from Default using Component variant field. 

9. Apply new rendering to a Partial Design it in Experience Editor. Do not forget to configure footer placeholder to accept only Footer component by creating a placeholder setting,

Result

Save the page and enjoy the result:


No single line of back-end code!

How to add id and data-attributes to a Rendering Variant in SXA?

When dealing with a rendering variant field, it is not a big deal to set few data-attributes to it - those inputs are located at the very bottom of Variant Details section. You can do it like that:


But what if you need data attributes to the top level of component, which it Rendering Variant item itself? There isn't such an option!

Requirements are

  1. An id attribute (ie. section-1, section-2, ... section-N)
  2. One or many data-attributes (ie. " User-friendly title", "Another user-friendly title", etc.)
  3. CSS class section-with-anchor on those instances, which have both previous requirements implemented
All of the above should be set for the top node of a rendering - outside of the control of Rendering Variant. Thinking logically - if we ever could add the above to Rendering Variant item itself, then it would present on every single instance of that given rendering variant. We do have CSS-class field on Rendering Variant item, but as I said, we need this class to present only occasionally for some individual instances as per requirement so we cannot use that field.

Solution

That is where Rendering Parameters come into a play, as they apply per each individual rendering usage. Let's take a look!

1. ID of a component. That was the easiest as luckily default rendering parameters do support field for that:


2. Data-attributes do not exist in Rendering Parameters control, unlike id attribute. But since that is just a collection of Key-Value pairs, why not to convert them into a set of data-attributes on a component node. Not all, of them, of course, but those that start with data- as on an image below:


In order to pick them up and assign to a rendering view, I write a simple extension method:
public static MvcHtmlString RenderAllDataAttributes(this HtmlHelper helper)
{
    var rendering = Sitecore.Mvc.Presentation.RenderingContext.Current.Rendering;

    string additionalAttributes = String.Empty;
    if (rendering?.Parameters != null)
    { 
        foreach (KeyValuePair<string, string> parameter in rendering.Parameters)
        {
            if (parameter.Key.ToLower().StartsWith("data-"))
            {
                additionalAttributes += $"{parameter.Key}=\'{parameter.Value}\' ";
            }
        }
    }

    return new MvcHtmlString(additionalAttributes.Trim());
}
It can be called like that:
<div @Html.RenderAllDataAttributes() @Html.Sxa().Component(Model.Rendering.RenderingCssClass ?? "default-class", Model.Attributes)>
    <div class="component-content">
        ...
    </div>
</div>


3 Setting a style class. As mentioned before, do not misuse Css Class field of rendering variant definition for styling a specific implementation of rendering variant. Style that are applied individually for each instance of rendering (regardless of variant selected) can be found at that same Rendering Parameter window, located at Styling section.


Of course, you may need to create this style beforehand, if not yet done. To do so, create a new Style item underneath Styles grouping item and restrict to renderings where give style can be shown. That is a part of your style them and is located under /sitecore/content/Tenant/Site/Presentation/Styles node:

Result

Finally, I got it all rendered as expected:


That blog post shows how useful Rendering Parameters are, hope you find it helpful!

Script rendering variant field in SXA - why would one need it?

Note! The code used in this post can be cloned from GitHib repository: SXA.Foundation.Variants

Yet another rendering variant field came to my to-do list for implementation - Script rendering variant field. Why would I need one at all? 

I came across two uses cases where implementing this field type made my job done and that's just recently. Some developers have (reasonable) biases against having javascript code inline instead of referencing JS-file at the bottom of a page, but keep in mind that given field comes up as a part of a component dynamically added to a page so there isn't a big choice on how to extend running website with some additional client-side functionality. I am presenting both cases below, let's take a look at them. Would you know the better way of achieving these goals - please let me know via Slack or Twitter. Also, the code of Script Variant Field is at the very bottom of the page.


Use case 1: implementing in-page navigation panel

As usual, I got a precise requirement from my strict front-end team to implement such a piece of code as a component. It has a UL-tag that will keep a link list to other components of this same page (prefixed with #) created client-side dynamically, and a script that does the actual job. When rendering the in-page navigation component by the backend, we're now aware of other components and their attributes, so the walk-around was handling page loaded event and identifying all the components that have IDs set and class section-with-anchor.

<div class="component content col-12 content-section">
    <div class="component-content">
        <div class="anchor-panel">
            <ul class="anchor-panel__list"></ul>
            <script defer>
                document.addEventListener('DOMContentLoaded', function(){
                    if (!$('.section-with-anchor').length || !$('.section-with-anchor').length) return;
                    $('.section-with-anchor').each(function(index, el) {
                        var anchor = '#' + $(el).attr('id');
                        var text = $(el).attr('data-text');
                        var $anchorList = $('.anchor-panel__list');
                        var $anchorItem = $('<li class="anchor-panel__item"></li>');
                        var $anchorLink = $('<a href=""></a>')
                        $anchorItem.append($anchorLink.text(text).attr('href', anchor));
                        $anchorList.append($anchorItem);
                    });
                });
            </script>
        </div>
    </div>
</div>

Quite obviously, my rendering variant will contain 2 fields: a UL-tag section field for in-page navigation that takes the links dynamically and script variant field, containing the client-side logic. You can test this script in action by this link. That is how it looks in Content Editor:


Since I have 5 other sections qualifying script requirements - they all have been identified by the script and added to in-page navigation panel. Here's how the result looks like on a styled page for me:


Once again, I decided to implement a new script variant reference field because the component is subject to some seldom minor JavaScript changes, but should be configurable. Also, at given use case it can be dropped only once into a page, so there's no problem with multiple instances of the same script for me, but using it you might need to check if that applies for your scenarios. But even with that in mind, I'd probably not bothered creating yet another new rendering variant field, if not few more potential usages I have in backlog.


Use case 2: accessing client side URL hash-key parameter and presenting it on a page

Recently I implemented a URL query string parameter variant field, where one can set any parameter and the field present its URL-decoded value on a page in any given tag and style. A colleague of mine who develops search results page with SXA asked if that's doable to extract a hash-key URL parameter and show it on a page along with other content, however that a fully client-side parameter that never got posted to the server.

I decided to give a try and managed to confirm with a small proof of concept. I quickly wrote this script (so please be forgiving for it being just a quick PoC and me not being the proper FED).

window.addEventListener("hashchange", function () {
    var h1 = document.getElementsByClassName("updatedHashValue");
    if (h1.length > 0) {
        h1[0].innerHTML = getHashValue("param");
    }
    function getHashValue(parameter) {
        var hashValues = window.location.hash.substr(1);
        var result = hashValues.split('&').reduce(function (result, item) {
                var parts = item.split('=');
                result[parts[0]] = parts[1];
                return result;
        }, {});
        return result[parameter];
    };
});

That's how it looks implemented as the part of rendering variant. It creates an H1 element to store the value, extracted out of hash parameters stored at URL bar and updated without any postback to the server, and of source script variant field:


Testing. After adding a component to a page, saving and selecting the above rendering variant, I got the page reloaded as anticipated with no visual changes. Then I open a console from browser dev.tools and enter:

window.location.hash = "param=Successful!"

From dev.tools that was easy to confirm that there was no postback done to the server, however, browser navigation bar predictably changed, appending new hash parameters pair: 

And guess what? H1 element immediately got that value displayed. Love that magic!


The code is very simple, similar to other variant fields I've blogged previously. Create the model and implement property for a field:

public class VariantScript : VariantField
{
    public string Script { get; set; }
}

Reference ID of that field from a template and ID of a template itself:

public static class Constants
{
    public static class RenderingVariants
    {
        public static class Templates
        {
            public static ID Script = new ID("...");
        }
        public static class Fields
        {
            public static class Script
            {
                public static ID ScriptField { get; } = new ID("...");
            }
        }
    }
}

Here's a parser

public class ParseScript : ParseField
{
    public override ID SupportedTemplateId => Constants.RenderingVariants.Templates.Script;

    public override void TranslateField(ParseVariantFieldArgs args)
    {
        ParseVariantFieldArgs variantFieldArgs = args;

        variantFieldArgs.TranslatedField = new VariantScript
        {
            Script = args.VariantItem[Constants.RenderingVariants.Fields.Script.ScriptField]
        };
    }
}

and a renderer:

public class RenderScript : RenderVariantField
{
    public override Type SupportedType => typeof(VariantScript);

    public override RendererMode RendererMode => RendererMode.Html;

    public override void RenderField(RenderVariantFieldArgs args)
    {
        var variantField = args.VariantField as VariantScript;
        if (variantField != null)
        {
            args.ResultControl = RenderScriptField(variantField, args);
            args.Result = RenderControl(args.ResultControl);
        }
    }

    protected virtual Control RenderScriptField(VariantScript variantScript, RenderVariantFieldArgs args)
    {
        if (!string.IsNullOrWhiteSpace(variantScript.Script))
        {
            var tag = new HtmlGenericControl("script") { InnerHtml = variantScript.Script };
            tag.Attributes.Add("defer", String.Empty);
            return tag;
        }

        return new LiteralControl();
    }
}

New Script Rendering Variant field, that enhances your tooling for implementing more modern-looking websites. And as usual - please use it responsibly!

Welcome Item Reference - a rendering variant field missing out of the box in SXA

Note! The code used in this post can be cloned from GitHib repository: SXA.Foundation.Variants
The majority of folks working with SXA are aware of Reference Item variant field - that allows switching a context of rendering variant from a context item (current page or datasource item, if set) to either one or many items referenced by a link-type field of a given context item. It works like a charm, but in some cases one may meet a case where setting datasource is not applicable or you may need your component to show something different apart from provided datasource item - I have already described how to implement that using Query Variant Field. Let's evaluate it:
Pros
  • it comes out of the box, and could be used straight away
  • you may query in more complex way rather just proving an ID
Cons
  • it relies on SXA search index to be in actual state - you need to have it rebuilt
  • writing query is less user friendly then just picking up an item.
So why not to implement a dedicated Item Reference variant field - it definitely pays off, once used often.

This time I picked up built-in Reference variant field as a donor. Instead of PathTrough field I added Item field of Droptree type:



Code-wise, I implemented one property named PassThroughItem. There's also another one to store child fields, nested underneath given reference item field, those will be executed and render in a switched context:
public class VariantItemReference : BaseVariantField
{
    public string PassThroughItem { get; set; }

    public IEnumerable<BaseVariantField> NestedFields { get; set; }
}
Need to reference IDs of template and its single field:
public static class Constants
{
    public static class RenderingVariants
    {
        public static class Templates
        {
            public static ID ItemReference = new ID("...");
        }

        public static class Fields
        {
            public static class ItemReference
            {
                public static ID Item { get; } = new ID("...");
            }
        }
    }
}
Implement a parser:
public class ParseItemReference : ParseVariantFieldProcessor
{
    public override ID SupportedTemplateId => Constants.RenderingVariants.Templates.ItemReference;

    public override void TranslateField(ParseVariantFieldArgs args)
    {
        ParseVariantFieldArgs variantFieldArgs = args;

        var reference = new VariantItemReference();
        reference.ItemName = args.VariantItem.Name;
        reference.PassThroughItem = args.VariantItem[Constants.RenderingVariants.Fields.ItemReference.Item];

        reference.NestedFields = args.VariantItem.Children.Count > 0
            ? ((IVariantFieldParser)ServiceLocator.ServiceProvider.GetService(typeof(IVariantFieldParser))).ParseVariantFields(args.VariantItem, args.VariantRootItem, false)
            : new List<BaseVariantField>();

        variantFieldArgs.TranslatedField = reference;
    }
}
And a renderer:
public class RenderItemReference : RenderVariantFieldProcessor
{
    public override Type SupportedType => typeof(VariantItemReference);

    public override RendererMode RendererMode => RendererMode.Html;
      
    public override void RenderField(RenderVariantFieldArgs args)
    {
        var control = new PlaceHolder();

        var variantItemReference = args.VariantField as VariantItemReference;
        if (!string.IsNullOrWhiteSpace(variantItemReference?.PassThroughItem))
        {
            var newContextItem = Sitecore.Context.Database.GetItem(new ID(variantItemReference.PassThroughItem));
            if (newContextItem != null)
            {
                foreach (BaseVariantField referencedItem in variantItemReference.NestedFields)
                {
                    RenderVariantFieldArgs variantFieldArgs = new RenderVariantFieldArgs
                    {
                        VariantField = referencedItem,
                        Item = newContextItem,
                        HtmlHelper = args.HtmlHelper,
                        IsControlEditable = args.IsControlEditable,
                        IsFromComposite = args.IsFromComposite,
                        RendererMode = args.RendererMode,
                        Model = args.Model
                    };

                    CorePipeline.Run("renderVariantField", variantFieldArgs);
                    if (variantFieldArgs.ResultControl != null)
                    {
                        control.Controls.Add(variantFieldArgs.ResultControl);
                    }
                }
            }
        }

        args.ResultControl = control;
        args.Result = RenderControl(args.ResultControl);
    }
}
Here is the usage:


Don't' forget to add it into Insert Options and hope you'll enjoy it!

SXA: Implementing URL query parameter rendering variant with little efforts

Note! The code used in this post can be cloned from GitHib repository: SXA.Foundation.Variants

Got a requirement to display a value passed with a URL parameter on a page (URL decoded of course). I want to achieve the goal as quick as possible, with the minimum steps, ideally. So, here we go..

1. Just use existing VariantField (located at /sitecore/templates/Foundation/Experience Accelerator/Rendering Variants/VariantField). When copying, ensure you keep new variant field template outside of Experience Accelerator folder (to avoid it being lost on future SXA upgrade), ideally somewhere within a folder with serialization configured.


2. Remove unwanted fields from a new template. I left only a few I felt necessary, but you may end up having even less. Another thing I've done at this step was adding a user-friendly label to the field, to avoid editors' confusion with a misleading naming:


3. Then you need to create a model. As the simplest, I inherited from VariantField

public class VariantQueryString : VariantField
{
    public VariantQueryString(Item variantItem) : base(variantItem)
    {
    }
}

With a parser, I only process those fields I will be using - what I kept from the previous step: 

public class ParseQueryStringField : ParseField
{
    public override ID SupportedTemplateId => Constants.RenderingVariants.Templates.QueryString;

    public override void TranslateField(ParseVariantFieldArgs args)
    {
        ParseVariantFieldArgs variantFieldArgs = args;

        variantFieldArgs.TranslatedField = new VariantQueryString(args.VariantItem)
        {
            // this property is reused under different purpose rather than named - it stores URL parameter name
            FieldName = args.VariantItem[Constants.RenderingVariants.Fields.QueryField.FieldName],

            Tag = args.VariantItem.Fields[Constants.RenderingVariants.Fields.QueryField.Tag].GetEnumValue(),
            CssClass = args.VariantItem[Constants.RenderingVariants.Fields.QueryField.CssClass],
            Prefix = args.VariantItem[Constants.RenderingVariants.Fields.QueryField.Prefix],
            Suffix = args.VariantItem[Constants.RenderingVariants.Fields.QueryField.Suffix],
            RenderIfEmpty = args.VariantItem[Constants.RenderingVariants.Fields.QueryField.RenderIfEmpty] == "1"
        };
    }
}

Also reference IDs of these fields within Constants class:

public static class Constants
{
    public static class RenderingVariants
    {
        public static class Fields
        {
            public static class QueryField
            {
                public static ID Tag { get; } = new ID("{F556DEDD-5D6B-4FFF-A904-E4C65AB0E698}");
                public static ID CssClass { get; } = new ID("{B3B6B300-1704-493B-999C-AD21CCE58FEF}");
                public static ID FieldName { get; } = new ID("{9D3717EF-CD09-4D98-8C4A-914299503626}");
                public static ID Prefix { get; } = new ID("{5844C4AA-5E48-4721-8E30-91646E430C83}");
                public static ID Suffix { get; } = new ID("{0EFAF48E-E0DD-4408-80D3-4C001101E834}");
                public static ID RenderIfEmpty { get; } = new ID("{E14C3930-FE6C-48AC-99D8-C6867B489066}");
            }
        }
    }
}

4. Finally, implementing RenderQueryStringField class. Nothing complex - just getting a value from the query string, wrapping it with a selected tag/styles and rendering it into the page. Also, there's a fallback scenario for Experience Editor when a given parameter is missing from URL but component needs to be visible.

public class RenderQueryStringField : RenderVariantField
{
    public override Type SupportedType => typeof(VariantQueryString);

    public override RendererMode RendererMode => RendererMode.Html;

    public override void RenderField(RenderVariantFieldArgs args)
    {
        var variantField = args.VariantField as VariantQueryString;
        if (variantField != null)
        {
            args.ResultControl = RenderQueryStringValue(variantField, args);
            args.Result = RenderControl(args.ResultControl);
        }
    }

    protected virtual Control RenderQueryStringValue(VariantField variantField, RenderVariantFieldArgs args)
    {
        var queryString = HttpContext.Current.Request.QueryString;

        if (!string.IsNullOrEmpty(variantField.FieldName) && !string.IsNullOrWhiteSpace(variantField.Tag))
        {
            if (queryString.HasKeys() && queryString[variantField.FieldName] != null)
            {
                var tag = new HtmlGenericControl(variantField.Tag);
                AddClass(tag, (variantField.CssClass + " " + GetFieldCssClass(variantField.FieldName)).Trim());
                tag.InnerText = queryString[variantField.FieldName];
                return tag;
            }

            if (args.IsControlEditable && PageMode.IsExperienceEditorEditing)
            {
                return GetVariantFieldNameLiteral(variantField.FieldName);
            }
        }

        return new LiteralControl();
    }

    protected virtual HtmlGenericControl GetVariantFieldNameLiteral(string parameterName)
    {
        var missingField = new HtmlGenericControl("span");
        missingField.Attributes.Add("class", "missing-field-hint");
        missingField.InnerText = $"[{parameterName}] URL paramenter";
        return missingField;
    }
}

5. I am adding new field variant into existing Foundation project, but if you haven't got one - you might create it. Do not forget to include config patches. something like below:

?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <parseVariantFields>
        <processor type="Platform.Foundation.Variants.Pipelines.VariantFields.QueryString.ParseQueryStringField, Platform.Foundation.Variants" resolve="true" patch:before="processor[@type='Sitecore.XA.Foundation.RenderingVariants.Pipelines.ParseVariantFields.ParseField, Sitecore.XA.Foundation.RenderingVariants']" />
      </parseVariantFields>
      <renderVariantField>
        <processor type="Platform.Foundation.Variants.Pipelines.VariantFields.QueryString.RenderQueryStringField, Platform.Foundation.Variants" resolve="true" patch:before="processor[@type='Sitecore.XA.Foundation.RenderingVariants.Pipelines.RenderVariantField.RenderVariantField, Sitecore.XA.Foundation.RenderingVariants']"  />
      </renderVariantField>
    </pipelines>
  </sitecore>
</configuration>

6. You might consider adding a new variant field into Insert options. Once done, add it as you normally do with variant fields:


Result. That is how it works when passing a URL parameter with a value:


When a parameter is missing from URL, this is how it looks like in Experience Editor, at least making component selectable:


Hope you find this post helpful!

Troubleshooting SXA "Failed to render rendering. Message: An unhandled exception occurred. Source: Sitecore.Mvc"

Got a weird exception, very similar to the one described at StackExchange (SXA Error when open page in Experience Editor). It happened at my CI server, so decided to reproduce locally, deploying on top of a clean instance of Sitecore, and got an issue reproduced.

Here's a call stack:

17264 22:03:56 ERROR Failed to render rendering
Exception: System.Web.HttpUnhandledException
Message: An unhandled exception occurred.
Source: Sitecore.Mvc
   at Sitecore.Mvc.Pipelines.MvcEvents.Exception.ShowAspNetErrorMessage.ShowErrorMessage(ExceptionContext exceptionContext, ExceptionArgs args)
   at Sitecore.Mvc.Pipelines.MvcEvents.Exception.ShowAspNetErrorMessage.Process(ExceptionArgs args)
   at (Object , Object )
   at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
   at Sitecore.Pipelines.DefaultCorePipelineManager.Run(String pipelineName, PipelineArgs args, String pipelineDomain, Boolean failIfNotExists)
   at Sitecore.Pipelines.DefaultCorePipelineManager.Run(String pipelineName, PipelineArgs args, String pipelineDomain)
   at Sitecore.Mvc.Pipelines.PipelineService.RunPipeline[TArgs](String pipelineName, TArgs args)
   at Sitecore.Mvc.Filters.PipelineBasedRequestFilter.OnException(ExceptionContext exceptionContext)
   at System.Web.Mvc.ControllerActionInvoker.InvokeExceptionFilters(ControllerContext controllerContext, IList`1 filters, Exception exception)
   at System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName)
   at System.Web.Mvc.Controller.ExecuteCore()
   at System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext)
   at Sitecore.Mvc.Controllers.ControllerRunner.ExecuteController(Controller controller)
   at Sitecore.Mvc.Controllers.ControllerRunner.Execute(TextWriter writer)
   at Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer.Render(Renderer renderer, TextWriter writer, RenderRenderingArgs args)

Nested Exception

Exception: System.NullReferenceException
Message: Object reference not set to an instance of an object.
Source: Sitecore.XA.Foundation.Grid
   at Sitecore.XA.Foundation.Grid.Commands.ShowGridPropertiesDialog.QueryState(CommandContext context)
   at Sitecore.Shell.Framework.Commands.CommandManager.QueryState(Command command, CommandContext context)
   at Sitecore.Pipelines.GetChromeData.GetChromeDataProcessor.QueryButtonState(WebEditButton button, CommandContext context, String click)
   at Sitecore.Pipelines.GetChromeData.GetChromeDataProcessor.AddButtonToChromeData(WebEditButton button, GetChromeDataArgs args)
   at Sitecore.Pipelines.GetChromeData.GetChromeDataProcessor.AddButtonsToChromeData(IEnumerable`1 buttons, GetChromeDataArgs args)
   at Sitecore.Pipelines.GetChromeData.GetRenderingChromeData.Process(GetChromeDataArgs args)
   at (Object , Object )
   at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
   at Sitecore.Pipelines.DefaultCorePipelineManager.Run(String pipelineName, PipelineArgs args, String pipelineDomain, Boolean failIfNotExists)
   at Sitecore.Pipelines.DefaultCorePipelineManager.Run(String pipelineName, PipelineArgs args, String pipelineDomain)
   at Sitecore.Pipelines.GetChromeData.GetChromeDataPipeline.Run(GetChromeDataArgs args)
   at Sitecore.Mvc.ExperienceEditor.Presentation.RenderingMarker.GetClientData()
   at Sitecore.Mvc.ExperienceEditor.Presentation.RenderingMarker.get_ClientData()
   at Sitecore.Mvc.ExperienceEditor.Presentation.RenderingMarker.GetStart()
   at Sitecore.Mvc.ExperienceEditor.Presentation.Wrapper..ctor(TextWriter writer, IMarker marker)
   at Sitecore.Mvc.ExperienceEditor.Pipelines.Response.RenderRendering.AddWrapper.Process(RenderRenderingArgs args)
   at (Object , Object )
   at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
   at Sitecore.Pipelines.DefaultCorePipelineManager.Run(String pipelineName, PipelineArgs args, String pipelineDomain, Boolean failIfNotExists)
   at Sitecore.Pipelines.DefaultCorePipelineManager.Run(String pipelineName, PipelineArgs args, String pipelineDomain)
   at Sitecore.Mvc.Pipelines.PipelineService.RunPipeline[TArgs](String pipelineName, TArgs args)
   at Sitecore.Mvc.Pipelines.Response.RenderPlaceholder.PerformRendering.Render(String placeholderName, TextWriter writer, RenderPlaceholderArgs args)
   at (Object , Object )
   at Sitecore.Pipelines.CorePipeline.Run(PipelineArgs args)
   at Sitecore.Pipelines.DefaultCorePipelineManager.Run(String pipelineName, PipelineArgs args, String pipelineDomain, Boolean failIfNotExists)
   at Sitecore.Pipelines.DefaultCorePipelineManager.Run(String pipelineName, PipelineArgs args, String pipelineDomain)
   at Sitecore.Mvc.Pipelines.PipelineService.RunPipeline[TArgs](String pipelineName, TArgs args)
   at Sitecore.Mvc.Helpers.SitecoreHelper.RenderPlaceholderCore(String placeholderName, TextWriter writer)
   at Sitecore.Mvc.Helpers.SitecoreHelper.Placeholder(String placeholderName)
   at ASP._Page_Views_Shared_Partial_Design_Dynamic_Placeholder_cshtml.Execute() in c:\inetpub\wwwroot\RssbPlatform.dev.local\Views\Shared\Partial Design Dynamic Placeholder.cshtml:line 5
   at System.Web.WebPages.WebPageBase.ExecutePageHierarchy()
   at System.Web.Mvc.WebViewPage.ExecutePageHierarchy()
   at System.Web.WebPages.WebPageBase.ExecutePageHierarchy(WebPageContext pageContext, TextWriter writer, WebPageRenderingBase startPage)
   at System.Web.Mvc.ViewResultBase.ExecuteResult(ControllerContext context)
   at System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilterRecursive(IList`1 filters, Int32 filterIndex, ResultExecutingContext preContext, ControllerContext controllerContext, ActionResult actionResult)
   at System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilterRecursive(IList`1 filters, Int32 filterIndex, ResultExecutingContext preContext, ControllerContext controllerContext, ActionResult actionResult)
   at System.Web.Mvc.ControllerActionInvoker.InvokeActionResultFilterRecursive(IList`1 filters, Int32 filterIndex, ResultExecutingContext preContext, ControllerContext controllerContext, ActionResult actionResult)
   at System.Web.Mvc.ControllerActionInvoker.InvokeActionResultWithFilters(ControllerContext controllerContext, IList`1 filters, ActionResult actionResult)
   at System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName)

No single reference to my code, or whatsoever prompting to the actual reason.

Troubleshooting this was not an easy walk out. Logs also contained nothing relevant to the issue. Since I always keep at least daily backups done with a helpful tool Sif0n, it was relatively easy to substitute a web root folder from a fully functional solution. but the problem persisted. Then it is likely to be caused by something stored in a database.

I suspected something could be wrong with my serialization, I checked the settings but those were all correct. Then I restored fully functional database along with existing instance, calling it master_OK and started comparing those items one by one. And finally, I found the difference:


Reason for error: Rendering parameters template was missing due not being serialized and synced on that new environment. How could that happen? Don't know for sure, but a very alike suspect is that when using a clone rendering SXA script I accidentally missed out secondary options, so that script cloned rendering parameters for a new rendering, but left it underneath Experience Accelerator folder. Which is kept outside of serialization (as I more or less correctly predicted at the beginning of my investigation). So rendering parameters template was referenced, but did not exist on the target system. 

After fixing it, the site worked well. I do understand that the call stack I included above and the error messages are quite generic and may be met but any other scenarios, so probably Sitecore needs to assert and explicitly log such errors. At least I shared the way of dealing with it and my exact reason with you, hope it may help someone.

What is Query Variant Field to be used with rendering variants and few real life scenarios of using it

Imagine a case - there is a datasource item, that has a Droplink field to pointing to another item, let's say a Rich Text with an address (as on an image below):


That means, in your rendering variant you simply use Reference Variant Field referencing an item behind Rich text droplink, and within that Rich text item you simply render a field, as normal:


Seems obvious, right? Let's increase complexity!


Scenario one

We change scenario so that Droplink would now point not to an item having the field to render, but rather to an item, containing other children you might need to display:


So instead of referencing a Rich text item, we'll reference Link list item, where you need to iterate child items (links) of a referenced Link List item, and show all these links on a page with a rendering variant:


Question: how do I iterate children of my referenced item?

Okay, that's where your new friend comes to help you. Welcome, Query Rendering Variant Field!

What it actually does is also switching a context to a single item / multi items, similarly to Reference Variant Field, but instead of taking switching context from a link-powered field, it uses Sitecore search query and switches context to its execution result. Here's how one can add Query Variant Field into a rendering variant:


So for my simple example with Link List children, I use a query to search all the children of a current item (Link List):


That does the job and withing this query item you may use fields related to items coming out from a search.

One more helpful tip: when working with references, queries or whatsoever changes the context, you would often wondering iа changing context worked well and referencing what you intend to. The trick I use is inserting a debugging template field exposing ID and/or a name of where we've switched to (or any other universal property all the items do have and that are accessible from NVelocity $item):


This takes only 5 seconds of my time to implement but immediately helps to visualise a context on output. Very helpful!


Second scenario

... where Query Field Variant helps me so much. I need to implement a Subscription Level identifier on every page, so that it shows which privilege a current user has (obviously, he/she will see an only single one of the labels show below at one time):


But there is a requirement for that labels itself to be changeable and kept somewhere under site's settings item - subject to alterations at a later stage. So I already have a separate folder with such items:


With using a Reference Variant Field one would need to have rendering having a datasource item with a field pointing to one of those subscription items, which is not an elegant scenario. And what if my rendering does not accept datasources? Or it simply breaks sense, as in a described scenario?

Since we know the IDs of these subscription items we can reference them directly and that's where Query Variant Template also helps us. Also, search query by an ID always returns not more than one item, and since we know item exists we can use it as direct linking. That's how I do that:


Works like a charm!

I have just shown only two real-life examples of using Query Variant Field, but the potential area of their usage is virtually unlimited - as much as you can query you indexes. This gives an ultimate tool for comfy switching context as much as you may need that, but I'd warn you from overusing it too much.