Extending SXA asset rendering

The past few months I have been working a lot with Sitecore Experience Accelerator and, as with every module or given functionality, I wanted to extend it.
Sitecore’s SXA offers a lot of functionalities, so you want to reuse a lot of that. Things like a map component, search results and filter are great components to reuse but, as with a lot of things, it probably wont entirely fit the needs of your client. In order to completely be able to extend these components you also need to be able to extend or overwrite the JavaScript of those components. However, you don’t want to be overwriting the actual files and renderings as they will be overwritten again when you upgrade SXA.

There are two reasons I wanted to be able to assign SXA Base Themes to specific renderings.
First of all because we don’t want to load all the JS on every page, this to minimize the page size and optimizing frontend performance. Secondly to more easily clone SXA renderings and replace their JS with customized JS.

SXA uses a pipeline ‘assetService’ to render all the required asset includes on a page. This is being used for loading all the grid and theme assets for example, both the CSS and JS.

<assetService patch:source="Sitecore.XA.Feature.Composites.config">
  <processor type="Sitecore.XA.Feature.Composites.Pipelines.AssetService.AddGridTheme, Sitecore.XA.Feature.Composites" resolve="true"/>
  <processor type="Sitecore.XA.Feature.SiteMetadata.Pipelines.AssetService.AddMetaPartialDesignTheme, Sitecore.XA.Feature.SiteMetadata" resolve="true" />
  <processor type="Sitecore.XA.Foundation.Editing.Pipelines.AssetService.AddEditingTheme, Sitecore.XA.Foundation.Editing" resolve="true" />
  <processor type="Sitecore.XA.Foundation.Editing.Pipelines.AssetService.AddHorizonEditingTheme, Sitecore.XA.Foundation.Editing" resolve="true" />
  <processor type="Sitecore.XA.Foundation.Grid.Pipelines.AssetService.AddGridTheme, Sitecore.XA.Foundation.Grid" resolve="true" />
  <processor type="Sitecore.XA.Foundation.Theming.Pipelines.AssetService.AddTheme, Sitecore.XA.Foundation.Theming" resolve="true" />
  <processor type="Sitecore.XA.Foundation.Theming.Pipelines.AssetService.AddThemeExtensions, Sitecore.XA.Foundation.Theming" resolve="true" />
  <processor type="Sitecore.XA.Feature.Overlays.Pipelines.AssetService.AddTheme, Sitecore.XA.Feature.Overlays" resolve="true" />
</assetService>

Each processor in this pipeline looks for assets needed and adds them to the AssetsList in the processor args.

In the pipeline we also have access to the PageContext, which contains a list of all renderings on that page.

  foreach (Rendering rendering in GetRenderingsList())

By adding an extra Treelist field to the Controller rendering template, we can associate Base Themes to specific renderings.

Sitecore.Data.Fields.MultilistField baseThemes = item.Fields[Templates.RenderingAssets.Fields.BaseThemes];

foreach (var theme in baseThemes.GetItems())
{
  if (theme == null || assetsList.Any(i => i is ThemeInclude assetInclude && assetInclude.ThemeId == theme.ID))
    continue;
  
  renderingThemes.Add(new ThemeInclude
    {
      Name = theme.Name,
      ThemeId = theme.ID,
      Theme = theme
  });
}

Within a custom processor in the assetService pipeline we can then pull those Base Themes and add the assets to the AssetsList if not already present. The end result:

public class AddRenderingAssets : AddAssetsProcessor
{
  public override void Process(AssetsArgs args)
  {
    var assetsList = args.AssetsList as List<AssetInclude> ?? new List<AssetInclude>();
    foreach (Rendering rendering in GetRenderingsList())
    {
      var item = GetRenderingItem(rendering);

      if (item == null)
        continue;

      var cacheKey = $"{Context.Database.Name}#{item.ID}#{this.Context.Device.ID}#RenderingAssets";
      var renderingThemes = (List<AssetInclude>)HttpRuntime.Cache.Get(cacheKey);

      if (renderingThemes == null)
      {
        renderingThemes = new List<AssetInclude>();

        Sitecore.Data.Fields.MultilistField baseThemes = item.Fields[Templates.RenderingAssets.Fields.BaseThemes];
        foreach (var theme in baseThemes.GetItems())
        {
          if (theme == null || assetsList.Any(i => i is ThemeInclude assetInclude && assetInclude.ThemeId == theme.ID))
            continue;

          renderingThemes.Add(new ThemeInclude
          {
            Name = theme.Name,
            ThemeId = theme.ID,
            Theme = theme
          });
        }

        if (global::Sitecore.Context.PageMode.IsNormal && rendering.RenderingItem.Caching.Cacheable)
          this.CacheRenderingAssets(cacheKey, renderingThemes);
      }

      assetsList.AddRange(renderingThemes);
    }
  }

  private List<Rendering> GetRenderingsList()
  {
    return global::Sitecore.Mvc.Presentation.PageContext.Current.PageDefinition.Renderings;
  }

  private Item GetRenderingItem(Rendering rendering)
  {
    if (rendering.RenderingItem == null)
    {
      Log.Warn($"rendering.RenderingItem is null for {rendering.RenderingItemPath}", this);
      return null;
    }

    return rendering.RenderingItem.InnerItem;
  }

  private void CacheRenderingAssets(string key, List<AssetInclude> assets)
  {
    if (assets == null || !assets.Any())
      return;
    HttpRuntime.Cache.Add(key, assets, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
  }
}

Small changes like these help SXA become even more flexible and bend it to the needs of your website and team.