Creating a Custom SXA Media Token in Sitecore XM Cloud

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/*

Sitecore 10.4.1 – What Developers Should Know

Sitecore 10.4.1 is out. At first it looks like a small update, but there are several things that can impact your setup as a developer. Especially if you’re working with containers, Solr, Identity Server or Azure monitoring. Here’s a quick overview of the most important changes and a few thoughts from my own point of view.

Solr 9.8.1 is now required

This update switches to Solr 9.8.1. The older 8.11.2 version is no longer supported, so you will need to upgrade. With the new Solr version you get a big leap in Security and performance fixes from the older version. I would say that’s the most important parts. Just modernising your Solr foundation.

If you’re using Sitecore containers, you also need to use the new container deployment package that matches the new Solr version. The older container images will stop getting OS patches, so best to move forward.

Identity Server now uses .NET 8

The Identity Server in Sitecore is now version 8.0.16 and built on .NET 8. That’s a good step forward, but it does come with two changes you need to handle:

  • Encrypted SQL connections are required now.
  • You have to run a database upgrade script.

So if you’re automating your deployments or have custom Identity Server config, be ready to update both connection strings and database setup.

Azure Application Insights changes

If you’re using Application Insights for monitoring, the old Instrumentation Key is no longer supported. You have to use a connection string instead. This is a Microsoft change and Sitecore follows it now. There are updated ARM templates included if you use Azure to set it up.

Lots of resolved issues from previous version

See all resolved issues here

Sitecore 10.4.1 brings some important backend changes. For most environments, it’s worth upgrading to keep things supported and future proof, just make sure to test and read the updated install instructions carefully.

Here’s the link to the new version: https://developers.sitecore.com/downloads/Sitecore_Experience_Platform/104/Sitecore_Experience_Platform_104_Update1

Custom Roles Setup in Sitecore XM Cloud

Overview

If you have multiple sites under a Site Collection and want to manage access per site or across the entire collection, it’s best to create custom roles. This gives you flexibility, for example, some editors can be Authors for the whole Site Collection, while others only have access to a specific Site.

If you only have one site or don’t need granular access, you can use the default Site Collection roles and Site roles in Sitecore. In that case, simply run the Setup Security script on your Site Collection and then on the Site.

Recommended Role Structure

Create custom roles before running Setup Security.
Preferably create 4 roles per Site Collection and 4 per Site. Use the Sitecore domain.

Site Collection Roles:
sitecore/MySiteCollection Admin
sitecore/MySiteCollection Author
sitecore/MySiteCollection Designer
sitecore/MySiteCollection Member

Site Roles:
sitecore/MySite Admin
sitecore/MySite Admin
sitecore/MySite Admin
sitecore/MySite Admin

Create your new custom roles for a new Site by creating 4 new roles.
Create new roles in the Sitecore domain. Follow this pattern:

For the headless Site Collection, you create 4 new roles for that Site Collection aswell.

Role Membership Setup

In Role Manager, select the <Site> Author role, then click Member Of. Make it a member of:

  • sitecore\<Site> Member (itself)
  • sitecore\<SiteCollection> Member

You can add additional Sitecore roles (e.g., Sitecore Client Securing) if needed.
Read more about Sitecore roles

Running the Setup Security Script

  1. In Content Editor, right-click your Site Collection and run Scripts > Setup Security.

2. Choose sitecore as domain

3. In the dialog, link the 4 custom roles to the appropriate fields before clicking Assign.

4. Then repeat for the Site.

You can now assign users to your new Author role and they should be ready for editing your site. Don’t forget to setup Workflows to make the editor experience as expected in Page builder!

Notes

If you only have 1 site or don’t see a reason to split the roles up in any way, there are default Site Collection roles and Site roles predefined in Sitecore that you could use instead and just run the Setup Security script on your Site Collection and Site. But for granular access rights I recommend setting up site specific roles.

Discover the Latest Updates in Sitecore XM Cloud

The latest release of Sitecore XM Cloud introduces several exciting new features designed to enhance content management and optimization. Here’s a summary of the key updates:

Content authoring improvements

1. Direct Editing: You can now edit all written content on your page directly within the Page Builder. This includes content fields associated with the page itself, such as metadata, which can be edited in the Page Content pane. Maybe you have a Multilist Tag template on your page, then you can do those things from Page Editor now, which is great!


2. Component Editing: Edit content item fields included in a component by selecting the component and using the Content tab in the right-hand panel. Before we only had the Design tab. Useful for changing component data fields without going to Content Editor if it’s fields that aren’t visible or supported.

Enhanced Field Editing: The Page Builder now supports editing various field types, including File, Droplist, Droplink, Droptree, Taglist, Checklist, Multilist, Treelist, Multiroot Treelist, and Checkbox. General link fields also support email links. This has been a long requested feature as we want to move editors away from Content Editor and just use Page Editor as much as possible.

Automatic Saving: All content edits are automatically saved and immediately displayed. Undo and Redo buttons are available for as long as you remain on the edited page.

Sitecore Stream Integrations

This new Sitecore product surely deserves its own blog post. Sitecore’s new AI product is designed for marketers to boost productivity and assist with on-brand campaign ideas.

In XM Cloud, with a Sitecore Stream subscription, marketers can set up brand kits. These kits allow them to define rules for the AI, including

  • Logo
  • Visual Guidelines
  • Global Goals
  • Brand Context
  • Dos and Don’ts
  • Tone of Voice
  • Checklist
  • Grammar Guidelines
  • Image Style

These rules then enable the AI to help editors ensure that new content adheres to the brand guidelines. For example, the AI can generate a text prompt using the defined rules, creating a draft that editors can accept, regenerate, or modify.

https://doc.sitecore.com/xmc/en/users/xm-cloud/sitecore-stream-in-xm-cloud.html

You can use Sitecore Stream to help with A/b testing and personalize pages for visitors in XM Cloud.

To enable Sitecore Stream with Rich Text Editor(RTE) in Page Editor you need to upgrade to the new RTE.
https://developers.sitecore.com/changelog/xm-cloud/13112024/the-legacy-rich-text-editor-in-pages-will-be-deprecated-on-may-8th%2c-2025

Receiving the Sitecore MVP Award 2025

I’m incredibly honored to receive the Sitecore Technology MVP Award for 2025! This is my second time receiving this recognition, yet it feels just as exciting and rewarding as the first.

Over the past year, I’ve had the privilege of meeting many amazing people in the Sitecore community and giving back through the mentorship program, an experience that has been truly fulfilling.

2024 was also a year of deepening my involvement with Sitecore’s evolving product ecosystem, particularly OrderCloud, Search, and XM Cloud. Exploring these innovations has given me valuable insights into how Sitecore continues to lead the way in composable digital experiences.

A huge thank you to the Sitecore MVP team for this recognition and for all the hard work you do to support the community.

Looking forward to another great year of learning, collaboration, and connecting with you all! 😊

Sitecore JSS tracking CORS origin errors

We’re running a vercel site with Sitecore 10.3 and JSS. I was getting errors about CORS origin like this:

Access to XMLHttpRequest at ‘https://{host}/sitecore/api/jss/track/event?sc_apikey={key}’ from origin ‘https://{host}’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: It does not have HTTP ok status

So the JSS tracking wasn’t working. Started to dig in the Sitecore documentation on the tracking API and CORS origin but gave no answer to the problem:
https://doc.sitecore.com/xp/en/developers/hd/20/sitecore-headless-development/jss-tracking-api.html

https://doc.sitecore.com/xp/en/developers/104/sitecore-experience-manager/cross-origin-resource-sharing–cors-.html

Finally I found that the error was thrown because Sitecore.JSS.TrackerServiceEnabled is disabled by default. By setting it to true with a patch config the error disappeared.

<configuration>
  <sitecore>
    <settings>
      <setting name="Sitecore.JSS.TrackerServiceEnabled" value="true" />
    </settings>
  </sitecore>
</configuration>

Event-driven With Webhooks And Message Senders In Sitecore OrderCloud

Webhooks are user-defined HTTP callbacks that enable real-time communication and interaction between OrderCloud and external systems or services. They help automate processes, validate and synchronize data, enhancing the efficiency and responsiveness of e-commerce operations.

There are two types of webhooks in OrderCloud. Pre-hook and Post-hook.

Pre-hook will happen before OrderCloud execute the endpoint given and Post-hook will execute after the given endpoint. Webhooks in Ordercloud only works with write API endpoints (POST, PUT, PATCH, DELETE).

Webhooks in Sitecore OrderCloud can be used for many different purposes like:

  1. Inventory management: Webhooks can be used to automatically adjust inventory levels in external systems when an order is placed or updated. This ensures accurate inventory tracking and helps prevent stock discrepancies.
  2. Validation: Pre-hook webhooks can be used to validate incoming requests, such as payment information or customer details, before processing an order. You can also use it to validate shipping addresses using external address validation services or other checkout information before an order is created.
  3. Data synchronization and third-party integrations: If you need to update or send data to your CRM, ERP or other platforms you can also use webhooks to send that data.
  4. Automation: Webhooks can be used to automate various e-commerce processes, such as generating invoices, updating shipping statuses, or sending marketing emails based on user behavior.

Webhooks in Sitecore OrderCloud play a crucial role in making your e-commerce solution a success by improving the efficiency and experience.

How to Setup a Webhook

There is a very well documented guide on how to setup webhooks here: https://ordercloud.io/knowledge-base/using-webhooks

But let’s take a look anyway. Go into portal.ordercloud.io, API console and select your marketplace.

Go to Webhooks and create a new Webhook

In the new interface setup a name, secret, payload url, etc.

Select the trigger events you want to trigger the webhook. For example when a order is submitted and you want to send data to another system. You can set the order submit as a trigger event. And check the pre-hook if you want it to execute something before OrderCloud.

Message Senders

Message Senders are a feature designed to deliver event-driven notifications to users based on activities such as order submission, shipment, password resets, and more.

In comparison with Webhooks, which are also event-driven, Message Senders are more suitable for notification tasks because they automatically retrieve all the relevant information and send a web request for each message that should be sent. This simplifies the process compared to the steps you’d have to take with webhooks, which involve writing code for several more steps.

OrderCloud provides different types of Message Senders, each corresponding to a different commerce activity. Users must be active to receive messages, meaning not only that their personal accounts are active but also that the buyer or supplier organization they belong to is active.

There are three different configuration options for Message Senders: Default Sender, Custom Mandrill Account, and Custom URL. Each option provides a different level of control:

  1. Default Sender: The easiest, no-code option. Uses OrderCloud’s Mandrill account and templates, providing minimal control over the email templates or the data sent.
  2. Custom Mandrill Account: A low-code option that allows you to provide your own custom email templates. The actual sending of emails is still handled by OrderCloud.
  3. Custom URL: The most flexible option that requires more work. You provide OrderCloud with the URL to your publicly available endpoint, which will receive the event payload and is responsible for formatting and sending the notification.

The system also provides a list of variables (Mandrill Variables) that you can use in your custom email templates if you’re using the Mandrill/Mailchimp integration.

Sitecore ContentSearch – Solr using Facets

Search header, magnifying glas

Facets are used with the ContentSearch API to enable filtering and grouping of search results based on specific fields or properties. Facets help users to narrow down search results based on their preferences or criteria. Here’s a guide on how to use facets with ContentSearch in Sitecore

Define the facet field in your search index configuration:

First, ensure that the field you want to use as a facet is included in your search index configuration. For example, if you want to use the “Manufacturer” field as a facet, you should have it defined in your search index configuration.

Create a model class for your search result items:

Create a model class that inherits from SearchResultItem and includes the properties you want to use as facets. I would recommend putting the IndexField name in a constant file to reuse everywhere in your solution. Because if it’s every changed you only need to change on 1 place and it won’t break the code. For example:

public class CustomSearchResultItem : SearchResultItem
{
[IndexField("manufacturer")] //Or use a constant in your foundation project
public string Manufacturer{ get; set; }
}

Perform a faceted search:

To perform a faceted search, use the .FacetOn() method in your search query. This method tells the search provider to calculate the count of items for each unique value in the specified field.

Here’s an example of performing a faceted search on the “manufacturer” field:

using (var context = ContentSearchManager.GetIndex("sitecore_web_index").CreateSearchContext())
{
// Create the base search query
IQueryable<CustomSearchResultItem> query = context.GetQueryable<CustomSearchResultItem>()
.Where(item => item.TemplateName == "YourTemplateName");

// Add the facet to the query
query = query.FacetOn(item => item.Manufacturer);

// Execute the search and get the facets
var searchResults = query.GetResults();
var manufacturerFacetResults = searchResults.Facets.Categories.FirstOrDefault(x => x.Name == "manufacturer");
}

Process the facet results:

In the example above, the manufacturerFacetResults variable contains the facet results for the “manufacturer” field. You can now process these results to display the facet options to the users or apply further filtering based on user input.

Here’s an example of processing the facet results and displaying them as a list of options:

if (manufacturerFacetResults != null)
{
foreach (var facetValue in manufacturerFacetResults.Values)
{
string manufacturer = facetValue.Name;
int count = facetValue.AggregateCount;
}
}

Apply facet filters to the search query:

Based on user input, you can apply facet filters to the search query to narrow down the search results. For example, if a user selects a specific manufacturer from the facet options, you can add a filter to the search query:

string selectedManufacturer = "example";

using (var context = ContentSearchManager.GetIndex("sitecore_web_index").CreateSearchContext())
{
IQueryable<CustomSearchResultItem> query = context.GetQueryable<CustomSearchResultItem>()
.Where(item => item.TemplateName == "YourTemplateName")
.Where(item => item.Manufacturer == selectedManufacturer)
.FacetOn(item => item.Manufacturer);

var searchResults = query.GetResults();
}

I hope you found this helpful. 🙂

Sitecore MVP 2024

I began my Sitecore journey in 2016, never imagining the depth of engagement and connection I would
find within a professional community.
Today, I am honored to stand among my peers with my first MVP title, deeply grateful for
the support and rich collaboration that defines our community.

Over the years, I’ve witnessed Sitecore evolve, expanding its product suite to groundbreaking
solutions that are shaping the future of digital innovation.
The growth of Sitecore’s offerings has mirrored my own journey of professional development,
continually inspiring me to explore, innovate, and contribute.

Receiving the MVP title is not just a recognition of my contributions;
it’s a powerful reminder of the strength found in collaboration and the endless
possibilities that lie ahead. With Sitecore and this incredible community,
I’ve never felt more confident in our collective path forward. Together, we are not just
adapting to the digital future—we are creating it.

Introducing Product Bundles in Sitecore OrderCloud

Sitecore OrderCloud is back with another great feature that has been highly anticipated – Product Bundles. It was recently released with API version 1.0.308. Aiming to provide ecommerce businesses with more flexibility, Product Bundles cater to a wide range of use cases to enhance customer experience.

Use case

A few examples on why it’s useful.

  • Suppose you’re buying a GoPro camera. There are probably hundreds of accessories for GoPro alone, and with Product Bundles you can setup and sell for instance “GoPro starter kit” or “GoPro battery kit”.
  • If you’re selling event tickets, you can also create a bundle like the “ticket + dinner package”.

The possibilities are only restricted by your imagination. This feature is an excellent opportunity for cross-selling. If customers typically purchase Product A alongside Product B, why not combine them into a bundle and offer a modest discount? It’s a common commerce feature, and it’s exciting to see it integrated into OrderCloud.

Bundles aren’t intended to replace individual products. Instead you create a bundle and assign specific products with it. New endpoints have been introduced regarding bundles, price schedules, catalogs etc. Let’s have look.

New endpoints

There are new /bundles endpoints under “Product Catalogs“, if you’re using the OrderCloud Portal. For a deeper overview, you can read up on all the endpoints at OrderClouds API reference for bundles.

Bundles follow the same visibility rules as products. They need to be active, present in a catalog, etc. Additionally, all products within a bundle must be visible to the user.

If you encounter any visibility-related challenges, OrderCloud has more information available at their site: https://ordercloud.io/knowledge-base/product-visibility#visibility-rules-checklist

Creating and assigning bundles

1. Use either the OrderCloud portal or make a POST /bundles with the below information.

2. Create a catalog bundle assignment with POST /catalogs/bundleassignment or in the portal as below.

3. Create a bundle assignment for a buyer if you wish. POST /bundles/assignments

4. Now we can see our bundle is added to our list of products in our catalog if we make a request to /products?catalogID=my-catalog

Note the new property IsBundle. This makes it easy for you to customize your commerce experience by identifying your bundles.

Assign products to a bundle

1. Use POST /bundles/productassignments. to assign products to a bundle. I’ve created 3 products for my “GoPro starter kit”, and now i’m assigning them using this endpoint.

  • GoPro
  • GoPro sleeve
  • GoPro tripod

If you want to automatically add these items to your cart when adding the bundle to your cart you need to set the Required field to true and set a Default Quantity.
You can also pass them in to the cart as normal lineitems. In this case I prefer that this bundle add the product automatically and with a specific quantity so I set those fields.

Price schedules

When creating a new price schedule, a “Bundle Price” option is now available. Once this price schedule is assigned to a product within a bundle and the bundle is added to the cart, the product will reflect the price set in that field.

Results

Regular product searches remain unchanged. However, you can now utilize the new filter BundleID={id} to exclusively retrieve products linked to a specific bundle, given correct catalog and price schedule assignments.

GET /me/products?BundleID=xF3IYMEQGUWPskFy4Jq6bw

To add the bundle to your cart, use POST /cart/bundles/{bundleID}

Either execute a GET /cart or GET /cart/lineitems to access cart information. When fetching the line items we can see there are 4 line items: 1 is our bundle, and 3 are the products assigned to that bundle, each priced at their Bundle Price.

Notes

New Roles have been established, including BundleReader, BundleAdmin, and BundleAssignmentAdmin.

As mentioned, this was a highly wished for feature. The OrderCloud team is working on expanding the possibilites with Product Bundles. Coming soon are integrations for custom validation and price overrides.
The just released new endpoints that supports adding bundles to subscriptions, or recurring bundles!

A noteworthy mention: Ensure the bundle xp schemas align with product xp schemas to avoid indexing issues.

With Product Bundles, Sitecore OrderCloud continues its journey in providing robust features, empowering businesses to tailor unique ecommerce solutions that resonate with their customers. Happy bundling! 🛍️🎉

Read more on OrderCloud website:
https://ordercloud.io/knowledge-base/product-bundles