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...