Wednesday, November 18, 2020

Sitecore Symposium 2020

My 2 cents on The Virtual Edition

Note that I'm being optimistic and wrote "The" virtual edition instead of the "First" virtual symposium. Not that this virtual experience was all bad - certainly not. I saw some inspiring presentations, had a few good chats and I even found some benefits as well - no overlap in sessions making it easier to watch them all and... you can easily sneak out of the room if the session is not what you actually expected :)  I didn't get to enlarge my collections though:

This post is not an overview of the big announcements this year. Nor a list of highlights and most remarkable moments. I can safely make the assumptions that those will be covered already by others - Sitecore themselves, Adam Seabridge and others. I will just share some screenshots to show how the Sitecore interface might look like in the (near) future:


Still here? Good.. as I did want to write a small post about my 2020 Symposium experience. First of all, I did miss.. you! Yes, you. And you, and all the other members of our great Sitecore community. Before each symposium we take a look at the agenda and dream away when we read session titles that promise us great knowledge. But what we usually remember from each symposium are the moments outside the sessions - not just the fun (ok, yes, there is some fun..) but also the people. Getting to know the people within the Sitecore community and/or company. People who you might have helped already, or who you might help in their future Sitecore endeavors. And if you get stuck, they will probably help you as well.



Luckily we had some chats.. on Slack, on some virtual chat platform.. and on the second day we even found a breakfast Teams chat session with Sitecorians and a bunch of eager developers. And in these times, anything is better than nothing - it was good to hear other people thoughts on the announcements and evolutions.

Was it all so bad? No, it wasn't even bad! I'm glad I joined virtual Sym. And I did see some good sessions. Note that sessions are still available so you still have some time to catch up. Of course you have the keynotes, the sessions on containers with CaaS, Content Hubs and rendering hosts that you should (have) see(n) but we also got a lot of on-demand sessions to fill the rainy lockdown days. 

If you have an interest in SXA, you should watch the best practices session by Adam & Mark. And after that got you warmed up, continue by watching Una -always good for an interesting session- amplifying her SXA. As I skipped the marketing, business and commerce sessions I can't tell you about hidden gems in those areas but I do remember some nice stuff on Content Hub, on Web push notifications and I learned a few extra things on Solr, Azure and EXM. I'm not going to share more details or screenshots - I'm just giving ideas, you should watch them yourself ;)

Creating shorter sessions was a good idea - I really liked the 30' format and the on-demand sessions are usually even shorter. 

Symposium 2020 did deliver what I expected - I did learn some new things and got to interact a bit. The shorter sessions and on-demand videos to watch afterwards are good ideas and might also work in a non-virtual edition. We might evolve to a combination - take the best of both experiences, let people interact live and give some content for those whom can't travel. But nobody knows.. lets just hope we can all meet in Vegas next year 🤗




  

Monday, October 19, 2020

SXA tokens in a Sitecore Treelist datasource

Treelist datasource

A Treelist is a field type in Sitecore that is commonly used to select one or more items from a tree view. It is easy to use for editors and has a rather special way to set the root (datasource) for the tree that is available to those editors. 

A few related field types are available ootb (e.g. TreelistEx) and the Sitecore community has provided rather a few blogpost already on how to customize the Treelist field - and especially the way we set the datasource. 

The basics of how to set a Treelist datasource using the "datasource=" parameters syntax are described very well in this post from Mark Ursino. Or in the official Sitecore archives where setting items available for selecting within the TreeList field was posted already in 2009! Out of the box you are able to select a parent path and determine which templates can be shown and/or selected.

As I mentioned Sitecore community people have been posting blogs on how to extend the datasource based on several different requirements. 

SXA tokens

Sitecore eXperience Accelerator has a pipeline called resolveTokens. This pipeline is used to resolve tokens that can be used in queries and is (amongst other) useful to make templates multisite-friendly. 

The fields on your templates should have sources when applicable - i.e. when another item is selected. This applies to link type fields, list type fields, the image field...  Setting those sources is easy in a single site environment, but when you want to use those templates in a multisite setup you usually want the source to be relative to the site, the site's data folder... 

This is possible with the SXA tokens. You can set sources like "query:$site/..." and those will be resolved relatively within your sites. In a previous post I already showed how to extend this by creating a token for the real media root folder (to be able to add additional -lower- folders after the token).

This is a really nice way of working, but... it only applies to sources that start with "query:".

SXA tokens in a Treelist datasource

I do have a multisite setup. With SXA. And some of my templates do have Treelist fields. You can use the query notation in the datasource and use tokens that way, but then you loose the flexibility of the other parameters - so I was looking for a solution to have both. I want parameters to define the possible templates and I also want to use tokens to define the parent. 

My research led me to two blogposts that inspired me. Diego Moretto wrote about dynamic datasources in Treelist fields but his solution was for non-SXA and with the SXA tokens we do already have so much flexibility. He based his solution on a post from Kamruz Jaman that explains a solution on how to embed queries within the datasource syntax.

While reading that (really good) post I thought I had my solution. By implementing this you actually do have a source that (can) start with "query:" so the resolveTokens pipeline will run and everything will work. I had a tweet ready to thank Kamruz, but then I stumbled upon a small issue: 

SXA cannot resolve the tokens outside an SXA site. This is very logical, but as this is the case in your standard values that resulted in errors when I used this in a feature template with standard values. I don't want errors and the solution was simple - just return a standard path instead of null when the result of the query is null.

As a recap, I'll add my version of the code here (but still - credits to Kamruz for the original):
public class TokenedTreelist : Sitecore.Shell.Applications.ContentEditor.TreeList
{
  public override string DataSource
  {
    get
    {
      var data = base.DataSource;
      if (!base.DataSource.StartsWith("query:", StringComparison.OrdinalIgnoreCase))
      {
        return data;
      }

      if (Sitecore.Context.ContentDatabase == null || ItemID == null)
      {
        return null;
      }

      var currentItem = Sitecore.Context.ContentDatabase.GetItem(ItemID);
      Item source = null;
      try
      {
        source = LookupSources.GetItems(currentItem, data).FirstOrDefault();
      }
      catch (Exception ex)
      {
        Log.Error("Treelist field failed to execute query.", ex, this);
      }

      return source == null ? "/sitecore/Content" : source.Paths.FullPath;
    }

    set => base.DataSource = value;
  }
}  
We are actually creating a new field here - fully using the TreeList as a base and overriding the Datasource property. A few minor changes were made to the code from Kamruz - but the most important one is the last line of the getter where we return "/sitecore/content" as default instead of null. 

Don't forget to register this field in the core database as this is a new field. Add an item at /sitecore/system/Field types/List Types and fill your assembly and class information.


Also add it to the field types list in the Sitecore config:
<sitecore>
  <fieldTypes>
    <fieldType patch:after="*[@name='Treelist']" name="Tokened Treelist" type="Sitecore.Data.Fields.MultilistField, Sitecore.Kernel" />
  </fieldTypes>
</sitecore>
You might want to add it to the index configuration as well - in our case that was not needed.

If we select "Tokened Treelist" as field type, we can now use all SXA tokens within our datasource. Such a datasource could look like:
datasource=query:$site/Data/Documents&allowmultipleselection=yes&includetemplatesfordisplay=Document,Document folder&includetemplatesforselection=Document

Conclusion

I don't think Kamruz realised in 2016 when he wrote that post that this would now enable me to use SXA tokens in a Treelist datasource. Sitecore community has proven this strength once more - so I had to share my findings as well to inform you about the extra options this can give us: multisite Treelists with SXA tokens! 

Tuesday, October 13, 2020

Cloning a SXA site (in another folder)

Cloning a Sitecore SXA site

Part 7 already in my series of blog posts related to a multisite project where we wanted to make it as easy as possible to create a new site - with the premise that all the sites can share almost all settings. As you might have read in the previous posts we are really doing this - and keeping it all flexible. It's not because we share (all) things, we cannot configure or overrule some parts if needed.

Anyway - we do have a setup with a shared site and a base "master" site within a tenant (as recommended by SXA). As I did not want these sites to be amongst all the other created sites, I placed those two in a Sites Folder within the tenant. We have (at least) one other Sites Folder where we want to place the actual sites - and we might even want to split those sites over different folders.

We use SXA site cloning to create a new site. This is a very easy and rapid process. The "Clone Site" Powershell (SPE) script that is used can be found by a right click on a site item in the Scripts section. It will ask the name of the new destination site and copy everything from the master site. Note that:
  • it will also copy media items within the sites media folder - really useful if you have a standard folder setup there
  • it will not copy any forms - a new empty folder is created though within Forms
  • it will change all links towards the newly created items

The script works fine and it is a very fast way to create a site. But.. it will create a site next to the master site. Of course, we can move all items after the creation - but it would be faster to just ask a preferred location and created the site there. So that's what I did...

The solution provided here is only tested with one level of site folders - it probably could work with a tree of those folders with a few more adjustments though. 

The Powershell scripts to clone a site

Show-CloneSiteDialog

First script to take care of is the dialog script. We want the user that clones a site to be able to choose the site folder where the destination site should be created. I created a copy of the ootb script "/sitecore/system/Modules/PowerShell/Script Library/SXA/SXA - Multisite/Functions/Site cloning/Show-CloneSiteDialog" and adjusted that one (you could also opt to change the original one). The changes to the original script are not that big so I will not put the entire script here to keep focus.  

In the beginning of the script process the dialogParameters are defined. We add one more parameter there (siteLocation):
$dialogParameters.Add(@{ Name = "siteLocation"; 
  Title = [Sitecore.Globalization.Translate]::Text([Sitecore.XA.Foundation.Multisite.Texts]::Path); 
  Editor = "droplist"; Source="query:$($SourceSite.Parent.Paths.Path)/ancestor-or-self::*[@@templatename='Tenant']/*[@@templatename='Site Folder']"; 
  Tab = [Sitecore.Globalization.Translate]::Text([Sitecore.XA.Foundation.Multisite.Texts]::CloneSiteDialogGeneralTab); Mandatory=$true })  | Out-Null  
I am using a droplist here with a datasource query that finds my tenant and takes the children of type Site Folder. (note we are only supporting one level here - this customization would need to be changed to support a tree).
To keep it simple I used an existing text as label. The selection is marked as mandatory to make sure a choice is made.

The next (and final) change to this script is all the way at the bottom where the results are captured:
@{
    siteName              = $siteName
    siteDefinitionmapping = $mapping
    parent                = $siteLocation
}
We add a "parent" here that will contain the chosen location of the new site.

Clone-Site

Next script to adjust is the main one: "/sitecore/system/Modules/PowerShell/Script Library/SXA/SXA - Multisite/Content Editor/Context Menu/Clone Site". This is the script that gets called from the context menu and we need to adjust it so we can use the parent from our dialog:
$ctx = Get-Item .
    
Import-Function Copy-Site
Import-Function Show-CloneSiteInParentDialog
    
$dialogResult = Show-CloneSiteInParentDialog $ctx
$cloneSiteName = $dialogResult.siteName
$mapping = $dialogResult.siteDefinitionmapping

#custom    
 $parent = $dialogResult.parent
 if (!$parent) {
     $parent = $ctx.Parent
  }
#endcustom

$destinationSite = Copy-Site $ctx $parent $cloneSiteName $mapping
$destinationSiteID = $destinationSite.ID.ToString()
$host.PrivateData.CloseMessages.Add("item:load(id=$destinationSiteID)")

Run-SiteManager
I am just showing the main part here (within the "try") to keep it clear. So we changed the call to Show-CloneSite to the one we created, we fetch the parent (with a fallback scenario although that is probably not needed) and pass it to the Copy-Site function.

And that's it. Well, that's what we thought so we tested this. The site was indeed created in the correct folder. So was the forms folder. But the media folder was still next to our master. 

Copy-Site

Let's take a look at the "/sitecore/system/Modules/PowerShell/Script Library/SXA/SXA - Multisite/Functions/Site cloning/Copy-Site" script. The site creation was fine with our new parent. We noticed that creating the Forms folder was done as it would be for a new site and that script uses the paths to determine where to create a new (empty) folder - this works fine in our version as well.

So we need to take a look at the copy process for the media folder. Somewhere in the middle of the Copy-Site script you'll find the media part and let's customize that a bit:
if ($Site.SiteMediaLibrary) {
  $SiteMediaLibrary = $Site.Database.GetItem($Site.SiteMediaLibrary) | Wrap-Item

#custom
  if ($SiteMediaLibrary.Parent.Name -ne $destinationSite.parent.Name) {
    $SiteMediaRoot = $ctx.Database.GetItem($SiteMediaLibrary.Parent.Paths.Path.Replace($($SiteMediaLibrary.Parent.Name),$($destinationSite.parent.Name)))
    if (!$SiteMediaRoot) {
      $SiteMediaRoot = $ctx.Database.GetItem($ctx.SiteMediaLibrary).Parent
    }
  }
  else {
    $SiteMediaRoot = $SiteMediaLibrary.Parent
  }
#end custom

  $NewSiteMediaLibrary = Copy-RootAndFixReference $SiteMediaLibrary $SiteMediaRoot $destinationSite.Name

  Set-NewLinkReference $Site $destinationSite $SiteMediaLibrary.Paths.Path $NewSiteMediaLibrary.Paths.Path
  $destinationSite.SiteMediaLibrary = $NewSiteMediaLibrary.ID
}
Instead of copying the media tree to the parent of the master media tree, we use string replacement and the paths to determine the location we want. Note that we do need a folder structure corresponding to the Site Folders structure, otherwise this part will fail. We could expand the script to create the necessary folders if needed.


Conclusion

With that last change we managed it. Our new site cloning script will ask us where to create the destination site - of course within the same tenant and in our one-level Site Folder structure. Combined with all the stuff from my previous posts - and the ootb features - creating a site is becoming really fast.




Tuesday, October 6, 2020

Overriding partial designs from SXA shared site

Sitecore SXA shared partial designs

In a SXA multisite setup it is best/common practice to have a Shared site. This shared site can contain page and/or partial designs that can be used within the other sites.

Currently I am working on a multisite project where the aim is to make creating a site as easy as possible, knowing that all the sites will share as much as possible. In my previous posts I have been explaining how we shared various settings on top of the out of the box possibilities.

Partial designs

One of these ootb options are the partial designs. As our sites will share their main layout, header and footer (they can differ by theme) we do want to share the partial designs (and the page designs).

In our other sites -created from a master site- we do not have any partial nor page designs and I would like to keep it that way unless there is no other option.

This went fine, all ootb.. until we bumped into the logo in our header. This is actually a Rich Text component with a datasource -in the Shared site- and we do want to be able to override it when needed. Partial designs can be created with base designs and such but I really want to avoid having designs in the non-shared sites. 

Next issue was within the metadata partial design as we do have a 3rd party GDPR solution there that requires some javascript to be plugged in - which is a bit different for each domain. Our current implementation uses a html snippet so we needed a solution for that as well.  

Overriding a Rich Text datasource from a shared site

To solve our issue with the Rich Text component, I though I could easily create a new component -based on the original Rich Text- that works like the reusable Rich Text and additionally:
  1. checks the current site if a suited datasource was provided
  2. automatically uses the fallback (original datasource) from the shared site if no data was found in the current site
The code:
public class RichTextSharedController : VariantsController
{
  protected override object GetModel()
  {
      var model = base.GetModel() as VariantsRenderingModel;
      if (model?.DataSourceItem == null)
      {
          return model;
      }

      if (PageMode.IsExperienceEditor || PageMode.IsExperienceEditorEditing || !Context.Item.HasSharedSite())
      {
        return model; // skip this if we are in a shared site
      }

      var path = model.DataSourceItem.Paths.FullPath;
      var newPath = path.Replace(MultisiteContext.GetDataItem(model.DataSourceItem).Paths.ContentPath,
                MultisiteContext.DataItem.Paths.ContentPath);
      var contentItem = model.DataSourceItem.Database.GetItem(newPath);
      if (contentItem != null && contentItem.Versions.Count > 0)
      {
        //model.DataSourceItem = contentItem;
        model.Item = contentItem;
      }

      return model;
  }
}
We create a new controller that inherits from the VariantsController. We don't need a model or view as we will use the SXA versions for that. We override GetModel:
  1. Get the base model
  2. If there is no datasource, return the model as we have nothing more to do in this case
  3. If we are in edit mode, return the model as we want to show the original datasource in that mode
  4. If the current item does not have a shared site we also do not need to continue
  5. The actual fallback: we get the path of the datasource item (which is in the shared site) and want to move this path into the current site - we do this by replacing the paths of the data items from both sites
  6. We check if the "local" content item exists and adjust the model (note that we need to change the Item property - the DatasourceItem is optional)
To check if we have a shared site present, we used an extension method:
public static bool IsItemFromSite(this Item item, Item site)
{
  return item.Paths.FullPath.StartsWith(StringUtil.EnsurePostfix('/', site.Paths.FullPath), StringComparison.OrdinalIgnoreCase);
}

public static bool HasSharedSite(this Item item)
{
  var sites = ServiceLocator.ServiceProvider.GetService<ISharedSitesContext>().GetSharedSites(item);
  return sites.Any() && !sites.Any(item.IsItemFromSite);
}
So now we have our controller. Next step is creating a new rendering in Sitecore. Easiest way to do this is to create a copy of the Rich Text (Reusable) rendering in your own folder and adjust the Controller field towards your implementation. As this is a new rendering, you probably also need to add it to the available renderings and placeholder settings. 

But that's it. We can now use this field in a partial design in the shared site - it will work just like a reusable rich text in all other sites with one big difference: if we create a Text datasource at the relative same location as the original datasource, this one will be used instead. We can now override datasources in Rich Text components in shared partial designs.  

Overriding a Html Snippet from a shared site

Next one is the html snippet. This is actually very similar to the Rich Text one. We created a controller based on the Sitecore.XA.Feature.GenericMetaRendering.Controllers.HtmlSnippetController and changed the GetModel just like in our Rich Text example. 

A few small differences though: for the snippet an extra property is present, called "Html". Changing the datasource is not sufficient - you also need to set the value for that property (again). In order to be able to do this, make sure the model is of type HtmlSnippetModel. Another difference is that our data is not in the data folder but in the settings folder so we have to use those when replacing the paths.
public class HtmlSnippetSharedController : HtmlSnippetController
{
  public HtmlSnippetSharedController(IHtmlSnippetRepository htmlSnippetRepository) : base(htmlSnippetRepository)
  {
  }

  protected override object GetModel()
  {
    var model = base.GetModel() as HtmlSnippetModel;
    if (model?.DataSourceItem == null)
    {
      return model;
    }

    if (PageMode.IsExperienceEditor || PageMode.IsExperienceEditorEditing || !Context.Item.HasSharedSite())
    {
      return model; // skip this if we are in a shared site
    }

    var path = model.DataSourceItem.Paths.FullPath;
    var newPath = path.Replace(MultisiteContext.GetSettingsItem(model.DataSourceItem).Paths.ContentPath,
                        MultisiteContext.SettingsItem.Paths.ContentPath);
    var contentItem = model.DataSourceItem.Database.GetItem(newPath);
    if (contentItem != null && contentItem.Versions.Count > 0)
    {
      model.DataSourceItem = contentItem;
      model.Html = model.DataSourceItem[Sitecore.XA.Feature.GenericMetaRendering.Templates.HtmlSnippet.Fields.Html];
    }

    return model;
  }
}  

To create your rendering, use a copy from the Html Snippet rendering (in the Generic Meta Rendering feature folder). Replace the Controller and in this case you also need to set the Rendering view path as that is not set here by default (it is for the Rich Text) - enter ~/Views/HtmlSnippet/HtmlSnippet.cshtml to use the ootb view.

Conclusion

We are now able to override datasources from (some) components in a partial design in a shared site, which makes our solution even more flexible. 

If you want those changes for the original components (instead of creating a new one), that is also possible. In that case you will need to create a new version of the repositories that create the model and put the same logic inside those repositories. Registering your version with the dependency injection will then do the trick.

As we are adding extra logic and item retrievals you might want to take extra care with caching on these components - but I guess that should always be the case...

This was a big step in our solution towards a very flexible site creation in minimal time. In a next post, we'll be diving into some Powershell.



Wednesday, September 30, 2020

Sharing settings in a multisite SXA setup

Settings in a shared site - multisite Sitecore SXA

As you might know if you read my previous blogs, I'm working on a setup with an undefined number of (mini-)sites within a tenant that should share as much as possible in order to make the setup of a new site as simple and fast as possible. We already managed to share a lot: placeholder settings, 404 settings and of course the out of the box stuff like page and partial designs, rendering variants and so on.

We have a setup that matches the best practices with a shared site and a master site to clone from. 

This is the 5th part already in the blog series that results from that project, and this time we'll take a look at the Settings item in each SXA site. There are some candidates that we could share - the error handling section was taken care of in a previous post but I'll go over some other ones here.
Let's work bottom up..

Robots

It would be possible to share the robots settings which generates the robots.txt content for the site. There is a pipeline called getRobotsContent where we can plugin an extra processor if needed. This pipeline has ootb 3 processors.
The first one -GetContentFromSettings- will get the content from the Settings item of the current site. The second one is a backup and will fetch a physical robots.txt file if no content was provided (note that this could be useful in some cases to use this fallback functionality). The last one will add the sitemap information. 

We could insert our own version of the GetContentFromSettings in the 2nd spot where we get the contents from the shared site if we have no local content yet. That would be fairly easy to do.. but as you might notice it's all conditional here (could/would/...). Indeed, we actually decided not to do this.

We have a robots.txt file as fallback for the whole environment. Also, we want our shared (and master) site to stay closed for all robots - ok, they shouldn't be able to access them anyway but still...  So, all things considered we've put robots content in the master site that disallows all robots to make sure a new site does not gets opened until the site admin actually wants it to open. Still, it was interesting to see how the robots feature in SXA works.

Favicon

The favicon for your site can be managed in the Settings item of each site. You can select an item from the media library and it will be set as the favicon in that site by the Favicon rendering which is usually present in the metadata partial design.

Most of our sites will use the same favicon however. So we would like to share it - and there are a few ways of doing that. We could select the favicon in the master site and all newly created sites would use that favicon as they copy that value. It could be overwritten if needed so that is a valid option. It is an ootb option as well. The only disadvantage is that if the icon has to be changed (we usually do create a new item in that case so we don't have caching issues) we would need to change it on all sites.

Another option to explore was a fallback to the shared site where we could define the favicon just once. It appeared to be rather easy - this is the code we need:
public class FaviconRepository : Sitecore.XA.Feature.SiteMetadata.Repositories.Favicon.FaviconRepository, IFaviconRepository
{
  public override string GetFaviconHref(Item contextItem)
  {
    var icon = base.GetFaviconHref(contextItem);
    if (!string.IsNullOrEmpty(icon))
    {
      return icon;
    }

    var field = GetSharedSettingsItem(contextItem)?.Fields[Sitecore.XA.Feature.SiteMetadata.Templates._Favicon.Fields.Favicon];
    return field != null ? field.GetImageUrl() : string.Empty;
  }

  private static Item GetSharedSettingsItem(Item contextItem)
  {
    var shared = ServiceLocator.ServiceProvider.GetService<ISharedSitesContext>().GetSharedSitesWithoutCurrent(contextItem).FirstOrDefault();
    return shared != null ? ServiceLocator.ServiceProvider.GetService<IMultisiteContext>().GetSettingsItem(shared) : null;
  }
}
We override the FaviconRepository which is used in the Favicon rendering. The new GetFaviconHref function first calls the base version to check if anything was filled on the current site. If that is not the case we locate the Settings item for the (first - we know we have no more than one) shared site. On that Settings item we read the Favicon field and return the image url (or nothing). 

Registering this version in the dependency injection will make sure this is used and that's all there is to it. 


Conclusion

A smaller post this time.. but all small things will bring us to what we want (a site in 15 minutes). 

Don't worry though, as there will be a part 6 as well in which I'll cover locally overwriting some datasources set in the partial designs from the shared site. 

Monday, September 28, 2020

Custom SXA media query token

SXA query tokens

The resolveTokens pipeline is used to resolve tokens that can be used in a Sitecore SXA site in queries at several places: a query rendering variant, a template field source..  

In the multisite project that I'm working on -which resulted in a bunch of posts already- we wanted to have the source of an Image field to point towards a folder inside the media library that exists for each site.

One of the ootb tokens that can be used is $siteMedia. The problem with this token is that it will be resolved into the virtual media root within the site - which means that adding extra folders (e.g. $siteMedia/pdf) in the query does not work - the resulting path does not exist because the resolved part is a virtual media folder.

We can create a custom resolver for our own token however. This has been described by Maciej Gontarz in his blog post - there are some particular bits into our custom token however so I wanted to share anyway.

Custom SXA media root token

The idea is simple: we want to have the actual root of the media items for the site (inside the Media Library) and not the virtual one (inside the site). This way we can add a subfolder to it and still have a valid path. 

Note: this can really help your editors to get into the correct folder immediately, but it's also a bit dangerous as you will get an error when opening your item in the content editor when the resulting folder does not exist.  

To resolve our token we need a resolver - a processor to add into the pipeline:
using Sitecore.XA.Foundation.Multisite;
using Sitecore.XA.Foundation.Multisite.Extensions;
using Sitecore.XA.Foundation.TokenResolution.Pipelines.ResolveTokens;

public class ResolveTokens : ResolveTokensProcessor
{
  private const string Token = "$mediaSiteRoot";
  private readonly IMultisiteContext multisiteContext;

  public ResolveTokens(IMultisiteContext multisiteContext)
  {
      this.multisiteContext = multisiteContext;
  }

  public override void Process(ResolveTokensArgs args)
  {
      args.Query = ReplaceTokenWithItemPath(args.Query, Token, () => GetMediaRoot(args.ContextItem), args.EscapeSpaces);
  }

  private Item GetMediaRoot(Item contextItem)
  {
    var mediaRoot = multisiteContext.GetSiteMediaItem(contextItem);
    if (mediaRoot == null)
    {
      return null;
    }

    var root = mediaRoot.GetVirtualChildren();
    return root.Any() ? root.First() : mediaRoot;
  }
}
We defined our token as "$mediaSiteRoot" but you can call it (almost) anything you want - just try to avoid picking something that starts with an already existing token.

We use the ReplaceTokenWithItemPath function (comes with the ResolveTokensProcessor) as this will handle everything for us if we pass the correct item. To find our root item we start by fetching the media root item for the site using the MultisiteContext. If this is found, we can use the extension method GetVirtualChildren to get all virtual children that are defined on that item. 

SXA will define 2 children on the virtual media root - by default it's the first one we are looking for.

Now we need to add this resolver to the resolveTokens pipeline:
<sitecore>
  <pipelines>
    <resolveTokens>
      <processor type="Feature.Multisite.Pipelines.ResolveTokens, Feature.Multisite" resolve="true" 
             patch:before="processor[@type='Sitecore.XA.Foundation.TokenResolution.Pipelines.ResolveTokens.EscapeQueryTokens, Sitecore.XA.Foundation.TokenResolution']" />
    </resolveTokens>
  </pipelines>
</sitecore>
As you can see we patch this in before the EscapeQueryTokens. This means our token will be after the standard ones -which is ok- but it has to be before the escape one because otherwise the query won't work as it will be escaped (with #) already before we do our magic.

Using the custom token

We are now ready to use the token. In the source of our Image field we can now put things like 
query:$mediaSiteRoot/Content/Hero


Conclusion

We can now guide our editors to the correct folders inside the media library. But be aware - as mentioned before those folders do need to exist in all sites where you use the template. In our case we start from a master site that gets cloned to create new sites. This also means that the (media) folders that are created in that master site will get created in all new sites, which is great of course as we can make sure that certain folders exist this way. SXA can be really great sometimes 😃

Wednesday, September 23, 2020

Shared 404 setting in Sitecore SXA

Sharing a 404 page across SXA sites 

A third post inspired by a project to create a number of (mini-)sites that should share as much as possible and be very easy to create. The first one covered enhancing the language selector,  the second one showed how you can share placeholder settings across sites within a tenant. In this episode we will see how we can make the site setup a bit easier by sharing the 404 error page.

As our main focus is to make creating a new site within the tenant as easy as possible, my idea is to have a shared 404 error page and no worries about any settings when creating a new site.

Delegated area

First thing to do is create an actual page to display in case of a 404 error. This page should be in a delegated area. We have this setup in the master site (the site which is used as a base site for all other sites to be cloned from). The actual item -with content- for the 404 page is in the shared site and can be edited there.

This is a pretty standard setup for content sharing in SXA - nothing fancy so far.

404 setting

In SXA you can/should define the 404 page on each site. In the Settings item of your site you will find a Error Handling section (don't forget to switch to the content tab, as the Settings item is a folder and by default will open the Folder tab with icons of the children).

On the Shared site, we insert our 404 page in the Page Not Found Link.

Normally we should do this on every site. Of course, I could do this on the master site and it would get cloned to all new sites. But maybe there is way to tell our sites to get the value from the Shared site if we don't have a local value...  not out of the box, but with a little bit of custom code we got this working.

The code

public class ErrorPageLinkProvider : Sitecore.XA.Feature.ErrorHandling.Services.ErrorPageLinkProvider, IErrorPageLinkProvider
{
  public ErrorPageLinkProvider(IContext context, IMultisiteContext multisiteContext, BaseLinkManager baseLinkManager, ILinkProviderService linkProviderService)
            : base(context, multisiteContext, baseLinkManager, linkProviderService)
  {
  }

  public override Item Get404ErrorPageItem()
  {
    var item = base.Get404ErrorPageItem();
    if (item != null)
    {
      return item;
    }

    var site = Sitecore.Context.Site;
    if (!site.IsSxaSite())
    {
      return null;
    }

    var home = Sitecore.Context.Database.GetItem(site.StartPath);
    if (home == null)
    {
      return null;
    }

    foreach (var shared in ServiceLocator.ServiceProvider.GetService<ISharedSitesContext>().GetSharedSitesWithoutCurrent(home))
    {
      item = Get404ErrorItem(shared);
      if (item != null)
      {
        return item;
      }
    }

    return null;
  }

  private Item Get404ErrorItem(Item startItem)
  {
    var settingsItem = MultisiteContext.GetSettingsItem(startItem);
    return settingsItem != null ? Context.Database.GetItem(settingsItem[Sitecore.XA.Feature.ErrorHandling.Templates._ErrorHandling.Fields.Error404Page]) : null;
  }
}

We are overriding the ErrorPageLinkProvider and in particular the Get404ErrorPageItem function. If the base call does not find an item as 404 page, we try to fetch it from the shared sites. 

Note that we use the SharedSitesContext to get the shared site info and the MultisiteContext to get the site related information (e.g. here the Settings item). When working with SXA and writing code to do stuff like this those contexts are very useful.

One small tip when working with this 404 solution: make sure to set RequestErrors.UseServerSideRedirect Sitecore setting to true to get a real 404 (instead of a 302 redirect to a 200-status page).

Conclusion

Once again we managed to tweak SXA into doing what we want with very little code. And in the meantime we are almost where we need to be with our multisite project. Almost.. meaning yet a few things to do and a few more blogpost to come - sharing settings, using tokens, overriding logo's...  

Thursday, September 17, 2020

Shared placeholder settings in SXA

Sitecore SXA placeholder settings

In Sitecore you can can set "placeholder settings" to manage/restrict the renderings that can be added in a placeholder by the editors. This is no different in SXA, although you will edit those restriction settings in another way as you can read on https://doc.sitecore.com/developers/sxa/93/sitecore-experience-accelerator/en/set-placeholder-restrictions.html.

In SXA you can set placeholder restrictions per site. These restrictions can be found in the Presentations folder of the SXA site and are applied on all pages in the site.




Sharing placeholder settings from a shared site

I was working on a setup for a multi-site solution where we have a tenant which includes many (mini-)sites that share almost everything except content (and sometimes the theme). We followed the SXA best practices for multi-site setups and have a shared site to manage all page designs, partial designs, themes, styles, variants and some content in delegated areas. We also have a master site to clone when creating new sites. 

When I was setting the placeholder settings for the mini-sites, I could have set them in the master site. But I know that all my site level placeholder settings will be the same for all those sites and it seemed an idea to set them in the shared site instead of copying them all over the sites. This was not possible ootb, but SXA is extensible and so.. with a little bit of custom code I made this work.

My code will first check if the current site has any placeholder settings defined. If no settings are found, we check the shared site(s) and get all settings from there. This works for us, but can be altered of course (e.g. to use a site setting to determine to use the shared restrictions or not).

public class LayoutsPageContext : Sitecore.XA.Foundation.PlaceholderSettings.Services.LayoutsPageContext, ILayoutsPageContext
{
  public override Item GetSxaPlaceholderItem(string placeholderKey, Item currentItem)
  {
    var result = base.GetSxaPlaceholderItem(placeholderKey, currentItem);
    if (result == null || !result.Children.Any())
    {
      foreach (var site in ServiceLocator.ServiceProvider.GetService<ISharedSitesContext>().GetSharedSitesWithoutCurrent(currentItem))
      {
        result = base.GetSxaPlaceholderItem(placeholderKey, site);
        if (result != null && result.Children.Any())
        {
           return result;
        }
      }
    }

    return result;
  }
}
We are implementing ILayoutsPageContext and inheriting from Sitecore.XA.Foundation.PlaceholderSettings.Services.LayoutsPageContext to keep the ootb functionality. In our case we want to extend the functionality that fetches the root item for placeholder settings - we need to override GetSxaPlaceholderItem

First thing to do is check the setting items in the current site which can be done by using the base (ootb) function. If that has no result we loop through the available shared sites (using Sitecore.XA.Foundation.Multisite) and call the base function for each site item. As the base function uses the item parameter to search for an item with the "Placeholder Settings Folder" template, this works fine and gets us what we want: the first folder with placeholder settings will be used.

Register this new context class with your dependency injection framework and your Sitecore solution will use it. And you are able to share placeholder restriction settings across sites - defining them in one place in a shared site.


Conclusion

This solution is not something you will want in all multi-site setups. But in our case we really benefit from having -next to all the other shared stuff- our placeholder settings in one place. For one tenant of course, not for the other tenants. If you also have a solution where your sites within a tenant could share the placeholder settings, this might be something for you. 

This post is the second one in a series inspired by our multi-mini-site setup. The first one handled the language selector. Stay tuned for more SXA knowledge sharing...


SXA language selector in a multisite/multilanguage environment

Sitecore SXA multi-language + multi-site

We have an environment with a full blown SXA site in place, available in 4 languages. Language fallback is setup on item level, including some search engine enhancements to avoid duplicate content issues.

Next to this company site, a bunch of (mini) sites is being created. These sites (should) have nothing in common with the main site (they are in another tenant) but they do share almost everything between them. As they do live in the same environment however, they are also available in all 4 languages - and with the item language fallback those versions will always exist.

But.. those sites do not need to exist in all those languages. They could be in any subset of the available (4) languages of the Sitecore environment.


SXA language selector

We are using the ootb language selector component from SXA to handle our language switch. This works fine on the sites, but of course it will show all the available languages and we would like to limit the number of languages per site. One option is creating our own language selector - that would be easy but I thought there would be a way to tell the ootb component which languages to use. And yes, there is.

First step: add site settings

On the site item in SXA (found in the "Settings/Site Grouping" folder in your site tree) you can add (custom) properties. Normally the formsRoot should be there already (set by the script that generated the site). We add a custom property to define the languages we want on the site: the name is "siteLanguages" and in the example here I've set the value (language list) to "nl|fr" to allow Dutch and French. 

Note that the name and the format for the value are custom - chosen in favor of the implementation.

Setting a default language is an ootb feature of SXA but note that if you do not have English in the list of allowed languages, you should (must) set a default language in the Language field in that same site definition item. 


Step two: a new LanguageSelectorRepository

The Language selector component gets its values from the Sitecore.XA.Feature.Context.Repositories.LanguageSelector.LanguageSelectorRepository which implements an interface ILanguageSelectorRepository. We need to create a new version of this repository and override the GetLangItems function:
public class LanguageSelectorRepository : Sitecore.XA.Feature.Context.Repositories.LanguageSelector.LanguageSelectorRepository, ILanguageSelectorRepository
{
  protected override IEnumerable<Item> GetLangItems(Item item)
  {
    var languageItems = base.GetLangItems(item);
    var languages = Sitecore.Context.Site.Properties["siteLanguages"];
    if (string.IsNullOrEmpty(languages))
    {
      return languageItems;
    }
    
    var siteLanguages = languages.Split(new[] { "|" }, StringSplitOptions.RemoveEmptyEntries);
    if (!siteLanguages.Any())
    {
      return languageItems;
    }

    return languageItems.Where(i => siteLanguages.Contains(i.Language.Name));
  }
}
As you can see we first use the existing (base) function to determine all the possible languages (the ootb implementation here is based on the existing language versions of the current item). We also fetch the siteLanguages property from the current Site. If we detect that this property is set we will filter the list of languages based on the set from the properties. 

It's a pretty simple example of extending this logic - you can do anything you want/need here of course.

Final step

As a final but easy step, use your dependency injection framework and register your LanguageSelectorRepository as the implementation of the ILanguageSelectorRepository interface.


Conclusion

Extending SXA is usually quite easy. That was not different in this case. We can still use the ootb component and only adapted the logic for fetching the data, which saved us time obviously.

However we did only adjust the data logic for the language selector component. We did not put anything in place to prevent viewing pages in the languages that we don't really want. That is out of scope for this blog post, but can be easily achieved with an extra resolver in the httpRequestBegin pipeline (an example can be seen in the EasyLingo module)
 

This is a first post in a series that will come from our multi-site project. Stay tuned for more SXA knowledge sharing...


Monday, March 9, 2020

Extending Sitecore Scriban to handle User objects

Sitecore SXA & Scriban

As from SXA 9.3 we can use Scriban templates in the variants of SXA. Out of the box, Sitecore provided us with a few extensions that allow us to get some context information and all sorts of stuff on items. We can also fetch item fields and so on...

All of this is very nice and actually rather complete for most cases, but we can still easily write our own extensions.

Access to a User object 

During the 2020 hackathon, we created a Scriban extension to display user information. I tried to return User objects and fetch the properties from within the Scriban template as that would be awesome and very flexible - but it didn't work. Limited in time, I decided to just return the full name of the user instead. But I was curious.. how does this work with Items? So, as any good Sitecore developer, I asked the question on Sitecore StackExchange.
How can I get the user object (or any other object) in the Scriban templates?
Dawid (from the always helpful SXA team) told me it was possible. Not easy (his words), but it could be done - that sounds like a challenge to me 😛

Challenge accepted. And yes, challenge succeeded. Otherwise this would be a silly blog post...

Object Accessor

First step in the process is an object accessor - a UserAccessor in our case - that has the logic to access the properties of the object from within Scriban. It's a class that implements Scriban.Runtime.IObjectAccessor.
using System;
using System.Collections.Generic;
using System.Linq;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Sitecore.Security;
using Sitecore.Security.Accounts;

public class UserAccessor : IObjectAccessor
{
  public int GetMemberCount(TemplateContext context, SourceSpan span, object target)
  {
    return GetMembers(context, span, target).Count();
  }

  public IEnumerable<string> GetMembers(TemplateContext context, SourceSpan span, object target)
  {
    var user = target as User;
    if (user == null)
    {
      return Enumerable.Empty<string>();
    }

    var properties = typeof(UserProfile).GetProperties().Where(p => p.GetType().IsValueType);
    var result = properties.Select(p => p.Name);
    return result;
  }

  public bool HasMember(TemplateContext context, SourceSpan span, object target, string member)
  {
    return true;
  }

  public bool TryGetValue(TemplateContext context, SourceSpan span, object target, string member, out object value)
  {
    value = string.Empty;
    var user = target as User;
    if (user == null)
    {
      return false;
    }

    try
    {
      var property = typeof(UserProfile).GetProperty(member);
      if (property == null)
      {
        return false;
      }

      value = property.GetValue(user.Profile, null);
      return true;

    }
    catch
    {
      return false;
    }
  }

  public static string ToString(User user)
  {
    return user.Profile.FullName;
  }

  public bool TrySetValue(TemplateContext context, SourceSpan span, object target, string member, object value)
  {
    throw new InvalidOperationException("Unable to change user properties during the rendering process");
  }
}

What we have here:

  • GetMemberCount: return the number of members (see next function)
  • GetMembers: I want to be able to use all value typed properties (string and such) of a users profile. After checking if the object actually is a user, we fetch all those properties from the "UserProfile" type and return their names.
  • HasMember: as we have members, this is just true
  • TryGetValue: this function tries to get the value for the given member from the object. We first verify the object is a User. Then we try to get the property named "member" from the UserProfile type. If the property exists (note this is case sensitive) we can get the value of that property from the given object (User) - in our case it's the user.Profile and not the user itself. The return value indicates whether the value was found or not - we return an empty string on all failures.
  • ToString: function to determine what happens if the object is used without a member in the Scriban template - see later in the Context
  • TrySetValue: we don't allow set functionality

Template Context

In the template context all accessors are registered. We inherit from the current Sitecore.XA.Foundation.Scriban.ContextExtensions.SitecoreTemplateContext and override a few functions:
using Scriban.Parsing;
using Scriban.Runtime;
using Sitecore.Security.Accounts;
using Sitecore.XA.Foundation.Mvc.Models;
using Sitecore.XA.Foundation.Scriban.ContextExtensions;

public class GatoTemplateContext : SitecoreTemplateContext
{
  protected UserAccessor UserAccessor { get; }

  public GatoTemplateContext(RenderingWebEditingParams webEditingParams) : base(webEditingParams)
  {
    UserAccessor = new UserAccessor();
  }

  protected override IObjectAccessor GetMemberAccessorImpl(object target)
  {
    return target is User ? UserAccessor : base.GetMemberAccessorImpl(target);
  }

  public override string ToString(SourceSpan span, object value)
  {
    return value is User user ? UserAccessor.ToString(user) : base.ToString(span, value);
  }
}

The three functions that we override:

  • Constructor: we call the base constructor and initialize the UserAccessor property
  • GetMemberAccessorImpl: this method decides which accessor to use. We check if the target object is a User - if so we return the UserAccessor, if not we call the base method.
  • ToString: the general ToString method - we need to check the target object and if that is a User we call the ToString we created in the UserAccessor, otherwise we call the base method.

InitializeScribanContext

Next and last step is a processor to set our template context. The first call in the generateScribanContext pipeline is the current Scriban context initializer. We create a new version of this processor - implementing Sitecore.XA.Foundation.Scriban.Pipelines.GenerateScribanContext.IGenerateScribanContextProcessor.
using Scriban.Runtime;
using Sitecore.XA.Foundation.Scriban.Pipelines.GenerateScribanContext;

public class InitializeScribanContext : IGenerateScribanContextProcessor
{
  public void Process(GenerateScribanContextPipelineArgs args)
  {
    args.GlobalScriptObject = new ScriptObject();
    args.TemplateContext = new GatoTemplateContext(args.RenderingWebEditingParams);
  }
}

This processor sets our template context - we have to configure this processor instead of the existing one:
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:search="http://www.sitecore.net/xmlconfig/search/">
  <sitecore>
    <pipelines>
      <generateScribanContext>
        <processor type="Feature.Attendees.Subscribers.InitializeScribanContext, Feature.Attendees" patch:instead="*[@type='Sitecore.XA.Foundation.Scriban.Pipelines.GenerateScribanContext.InitializeScribanContext, Sitecore.XA.Foundation.Scriban']" resolve="true" />
      </generateScribanContext>
    </pipelines>
  </sitecore>
</configuration>

And that's is for code. This example used the Sitecore User object, but we could enable other objects the same way.


Scriban template

To use this in a Scriban template, you need a function that returns one or more User objects. Such a function is out-of-scope for this post, but you can easily create that with a custom Scriban extension.
<div>
<ul>
{{for i_user in (sc_subscribers i_page)}}
  <li>{{i_user}} - {{i_user.Email}} - {{i_user.Portrait}}</li>
{{end}}
</ul>
</div>

The sc_subscribers function in the example is a custom function that returns users. As you can see we can then write {{i_user}} (this will use the ToString method) and also things like {{i_user.Email}} to get the profile properties of a user.

Pretty cool stuff.. making the Scriban templates even more useful!


from Cat GIFs via Gfycat


Thursday, March 5, 2020

Sitecore Hackathon 2020: The BeeGhentle story

Sitecore hackathon

February 29 - 02:00 AM local time. Way too early to get up on a Saturday morning, but it's that day of the year again: the annual Sitecore hackathon day! 82 teams from 23 countries this year, and we -BeeGhentle- are one of them.

It will be my 5th participation, 6th if you count the one were I abandoned sick as well. I've had various results over those years: complete failure, submissions, almost-submissions and one victory. But each year we learned a lot and had fun. And that is what the hackathon is about.

Of course it is also about creating an open source application and getting ideas, examples (and maybe modules) out in the community. With a bit of time pressure as you get 24 hours straight to finish, and a little competition as well to make it even more exiting.

Learning

As mentioned, the hackathon is an excellent opportunity to learn. You could stick to your daily tasks and routine, but it's more fun if you try something new. As you only have limited time, one of the things to manage is how much time you can spend on learning and how much on actually building something to deliver.  

Getting ideas, managing your time, making the right decisions and handling pressure when the "start finishing" tweets start coming.. it all makes part of the learning process.

Normally every participant should have learned something new on a technical perspective during the hackathon. But I'm quite sure they also learned something on a more personal level - still business/work related though. And if you participated with a team that also works together the rest of the year (as I did), you probably also learned something about how your team functions.
from Priceless GIFs via Gfycat

Team "Bee Ghentle" 2020

This year I entered the competition with 2 of my (junior) colleagues. Our front-end beard-guy Sam had some experience with SXA but now was immersed into the wonders of SXA 9.3. Bearded handyman Alex had some encounters with marketing terminology, extending Forms (and meeting the limitations), Unicorns and his new best friend "Helix". I hope you learned a lot guys - you did a good job!

First step: an idea     

The topics this year were quite surprising and somewhat challenging:
  • "Sitecore Meetup Website": create a site for the Sitecore user groups to replace "Meetup"
  • "Sitecore Marketplace Website": create a new marketplace website
  • "Sitecore Hackathon Website": create a new website for the hackathon (or how to get people to create your website for free - just kidding, it would be a pleasure doing this)

Still a bit sleepy we decided to go for the first topic. We though the Commerce geniuses would build an awesome marketplace and the JSS experts would come up with a mind-blowing yearly-one-pager for the hackathon. We installed SXA and got started...

Our idea(s) might be a bit different than others, as we went a bit creative and didn't stick to what might be seen as the current situation.

We wanted to create a solution for all user groups. Why? Because we can. We could have gone for one site per user group, but we thought that one site to rule group them all would benefit people as user groups might start working together that way.

Next part of the idea was the fact that user group organizers know Sitecore. That is an assumption, but if they don't they should be fired as organizer. With a bit of security it should be possible to give organizers the ability to edit their own stuff in Sitecore directly. It's still a Content Management System - so let it handle content. This way we didn't have to worry about forms to create user group events. After all, it's a Sitecore hackathon, not a form-writing contest.

We also decided to skip a lot of tasks that we normally would do in a website because they seemed out-of-scope for a hackathon. So we didn't care about a 404 page, security headers, ... and focused on getting a mvp version that worked with some nice fancy tricks to show some Sitecore potential. Unfortunately we weren't able to do all of it - I must admit we do miss some functionality (e.g. unsubscribe - personalizing the subscribe button could help to do that trick) and we would have liked to get visitors recommended sessions based on your xDB profile.. but we ran out of time.

The build

We did manage to deliver a working version - or at least we hope it works once installed on another instance :)

The code and documentation can be found on Github (you can also find all other submissions there) - the video is also on Github.

We started with a faceted overview of the available user groups.

We should display the user group logo's on this page, but that is content - we tested with one image to make sure it works.

Selecting the user group brings the visitor to the group page with some general information and an overview of the events of that group - with the first upcoming event highlighted on top.


An event also has a (not-fully-styled) detail page with the sessions and speakers in more detail. We decided to share speakers across user groups as some of them tend to speak at quite a few. 

Visitors of user groups can register on the site - a (extranet) Sitecore user is created. We had some ideas with EXM and marketing automation here but those didn't make the cut for the deadline.
Once registered and logged in, you can subscribe to an event with a simple click (as we already you). Each event also displays the people that are coming.

As a finishing touch we created a component on the homepage that displays all events you subscribed for (this should be the upcoming events you subscribed, which is just a small change to the index query but we had no more time to test that so we decided to go as is). A Sitecore site needs something personalized, right?

The techy stuff

Creating the site in SXA was rather trivial for me. We did write a few extensions that helped us showing what we wanted with as little code and as much flexibility as possible.

On some parts we had to make decisions that were not perfect. To store the visitors of the user group, we took a shortcut writing them in the Sitecore database(s). Yes, this will not work on a scaled setup - we know. But we didn't manage to find the time setting up a custom database to store them.

The faceted overview was done with standard SXA components and scopes. To display the first upcoming event we used a custom datasource which we registered as item query to be used in a page list. Extra queries to display other lists of events could be easily created this way.

For the "Who's coming" feature and the "My next event" I created a Scriban extension. To get the users next event was a fairly easy query on the Sitecore index and we returned the event Item - this way we can display anything we want about the event in the Scriban template.

To display who is coming to the event was more challenging as I actually wanted to return the Sitecore users to the Scriban template so we could select any property we wanted there. That didn't seem to work so we had to settle with a function that returned the names. Still pretty neat though.

Next steps

I was still wondering about users in Scriban so I did what every Sitecore developer should do: I asked it on Sitecore StackExchange. And as expected, I did get an answer and it even seems possible... so that might be a next blog post ;)

Go home

We had some stress, we had some fun - we learned - we delivered something... maybe not everything we hoped for (did we aim too high?), but anyway, the hackathon 2020 is over. If all goes well we'll celebrate it at Sugcon in Budapest - whoever wins, they will have deserved it.  

I didn't check all the code on Github - I did watch the video playlist on Youtube (which does not include our video as it's not on Youtube). I've seen some (potentially) great entries..  but if I see the entries in the meetup topic I do notice we really took another angle. We'll leave it to the honorable judges to decide what they think about it...  I still think we had a good approach. 

But most importantly: we enjoyed it - and we're still alive!

Again next year?


Friday, February 28, 2020

SXA date variant and language fallback

Sitecore SXA DateVariantField

Note: this post assumes you know what a SXA variant is - if not, please read the documentation first.

Within a SXA variant, you can use a DateVariantField "Date" to display date and time from a date field in a chosen format. This definition is similar to the Field variant, but has an additional field "Date format" that allows you to choose a date and time format.





The date formats are stored in /sitecore/system/Settings/Foundation/Experience Accelerator/Rendering Variants/Enums/Date Formats as items (with display names).

On https://sitecore.stackexchange.com/a/8489/237 Alan explains how this works together with cultures and such. In short: SXA will render the date in the current culture with the given format.

We have a project with a few languages and language fallback is enabled with English as fallback for all other languages. Our date variants worked perfectly in Sitecore 9.0.2 with SXA 1.7.1 but after our upgrade to Sitecore 9.2 and SXA 1.9 we saw something weird...

Date variants and language fallback

So we have a site with language fallback enabled and it seems to work fine. But then we noticed weird date formats in our date variants when switching languages - although we had the format set to dd/MM/yyyy we did see a time value:


In English everything was still ok.

The workaround

First thing to check is language fallback is actually enabled on the site.

Note that since SXA 1.9 there is a checkbox to enable item language fallback. As this is an upgraded site, we also still have the fallback setting in the Other properties field, just to be very sure...



The workaround for the issue that I solved it for me was:
  1. Create language versions for /sitecore/system/Settings/Foundation/Experience Accelerator/Rendering Variants/Enums/Date Formats/date2 (which is the dd/MM/yyyy item) and give them a display name just like the English version. Repeat for every format you want to use.
  2. Remove the standard value from the variants format field. Save. Then select the dd/MM/yyyy again (this time actually selected and not as standard value). 
  3. Create language versions for the variant item.
Not all steps might be needed - Support and myself had some different conclusions on that.. and I think caching was also involved. Step 2 however is certainly needed - with a standard value it won't work (bug listed with reference number 119290)

But with this workaround all was fine again.



Sitecore reporting server getting lots of requests?

Reporting server issues in Sitecore 9.x

As part of an upgrade process,  we installed a new Sitecore (9.2) instance on PAAS. No custom code was deployed to it yet. Rather soon, we noticed lots of requests to our reporting server in the logs. As we started importing packages with content, the number of requests seemed to go up until a level where it became troublesome as our logs were really flooded with those requests. With a little help from Sitecore Support we found the issue and I documented it on Sitecore Stack Exchange: https://sitecore.stackexchange.com/q/23923/237

However, recently Chris Auer send this tweet:
This made me realize that more symptoms might be found for actually the same "issue".  A blog post might help to capture those in one place...

Symptoms

What are the symptoms you might encounter:
  • rebuild of the sitecore_suggested_test_index is triggered every hour (this is actually the root cause, you should find this in your logs and probably think that is ok)
  • High peaks on the reporting server or database as described by Chris
  • Logs flooded with requests for: POST /~/v75/reporting/remotedatasourceproxy/
The symptoms will come in batches of an hour, as seen in this view of our Application Insights logs for the POST request to the reporting server:

The root cause

The root cause is actually the sitecore_suggested_test_index which triggers those POST requests. 

The issue is described on the Sitecore knowledge base, and appears in versions 9.1.1+ (reference number is 336911 - as soon as we see this in the release notes, it is fixed). Sitecore mentions this can increase resource consumption - meaning flooded logged and/or possible resource peaks.

The fix 

There is no real fix to stop the requests as those are apparently needed - but you can change the schedule as mentioned in the KB article: change the schedule info on /sitecore/system/Tasks/Schedules/Content Testing/Rebuild Suggested Tests Index to "20140101|99990101|127|1.00:00" for 1 day instead of 1 hour - or go for even less (7 days would be "20140101|99990101|127|7.00:00")  if you don't use the functionality.

Suggested test index

The suggested test index is described on the Sitecore documentation site. 
The Suggested Test index provides storage for computed values for all of the content items in the content tree and is used to suggest the items that should be optimized.

This index drives the suggested test list in the Experience Optimization and inside the Experience Editor. It is also used in the Sitecore client UI to show when a test has ended.
This might help you determine a good period for the rebuild schedule.