Using Sitecore to Manage Your Bundled Assets

There's no reason why you shouldn't be bundling your assets as it cuts down on requests by your Users. One thing about it though, is the bundles are defined in the BudleConfig class on application startup which means you have to re-deploy if the bundles need changing. So, let's use Sitecore Config and Pipelines instead so we can manage what's being created without changing anything other than a simple config file.

Setting up the Configurations

Since we're using the Helix pattern, we're going to add this functionality to the Foundation.Assets project so the individual sites can use it.  A processor will be added to the initialize pipeline which will look for site-specific configuration files. To get this running we need to start with a configuration to add the processor. In the example below you can see RegisterPlatformBundles will run on initialization. Another important addition to this config is the IgnoreUrlPrefixes setting, which adds "/Assets" to the OOB values. Without this Sitecore will try to serve up any bundle under this path which would result in a 404, so this fixes that issue.

<configuration xmlns:patch=""></configuration>  <sitecore>
        <processor patch:before="processor[@type='Sitecore.Mvc.Pipelines.Loader.InitializeGlobalFilters, Sitecore.Mvc']"
           type="Sitecore.Foundation.Assets.Pipelines.RegisterPlatformBundles, Sitecore.Foundation.Assets" />
      <setting name="IgnoreUrlPrefixes" value="/Assets|/sitecore/default.aspx|/trace.axd|/webresource.axd|/sitecore/shell/Controls/Rich Text Editor/Telerik.Web.UI.DialogHandler.aspx|/sitecore/shell/applications/content manager/telerik.web.ui.dialoghandler.aspx|/sitecore/shell/Controls/Rich Text Editor/Telerik.Web.UI.SpellCheckHandler.axd|/Telerik.Web.UI.WebResource.axd|/sitecore/admin/upgrade/|/layouts/testing|/sitecore/service/xdb/disabled.aspx"/>

The next config will be in the individual website project so the bundles can be specified. RegisterPlatformBundles will read through these values and create bundles from them. An important note, is that a minified file will take the place of a non-minified one, even if you specify the latter. For example, if you add an asset called test.css, but test.min.css is on the server in the same location, the bundler will take the test.min.css file.

<configuration xmlns:patch=""></configuration><sitecore>
            <processor type="Sitecore.Foundation.Assets.Pipelines.RegisterPlatformBundles, Sitecore.Foundation.Assets">
              <siteAssets hint="raw:AddBundleAssets">
                <asset file="~/Assets/Project/bootstrap.min.css" bundlename="website.min.css" />
                <asset file="~/Assets/Project/jquery-3.3.1.slim.min.js" bundlename="website.min.js" />
                <asset file="~/Assets/Project/bootstrap.min.js" bundlename="website.min.js" />

Creating the Bundles

Now that our values are set, we can start with the processor. The following snippets can all be added to the RegisterPlatformBundles class in the Sitecore.Foundation.Assets.Pipelines namespace.

The first thing to do is declare the Asset class and list.

        protected List<Asset> Assets = new List<Asset>();
        protected class Asset
            public string File { get; set; }
            public string BundleName { get; set; }

Then we start the with the Process and call the RegisterBundles method from it. You'll see that I added a log entry here that isn't absolutely needed, but it only write on initialization and if something's awry you can have a peek to see if an asset was skipped in adding to the bundle.

    public virtual void Process(PipelineArgs args)
        => RegisterBundles(BundleTable.Bundles);
    private void RegisterBundles(BundleCollection bundles)
        foreach (IEnumerable<Asset> assetGroup in Assets.GroupBy(x => x.BundleName))
            foreach(Asset asset in assetGroup){
                Log.Info($"[{GetType().FullName}.{MethodBase.GetCurrentMethod().Name}] -> Adding asset {asset.File}  to bundle {asset.BundleName}", this);
            bundles.Add(new Bundle($"~/Assets/{assetGroup.FirstOrDefault().BundleName}")
                .Include(assetGroup.Select(x => x.File).ToArray()));

Finally, we have to add the AddBundleAssets method which is reference in the config file. This will create the list of assets that the above RegisterBundles requires. Again there's some logs here to help out in case the config isn't quite right.

    public void AddBundleAssets(XmlNode node)
        if (node == null 
            || node.Attributes == null 
            || node.Attributes["file"] == null
            || string.IsNullOrWhiteSpace(node.Attributes["file"].Value.ToString())
            || node.Attributes["bundlename"].Value == null
            || string.IsNullOrWhiteSpace(node.Attributes["bundlename"].Value.ToString()))
            Log.Info($"[{GetType().FullName}.{MethodBase.GetCurrentMethod().Name}] -> Missing data in node.",this);
        Assets.Add(new Asset()
            File = node.Attributes["file"].Value,
            BundleName = node.Attributes["bundlename"].Value

Rendering the Bundles in Your Layout

Now your main layout file can look a little something like this.

@using Sitecore.Feature.Content
@using System.Web.Optimization
<!DOCTYPE html>

And you'll get the following generate into your layout.

  • <link href="/Assets/website.min.css?v=rYneK3o04F1vC98kmGCvJINDbReVXLY8sONeFoewNLI1" rel="stylesheet"/>
  • <script src="/Assets/website.min.js?v=kKA5PdkctqhJzBpkC6t_TsNTpaozhyblE_0HA9E3h8U1"></script>