Thoughts

Upgrading Blend Add-Ons for Optimizely CMS 13

Author

Bob Davidson

Categories:

Bob Davidson sitting at a table in his office.

After upgrading two Blend Optimizely add-ons to CMS 13 compatibility, Bob Davidson shares the three takeaways — including the new Application system, async content area filtering, and tag helper-based platform navigation.

If you’re maintaining an Optimizely add-on or custom package and CMS 13 is on your horizon, a few parts of the upgrade may catch you off guard.

We recently upgraded two of our internal Optimizely tools to CMS 13 compatibility: BLIT, our content import utility, and Blend.Optimizely, our collection of CMS utilities that we ship on every Optimizely project. And, while most of it was straightforward, three things are worth highlighting.

Blend.Optimizely

Blend.Optimizely is a collection of utilities and convenience extension methods for Optimizely CMS sites, installed by default on every Optimizely project we build at Blend. Currently, it's built to support only CMS 12, so we added CMS 13 support.

Overall, the required changes were relatively minimal. Most were either explicitly documented or easy enough to tease out with intellisense and a bit of intuition. The documentation was helpful, though not always correct or complete. I've submitted feedback on the items that were off.

Fetching the Start Page

Fetching the "start page" without context is a bit more involved now, which is not necessarily a bad thing. For the convenience methods within the scheduled job base class, I decided to remove this functionality entirely. Scheduled jobs making assumptions about the start page — which can be unpredictable when the job is run from different domains or from the scheduler — is a frequent source of bugs, so I require the caller to supply the start page reference themselves, forcing them to be explicit and hopefully consider *which* start page they actually want to use.

Content Area Filtering

One bigger change is that the FilteredItems property of a ContentArea is now deprecated. I believe the expectation is that rendering typically happens through tag helpers, where one would not need to filtered items, but I frequently need to access these items without going through the Optimizely rendering system. The documentation lists .Items as a possible API replacement, but those items are unfiltered. It also recommends using IEnumerable<IContentAreaItemsRenderingFilter> and manually filtering the items, which is what I ultimately did.

The catch is that the filtering method on IContentAreaItemsRenderingFilter is FilterAsync — which requires async and implies the filtering may be expensive and potentially involves calls to external services. This changed our API in a significant way, so I took the opportunity to break compatibility. I removed the GetContent method and introduced a few new methods:

- GetFilteredItemsAsync — fetches the list of filters from the service locator, applies the filtering, and returns only the filtered list, effectively recreating .FilteredItems, except as an async extension method.
- GetContentUnfiltered — uses the unfiltered .Items property and does not require async.
- GetContentAsync — recreates the original behavior of GetContent, except as a fully async method.

The HasValue extension method on a ContentArea had a similar change. HasValue was intended to check whether the content area is not null and has at least one item that isn't filtered out. Since filtering is now async, I've renamed this to HasValueUnfiltered, which checks whether the content area is not null and has any items at all. That's probably less useful in practice, and I chose not to implement a HasValueAsync variant — the concern being that HasValue-style methods are expected to be fast and cheap, and I have no guarantees about how filtering performs.

My recommendation then is to use GetFilteredItemsAsync or GetContentAsync first to retrieve the filtered list, then call .Any() on the result. A very common pattern is to check whether filtered items exist and then display them. Rather than calculate filtering twice, just do it once.

The New Application System

Internally, we've also switched over to the new Application system, which replaces the deprecated SiteDefinition. In this new system, IApplicationResolver resolves an Application from context. That application will be either a Website or InProcessWebsite — both have some shared properties, but those properties aren't on a shared base type or interface. We ended up using this switch expression pattern as a workaround in several places:

private static ContentReference? GetEntryPoint()
{
    var application = applicationResolver.Service.GetByContext();
    return application switch
    {
        Website website => website.EntryPoint,
        InProcessWebsite inProcessWebsite => inProcessWebsite.EntryPoint,
        _ => null
    };
}

Obsolete Methods

Since this is a major update and includes breaking changes, I also took the opportunity to remove a few obsolete methods — including the GetFriendlyUrl family. Use the ResolveUrl methods instead.

BLIT

Bend's Little Import Tool, or BLIT is our utility for improving the content migration process in Optimizely CMS. Rather than the fragile, one-off import jobs that are common in migration projects, BLIT uses a structured file format that makes imports repeatable, handles circular content references gracefully, and doesn't require hardcoded IDs or a deployment to fix a mistake.

Having already worked through most of the same changes in Blend.Optimizely, the BLIT upgrade was relatively quick and painless — with one notable exception: the platform navigation integration.

In CMS 12, we used HTML helper methods:


    Html.CreatePlatformNavigationMenu()

    <div @Html.ApplyPlatformNavigation()>
        …
    </div>

In CMS 13, these have been replaced with tag helpers:


    <platform-navigation />

    <platform-navigation-wrapper>
        …
    </platform-navigation-wrapper>

At the time of writing, the official documentation was incorrect on this point — feedback has been submitted.

Conclusion

For these upgrades, the three things most significant things to understand were:

- The new Application system, and the two types (Website and InProcessWebsite) that can be returned from it
- The new content area filtering system and the async implications of IContentAreaItemsRenderingFilter
- The new tag helper-based approach to platform navigation UI integration

None of these are insurmountable — but hopefully this post will save you some head-scratching.