World is now on Opti ID! Learn more

dada
Jun 26, 2025
  1
(0 votes)

Make Global Assets Site- and Language-Aware at Indexing Time

I had a support case the other day with a question around search on global assets on a multisite. This is the result of that investigation.
This code is provided as-is but please let me know if you have any feedback.

In Episerver (Optimizely) CMS, global assets—such as images, documents, and blocks stored in the Global Assets folder—are not tied to a specific site or language. 
Yet, the content that references typically is.

You might have noticed a common issue:
When implementing a search on Site A, assets referenced only by content on Site B still appear in the results and vice versa. That’s maybe not what we want .

Let’s say you have:

  • Assets used exclusively on Site A, or

  • Assets shared between Site A and Site B.

When a user searches on Site A, you want results that are relevant only to that site—including global assets referenced by that site’s content. Likewise, if an asset is shared between both sites, it should appear in both result sets.

To support this, your indexing process must make global assets aware of which sites and languages reference them.

This Is the Way

If you're using UnifiedSearch, this filtering on Site ID and language typically works out of the box via:

* FilterOnCurrentSite() / FilterOnSite()
* FilterForVisitor() / PublishedInLanguage()

And for typed searches, you can easily apply these filters yourself.

Update the Initialization Module

To get this working for global assets, we override the default indexing behavior for SiteId() and PublishedInLanguage() by excluding the built-in fields and replacing them with our own logic.

SearchClient.Instance.Conventions.ForInstancesOf<IContent>().ExcludeField(x => x.SiteId());
SearchClient.Instance.Conventions.ForInstancesOf<IContent>().IncludeField(x => x.SiteId(true));
SearchClient.Instance.Conventions.ForInstancesOf<IContent>().ExcludeField(x => x.PublishedInLanguage());
SearchClient.Instance.Conventions.ForInstancesOf<IContent>().IncludeField(x => x.PublishedInLanguage(true));

Extension Methods

We define custom logic in a static class to determine which sites and languages reference a given global asset. 

using EPiServer.Find.Cms;
using EPiServer.Framework.Cache;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using EPiServer.Find.Helpers;

public static class GlobalAssetsExtensions
{
    private static readonly Lazy<ISiteDefinitionResolver> _siteDefinitionResolver =
        new(() => ServiceLocator.Current.GetInstance<ISiteDefinitionResolver>());

    private static readonly Lazy<IContentLoader> _contentLoader =
        new(() => ServiceLocator.Current.GetInstance<IContentLoader>());

    private static readonly Lazy<IContentRepository> _contentRepository =
        new(() => ServiceLocator.Current.GetInstance<IContentRepository>());

    private static readonly Lazy<IContentVersionRepository> _versionContentRepository =
        new(() => ServiceLocator.Current.GetInstance<IContentVersionRepository>());

    private static readonly Lazy<IObjectInstanceCache> _objectCache =
        new(() => ServiceLocator.Current.GetInstance<IObjectInstanceCache>());

    private static readonly Lazy<IContentCacheKeyCreator> _contentCacheKeyCreator =
        new(() => ServiceLocator.Current.GetInstance<IContentCacheKeyCreator>());

    public static IEnumerable<string> SiteId(this IContentMedia content, bool foobar)
    {        
        content.ValidateNotNullArgument(nameof(content));

        var siteDefinitionResolver = _siteDefinitionResolver.Value;
        var contentLoader = _contentLoader.Value;
        var contentRepository = _contentRepository.Value;
        var objectCache = _objectCache.Value;
        var contentCacheKeyCreator = _contentCacheKeyCreator.Value;

        if (siteDefinitionResolver.GetByContent(content.ContentLink, false) == null)
        {
            if (ContentReference.IsNullOrEmpty(content.ParentLink) ||
                !contentLoader.TryGet<IContent>(content.ParentLink, out _))
            {
                return Enumerable.Empty<string>();
            }

            if (contentLoader.GetAncestors(content.ContentLink)
                             .Any(x => x.ContentLink == ContentReference.GlobalBlockFolder))
            {
                var cacheKey = $"SiteIds:{content.ContentLink.ID}";
                var cachedSiteIds = objectCache.Get(cacheKey) as IEnumerable<string>;
                if (cachedSiteIds != null)
                {
                    return cachedSiteIds;
                }

                var contentRefs = new HashSet<ContentReference>();

                foreach (var link in contentRepository.GetReferencesToContent(content.ContentLink, false))
                {
                    if (!ContentReference.IsNullOrEmpty(link.OwnerID))
                    {
                        contentRefs.Add(link.OwnerID);
                    }
                }

                var siteIds = new HashSet<string>();
                foreach (var contentRef in contentRefs)
                {
                    var site = siteDefinitionResolver.GetByContent(contentRef, false);
                    if (site != null)
                    {
                        siteIds.Add(site.Id.ToString());
                    }
                }

                objectCache.Insert(cacheKey, siteIds, new CacheEvictionPolicy(
                    TimeSpan.FromMinutes(120), CacheTimeoutType.Sliding,
                    [contentCacheKeyCreator.VersionKey]));

                return siteIds;
            }
        }

        return [content.SiteId()];
    }

    public static Dictionary<string, LanguagePublicationStatus> PublishedInLanguage(this IContentMedia content, bool foobar)
    {
        content.ValidateNotNullArgument(nameof(content));

        if (ContentReference.IsNullOrEmpty(content?.ContentLink))
        {
            return null;
        }

        var contentLoader = _contentLoader.Value;
        var contentRepository = _contentRepository.Value;
        var versionRepository = _versionContentRepository.Value;
        var objectCache = _objectCache.Value;
        var contentCacheKeyCreator = _contentCacheKeyCreator.Value;

        if (contentLoader.GetAncestors(content.ContentLink)
                         .Any(x => x.ContentLink == ContentReference.GlobalBlockFolder))
        {
            // Should we need to check whether entire ancestor chain are resolvable?

            var cacheKey = $"Languages:{content.ContentLink.ID}";
            var cachedLanguages = objectCache.Get(cacheKey) as Dictionary<string, LanguagePublicationStatus>;
            if (cachedLanguages != null)
            {
                return cachedLanguages;
            }

            var contentRefs = new HashSet<ContentReference>();

            foreach (var link in contentRepository.GetReferencesToContent(content.ContentLink, false))
            {
                if (!ContentReference.IsNullOrEmpty(link.OwnerID))
                {
                    contentRefs.Add(link.OwnerID);
                }
            }

            var languages = new Dictionary<string, LanguagePublicationStatus>(StringComparer.OrdinalIgnoreCase);

            foreach (var contentRef in contentRefs)
            {
                var publishedLanguages = versionRepository
                    .ListPublished(contentRef)
                    .Select(x => x.LanguageBranch);
                
                foreach (var lang in publishedLanguages)
                {
                    if (contentLoader.TryGet<IContent>(contentRef, new LanguageSelector(lang), out var contentInLang))
                    {
                        var langStatus = contentInLang.PublishedInLanguage();
                        if (langStatus != null)
                        {
                            foreach (var kvp in langStatus)
                            {
                                languages.TryAdd(kvp.Key, kvp.Value);
                            }
                        }
                    }
                }
            }

            objectCache.Insert(cacheKey, languages, new CacheEvictionPolicy(
                TimeSpan.FromMinutes(120), CacheTimeoutType.Sliding,
                [contentCacheKeyCreator.VersionKey]));

            return languages;
        }

        return content.PublishedInLanguage();
    }
}

What It Looks Like in the Index

After reindexing a global asset referenced across different sites and languages, it will now include enriched metadata, like this

"PublishedInLanguage": { "sv": { "StartPublish$$date": "2017-02-22T11:24:00Z" },
 "fi": { "StartPublish$$date": "2025-06-18T11:03:00Z" },
 "en": { "StartPublish$$date": "2012-10-04T11:53:00Z" },
 "en-GB": { "StartPublish$$date": "2025-05-21T06:24:00Z" } }, 
"SiteId": [ "site-a-guid", "site-b-guid" ]
Jun 26, 2025

Comments

Please login to comment.
Latest blogs
The remote server returned an error: (400) Bad Request – when configuring Azure Storage for an older Optimizely CMS site

How to fix a strange issue that occurred when I moved editor-uploaded files for some old Optimizely CMS 11 solutions to Azure Storage.

Tomas Hensrud Gulla | Jun 26, 2025 |

Enable Opal AI for your Optimizely products

Learn how to enable Opal AI, and meet your infinite workforce.

Tomas Hensrud Gulla | Jun 25, 2025 |

Deploying to Optimizely Frontend Hosting: A Practical Guide

Optimizely Frontend Hosting is a cloud-based solution for deploying headless frontend applications - currently supporting only Next.js projects. It...

Szymon Uryga | Jun 25, 2025

World on Opti ID

We're excited to announce that world.optimizely.com is now integrated with Opti ID! What does this mean for you? New Users:  You can now log in wit...

Patrick Lam | Jun 22, 2025

Avoid Scandinavian Letters in File Names in Optimizely CMS

Discover how Scandinavian letters in file names can break media in Optimizely CMS—and learn a simple code fix to automatically sanitize uploads for...

Henning Sjørbotten | Jun 19, 2025 |