Take the community feedback survey now.

dada
Jun 4, 2025
  434
(2 votes)

Searching Breadcrumbs and Content and their Assets

Recently, I handled a couple of support cases that revolved around two challenges in a Search & Navigation.

  1. Rendering breadcrumbs for search results, and

  2. Searching below a specific node and including content and content assets.

Neither problem was rocket science, but after digging in, testing, and writing some code, we ended up with ideas worth sharing. Hopefully, they’ll save you some time if you’re tackling something similar.

Case #1: Searching Below a Specific Node (And Including Assets)

Let’s say you're searching under a specific node (e.g. a section or page on your site). You want to include both pages and content assets (like PDFs, images, etc.) stored below this node.

You might start with something like:

var result = SearchClient.Instance.UnifiedSearch()
    .For(q)
    .Filter(x => ((IContent)x).Ancestors().Match(node_to_search_below))
    .GetResult();

This works perfectly for content pages, but not for content assets. Why?

🧠 Why Assets Are Missing

Assets are stored in a different structure than pages. Their Ancestors() path does not include the node you're filtering on. This is because assets like documents are typically stored in Asset Folders, which are often detached from your actual content hierarchy.


✅ Solution: Index Owner Ancestors for Assets

We can solve this by populating a virtual field with the ancestors of the owning content for each asset. That way, assets "inherit" the path of their parent content at index time.

Step 1: Extend MediaData With Owner Ancestors

In your Find initialization module:

SearchClient.Instance.Conventions.ForInstancesOf<MediaData>()
    .IncludeField(x => x.ContentAncestors());

Then, add this helper extension method:

public static IEnumerable<string> ContentAncestors(this MediaData content)
{
    var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
    var contentAssetHelper = ServiceLocator.Current.GetInstance<ContentAssetHelper>();

    var assetOwner = contentAssetHelper.GetAssetOwner(content.ContentLink);
    if (assetOwner != null)
    {
        var assetContent = contentRepository.Get<IContent>(assetOwner.ContentLink) as PageData;
        return assetContent?.Ancestors();
    }

    return null;
}

This code retrieves the owning page of the asset and gets its Ancestors() so we can treat the asset as if it were part of the same structure.


Step 2: Update Your Search Query

Now, combine the original ancestor filter with your new one:

var result = SearchClient.Instance.UnifiedSearch()
    .For(q)
    .Filter(x => ((IContent)x).Ancestors().Match(node_to_search_below)
             || ((MediaData)x).ContentAncestors().Match(node_to_search_below))
    .GetResult();

Voilà — you now get results for both pages and their related assets.


Case #2: Generating Breadcrumbs from Search Results

Breadcrumbs are often generated based on Ancestors(). While it’s possible to resolve these at render time, a more efficient approach is to index ancestor names so they’re available immediately from the search index.

✅ Solution: Index Ancestor Names

In your Find initialization code:

SearchClient.Instance.Conventions.ForInstancesOf<IContent>()
    .IncludeField(x => x.AncestorNames());

Then define the extension like this:

public static IEnumerable<string> AncestorNames(this IContent content)
{
    var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
    var ancestorNames = new List<string>();

    foreach (var ancestorContentRef in content.Ancestors())
    {
        var ancestorPage = contentRepository.Get<IContent>(new ContentReference(ancestorContentRef)) as PageData;
        if (ancestorPage != null)
        {
            ancestorNames.Add(ancestorPage.Name);
        }
    }

    return ancestorNames;
}

This way, your search results come with a breadcrumb trail like:

"AncestorNames": ["Start", "About Us", "Team"]

You can use this directly to render navigation or path-based UX without additional lookups.

 

 

Jun 04, 2025

Comments

Stefan Holm Olsen
Stefan Holm Olsen Jun 8, 2025 03:44 AM

Hi Daniel

Can you elaborate on the difference between AncestorNames with/without a "$$string" suffix?

I know that when returning List from an indexing helper the field name does not get such a suffix. But with an IEnumerable it does. But what is the difference in the index?

dada
dada Jun 14, 2025 09:45 AM

Hi Stefan

I have seen this as well.

It looks like when AncestorName is defined as a string or a list of strings, it gets suffixed with $$string. However, when I include it using .IncludeField in conventions, it appears without the suffix.

I've reported this to Engineering, as it's not what I expected. I don’t see an immediate issue from a functional perspective, but it’s probably unnecessary to have both mappings.

Please login to comment.
Latest blogs
Quiet Performance Wins: Scheduled Job for SQL Index Maintenance in Optimizely

As Optimizely CMS projects grow, it’s not uncommon to introduce custom tables—whether for integrations, caching, or specialized business logic. But...

Stanisław Szołkowski | Oct 8, 2025 |

Image Generation with Gemini 2.5 Flash

Gemini 2.5 Flash Image, nicknamed Nano Banana, is Google DeepMind’s newest image generation & editing model. It blends text‑to‑image, multi‑image...

Luc Gosso (MVP) | Oct 8, 2025 |

Automated Page Audit for Large Content Sites in Optimizely

Large content sites often face significant challenges in maintaining visibility and control over thousands of pages. Content managers struggle to...

Sanjay Kumar | Oct 6, 2025

Optimizely CMS Roadmap – AI, automation and the future of digital experiences

A summary of the roadmap for Optimizely CMS from the Opticon conference on September 30, 2025.

Tomas Hensrud Gulla | Oct 6, 2025 |