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.



No comments:

Post a Comment