SxA

TIHIDI: SXA Theme Deployment

With the release of SXA 10 the tooling around creating an SXA theme took a leap forward and added new functionality that made the development and delivery of themes much better. No more fighting with the ASP.Net bundler to minify and concatenate your files, now we can pre-optimize the files before they get imported into Sitecore.

This gives us a number of advantages, but how do we deploy a theme now? Do we import it into a live site? What does the development workflow look like?

Well, there are a few options, but in this post, I’m going to talk about how we do it at Perficient.

Pre-Requisites

Just a few caveates for this method. At Perficient, we use Unicorn exclusively for item serialization. This method could work for Sitecore Content Serialization, _if_ Sitecore gets feature parity with Unicorn and adds things like Field level exclusions.

Also, we are using Azure DevOps pipelines for build and release, with some PowerShell commands and Git as our source control system. The principles for this method could be applied to other setups, but I will be focusing on our setup.

This solution also makes use of environment variables to denote whether the environment is for local dev or non-local dev.

Source Control Setup

For the theme, there are 2 parts that we include in source control. First we have the static files, the SASS and JS files created by the SXA CLI. We make sure that any file that is generated by the CLI, e.g. all the CSS files, are excluded from source control. This helps prevent annoying merge conflicts and these files do not need source control anyway.

Secondly, we have the serialized Theme items. This includes all the items that are part of a theme, the fonts, images, CSS and JS items.

Unicorn Setup

The Unicorn setup is where this gets interesting, and a big thanks to Mark Cassidy for all the feature additions and maintenance he has done on Unicorn since taking it over from Kam, even with SCS - Unicorn is still the best option for item serialization.

We have 2 configurations for the theme. It may seem overkill, but the reasons will become apparent.

The first configuration, is a basic always deploy configuration for the main theme files.

1
2
3
4
5
6
7
8
9
10
11
12
13
<configuration name="Project.MyTheme.Theme"
description="Main theme scaffolding items"
extends="Helix"
dependencies="Foundation.Serialization"
patch:after="configuration[@name='Foundation.Serialization']">
<targetDataStore physicalRootPath="$(sourceFolder)\$(layer)\$(module)\serialization\theme" />
<predicate>
<include name="Theme" database="master" path="/sitecore/media library/Themes/Tenant/MyTheme">
<exclude path="MyTheme/styles/pre-optimized-min" />
<exclude path="MyTheme/Scripts/pre-optimized-min" />
</include>
</predicate>
</configuration>

Notice that we are excluding the pre-optimized-min files for both CSS and JS here.

The second configuration has the magic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<configuration name="Project.MyTheme.Theme.FED.Placeholders"
description="Placeholders for files which are populated from FED folder"
extends="Helix"
dependencies="Project.MyTheme.Theme"
patch:after="configuration[@name='Foundation.Serialization']">
<targetDataStore physicalRootPath="$(sourceFolder)\$(layer)\$(module)\serialization\fed.placeholder" />
<predicate>
<include name="Scripts" database="master" path="/sitecore/media library/Themes/Tenant/MyTheme/MyTheme/Scripts/pre-optimized-min"></include>
<include name="Styles" database="master" path="/sitecore/media library/Themes/Tenant/MyTheme/MyTheme/styles/pre-optimized-min"></include>
</predicate>
<fieldFilter type="Rainbow.Filtering.ConfigurationFieldFilter, Rainbow" singleInstance="true">
<exclude fieldID="{40E50ED9-BA07-4702-992E-A912738D32DC}" note="'Blob' field on /sitecore/templates/System/Media/Unversioned/File template " env:require="Local" />
<exclude fieldID="{6954B7C7-2487-423F-8600-436CB3B6DC0E}" note="'Size' field on /sitecore/templates/System/Media/Unversioned/File" env:require="Local" />
<exclude fieldID="{B1E16562-F3F9-4DDD-84CA-6E099950ECC0}" note="'Last run' field on Schedule template (used to register tasks)" />
<exclude fieldID="{52807595-0F8F-4B20-8D2A-CB71D28C6103}" note="'__Owner' field on Standard Template" />
<exclude fieldID="{8CDC337E-A112-42FB-BBB4-4143751E123F}" note="'__Revision' field on Standard Template" />
<exclude fieldID="{D9CF14B1-FA16-4BA6-9288-E8A174D4D522}" note="'__Updated' field on Standard Template" />
<exclude fieldID="{BADD9CF9-53E0-4D0C-BCC0-2D784C282F6A}" note="'__Updated by' field on Standard Template" />
<exclude fieldID="{001DD393-96C5-490B-924A-B0F25CD9EFD8}" note="'__Lock' field on Standard Template" />
</fieldFilter>
</configuration>

The field filter is what helps us out here. We are serializing the pre-optimized-min JS and CSS files, but excluding the Blob and Size fields. Notice that those field filters have env:require="Local", this means that these fields will only be excluded for local development. When this is deployed to the upper environments - dev/qa/stage/production etc… these fields will be included in any sync tasks.

What this gives us locally is a way to serialize the theme and prepare it for deployment, without having to worry about merge conflicts caused by the base64 encoded blob data in the pre-optimized items being updated when the themes are updated during development.

You could do all this with a single theme serialization config, but everytime a developer imported the latest theme, it would update the values in source control and becuase the blob data is a base64 encoded string, it will cause merge conflicts a lot. To get around that, your developers could just ignore changes to that file, but that relies on developers having a good memory… and I would forget frequently! Having the field filter just makes the process seamless and means there isn’t anything extra for developers to do day to day.

The final piece…. Build and Deploy scripts

To tie this all together, we use the build and deploy scripts in the azure-pipelines.yml file. First we need to make sure the theme is built using the SXA CLI. We could use the NPM tasks in in the azure pipelines, but its easier to just use a PowerShell script to do it:

1
2
3
4
5
6
- task: PowerShell@2
condition: and(eq(variables['Build.SourceBranch'], 'refs/heads/master'), not(canceled()))
displayName: "Run Creative Exchange tasks"
inputs:
filePath: '$(Build.SourcesDirectory)\scripts\Invoke-ThemeOptimization.ps1'
arguments: '-BuildSourcesDirectory $(Build.SourcesDirectory) -ProjectPaths \src\Project\Site1\,\src\Project\Site2\,\src\Project\Site3\'

This is the task, notice that we can use this to build multiple themes if we are a multi-site implementation. Just pass in the path to the static theme files for each sites theme you want to build.

Once we have built the theme into the pre-optimized-min CSS and JS files, we can use those files to populate the Blob and Size fields in the serialized scripts\pre-optimized-min.yml and styles\pre-optimized-min.yml. This effectively gives us a complete serialized file ready to be sync’d as part of the deployment process. This script assumes that your theme and theme serialization folders are at the same level.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
param (
[Parameter(Mandatory = $true)][string] $BuildSourcesDirectory = "C:\Projects\Sitecore9x\",
[Parameter(Mandatory = $true)][string[]] $ProjectPaths
)
function SetThemeYml{
param(
[Parameter(Mandatory = $true)][string]$themeFolder,
[Parameter(Mandatory = $true)][string]$ymlFolder,
[Parameter(Mandatory = $true)][string]$sourceFile
)
& "$BuildSourcesDirectory\src\scripts\Set-ThemeYaml.ps1" -SourceFile "$themeFolder\$sourceFile" -DestFile "$ymlFolder\pre-optimized-min.yml"
}

# Only need to install the global SXA CLI once.
npm config set @sxa:registry=https://sitecore.myget.org/F/sc-npm-packages/npm/
npm install -g @sxa/CLI

foreach($projectPath in $ProjectPaths){
$projectPath = $BuildSourcesDirectory + $projectPath
Write-Host "`r`nStarting Optimization for: $projectPath"

[string]$ThemeFolder=Get-ChildItem -Filter "gulpfile.js" -Path "$projectPath\theme" | Select-Object -ExpandProperty DirectoryName -Unique
if ($null -eq $ThemeFolder -Or $ThemeFolder -eq ""){
Write-Warning "SXA 10 Theme not found for directory: $projectPath"
Write-Warning "Exited Optimization for: $projectPath"
continue
}

[System.Collections.ArrayList]$ymlFolders=Get-ChildItem -Filter "pre-optimized-min.yml" -Path "$projectPath\serialization" -Recurse | Select-Object -ExpandProperty DirectoryName -Unique
if ($null -eq $ymlFolders -Or $ymlFolders.Count -eq 0){
Write-Warning "Destination YML files not found for directory: $projectPath"
Write-Warning "Exited Optimization for: $projectPath"
continue
}

Set-Location $ThemeFolder

npm install
sxa build SassStyles --debug
sxa build All --debug

$scriptFolder = $ymlFolders | Where-Object {$_.ToUpper().Contains("SCRIPTS") }
if($null -ne $scriptFolder -and $scriptFolder -ne ""){
try {
SetThemeYml -themeFolder $ThemeFolder -ymlFolder $scriptFolder -sourceFile 'scripts\pre-optimized-min.js'
}
catch {
Write-Warning "Script Optimization Failed"
Write-Warning $_.Exception.Message
}
}

$stylesFolder = $ymlFolders | Where-Object {$_.ToUpper().Contains("STYLES") }
if($null -ne $stylesFolder -and $stylesFolder -ne ""){
try {
SetThemeYml -themeFolder $ThemeFolder -ymlFolder $stylesFolder -sourceFile 'styles\pre-optimized-min.css'
}
catch {
Write-Warning "Style Optimization Failed"
Write-Warning $_.Exception.Message
}
}
Write-Host "`r`nFinished Optimization for: $projectPath"
}

Set-Location ${PSScriptRoot}

And now for the magic…. your deployment process doesn’t change. Assuming you already have release steps in place to sync Unicorn when you deploy your CM instance, this will automatically deploy your CSS and JS pre-optimized theme, nothing else to do!

Conclusion

This process may initially seem a bit daunting, but when you look at the implementation, its actually pretty simple and once it is setup it doesn’t take any effort to maintain. Its easy to apply to multiple projects and makes deploying the theme to QA/Stage and Production sites a breeze.

A big thanks to Ben Lipson for working out some of the implementation details around the Field filters.

  • Richard