In Sitecore XM Cloud you organize media assets per site in the Media Library. You do that in your Media item under the Headless Site. SXA provides helpful tokens like $siteMedia, but they resolve to virtual media folders under the content tree, which breaks when you want to append subfolders (e.g. $siteMedia/Documents/*).
I recently needed to solve this problem in a multisite solution: allow editors to select media items (like PDFs) from a per-site folder in the Media Library using a field source like:
query:$siteMediaRoot/Documents/*
The Problem
Using query:$siteMedia/Documents/* in a field source results in Sitecore falling back to the entire Media Library, or showing no items. That’s because $siteMedia resolves to a virtual folder, and appending paths like /Documents doesn’t work.
I needed a token that:
- Resolves to the actual media library root for the current SXA site (not the virtual one)
- Supports appending subfolders like
/Documents - Works across multiple sites with a shared component
Custom $siteMediaRoot Token
I created a custom SXA token called $siteMediaRoot by adding a processor to the resolveTokens pipeline. This token resolves to the real media folder path (e.g. /sitecore/media library/SiteA) so I can safely use subfolders. This code requires that there is a folder under Media library that has the same name as the Site. Then you just reference that to the Media item under your site.
Token Processor Code
public class ResolveSiteMediaRootToken : ResolveTokensProcessor
{
private const string Token = "$siteMediaRoot";
private readonly IMultisiteContext _multisiteContext;
public ResolveMediaSiteRootToken() : this(ServiceLocator.ServiceProvider.GetService<IMultisiteContext>()) {}
public ResolveSiteMediaRootToken(IMultisiteContext multisiteContext)
{
_multisiteContext = multisiteContext;
}
public override void Process(ResolveTokensArgs args)
{
if (!args.Query.Contains(Token)) return;
var mediaRootItem = GetSiteMediaRoot(args.ContextItem);
if (mediaRootItem == null) return;
args.Query = ReplaceTokenWithItemPath(args.Query, Token, () => mediaRootItem, args.EscapeSpaces);
}
private Item GetSiteMediaRoot(Item contextItem)
{
var virtualRoot = _multisiteContext.GetSiteMediaItem(contextItem);
if (virtualRoot == null) return null;
var siteItem = _multisiteContext.GetSiteItem(contextItem);
var siteName = siteItem?.Name;
if (string.IsNullOrEmpty(siteName)) return virtualRoot;
var matched = virtualRoot.GetVirtualChildren()
.FirstOrDefault(c => c.Name.Equals(siteName, StringComparison.OrdinalIgnoreCase));
return matched ?? virtualRoot;
}
}
Pipeline Patch
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<resolveTokens>
<processor type="YourNamespace.ResolveMediaSiteRootToken, YourAssembly" patch:before="processor[@type='Sitecore.XA.Foundation.TokenResolution.Pipelines.ResolveTokens.EscapeQueryTokens, Sitecore.XA.Foundation.TokenResolution']" />
</resolveTokens>
</pipelines>
</sitecore>
</configuration>
NuGet Setup
In your root packages.props (or Directory.Packages.props), add:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PlatformVersion>1.*</PlatformVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Update="Sitecore.XmCloud.XA.Foundation.Multisite" Version="$(PlatformVersion)" />
<PackageReference Update="Sitecore.XmCloud.XA.Foundation.TokenResolution" Version="$(PlatformVersion)" />
</ItemGroup>
</Project>
Then in your .csproj file:
<PackageReference Include="Sitecore.XmCloud.XA.Foundation.Multisite" />
<PackageReference Include="Sitecore.XmCloud.XA.Foundation.TokenResolution" />
Final Usage
Now I can use the following in a Multilist or Treelist field source:
query:$mediaSiteRoot/Documents/*
And it dynamically resolves to:
/sitecore/media library/SiteA/Documents/*