Binh Nguyen Thi
Nov 15, 2025
  714
(0 votes)

Full implementation - Fallback languages with Optimizely Graph

Nowadays, many people choose a headless approach when developing Optimizely CMS/Commerce projects using Opti Graph.
One challenge we may face is implementing language fallback, as it is not supported by default. There are a few tips available, and today I want to share my complete implementation. I hope it will help others who need to achieve the same thing.


1. In back-end code, add more fallback language property for Graph model:

public class FallbackLanguageContentsApiModelProperty(
    ILanguageBranchRepository languageBranchRepository,
    ICustomUrlService customUrlService,
    IContentLanguageSettingsHandler contentLanguageSettingsHandler) : IContentApiModelProperty
{
    public object GetValue(ContentApiModel contentApiModel)
    {
        if (contentApiModel.ContentLink != null)
        {
            var enabledLanguages = languageBranchRepository.ListEnabled();
            var pagesThatFallBackContentToCurrentPage = new List<FallbackLanguageContent>();
            var cRef = contentApiModel.ContentLink.ToContentReference();

            foreach (var enabledLanguage in enabledLanguages)
            {
                var fallbackLanguages = contentLanguageSettingsHandler.GetFallbackLanguages(cRef, enabledLanguage.Culture.Name);
                if (fallbackLanguages != null && fallbackLanguages.Any() && fallbackLanguages.Contains(contentApiModel.Language.Name))
                {
                    var lang = enabledLanguage.Culture.Name;

                    pagesThatFallBackContentToCurrentPage.Add(new FallbackLanguageContent()
                    {
                        LanguageName = lang,
                        RelativePath = !ContentReference.IsNullOrEmpty(cRef) ? customUrlService.GetRelativeUrl(cRef, lang) : string.Empty
                    });
                }
            }

            return pagesThatFallBackContentToCurrentPage;
        }
        else
        {
            return new List<FallbackLanguageContent>();
        }
    }

    public string Name => "FallbackLanguageContents";
}

internal class FallbackLanguageContent
{
    public required string LanguageName { get; set; }
    public required string RelativePath { get; set; }
}

 

2. In the front-end code, we need to query content using a relative path and locale, including the fallback logic. Here is the query:

query getContentByPathWithinFallback($path: [String!]!, $locale: String, $siteId: String) {
  content: Content(
    where: {
      SiteId: { eq: $siteId }
      _or: [
        { _and: [{ Language: { Name: { eq: $locale, boost: 2 } } }, { RelativePath: { in: $path } }] }
        { _and: [{ FallbackLanguageContents: { LanguageName: { eq: $locale, boost: 1 } } }, { FallbackLanguageContents: { RelativePath: { in: $path } } }] }
      ]
    }
    locale: ALL
  ) {
    items: item {
      ...IContentData
      ...PageData
    }
  }
}
fragment PageData on IContent {
  ...IContentData
}
fragment IContentData on IContent {
  contentType: ContentType
  _metadata: ContentLink {
    id: Id
    version: WorkId
    key: GuidValue
  }
  locale: Language {
    name: Name
  }
  path: RelativePath
  _type: __typename
}

 

By using boost values in the query, we can indicate which conditions should have higher priority. As a result, the fallback content is returned only when no content exists for the exact locale.

If you are using Optimizely SaaS Starter for your headless solution, you can call your custom content query in src/app/[[...path]]/page.tsx by replacing the following section:


 

That's all. Hope this makes your multilingual setup a bit easier. Happy coding!

Nov 15, 2025

Comments

Erik Täck
Erik Täck Dec 16, 2025 08:18 PM

This might be out of context for your implementation, but if a page in a language have multiple fallbacks defined, how would you determine which is the correct one to return?

 

Binh Nguyen Thi
Binh Nguyen Thi Dec 27, 2025 07:54 AM

Hi @Erik,

Thanks for your comment.

That is good question. We can correct this one by indexing more SortIndex in FallbackLanguageContent as following as:

public class FallbackLanguageContentsApiModelProperty(
    ILanguageBranchRepository languageBranchRepository,
    ICustomUrlService customUrlService,
    IContentLanguageSettingsHandler contentLanguageSettingsHandler) : IContentApiModelProperty
{
    public object GetValue(ContentApiModel contentApiModel)
    {
        if (contentApiModel.ContentLink != null)
        {
            var enabledLanguages = languageBranchRepository.ListEnabled();
            var pagesThatFallBackContentToCurrentPage = new List<FallbackLanguageContent>();
            var cRef = contentApiModel.ContentLink.ToContentReference();
            var sortIndex = enabledLanguages.FirstOrDefault(x => x.Name == contentApiModel.Language.Name)?.SortIndex ?? int.MaxValue;

            foreach (var enabledLanguage in enabledLanguages)
            {
                var fallbackLanguages = contentLanguageSettingsHandler.GetFallbackLanguages(cRef, enabledLanguage.Culture.Name);
                if (fallbackLanguages != null && fallbackLanguages.Any() && fallbackLanguages.Contains(contentApiModel.Language.Name))
                {
                    var lang = enabledLanguage.Culture.Name;

                    pagesThatFallBackContentToCurrentPage.Add(new FallbackLanguageContent()
                    {
                        LanguageName = lang,
                        RelativePath = !ContentReference.IsNullOrEmpty(cRef) ? customUrlService.GetRelativeUrl(cRef, lang) : string.Empty,
                        SortIndex = sortIndex
                    });
                }
            }

            return pagesThatFallBackContentToCurrentPage;
        }
        else
        {
            return new List<FallbackLanguageContent>();
        }
    }

    public string Name => "FallbackLanguageContents";
}

internal class FallbackLanguageContent
{
    public required string LanguageName { get; set; }
    public required string RelativePath { get; set; }
    public required int SortIndex { get; set; }
}

Then adjust the query with sorting by SortIndex ASC as following as:

query getContentByPathWithinFallback($path: [String!]!, $locale: String, $siteId: String) {
  content: Content(
    where: {
      SiteId: { eq: $siteId }
      _or: [
        { _and: [{ Language: { Name: { eq: $locale, boost: 2 } } }, { RelativePath: { in: $path } }] }
        { FallbackLanguageContents: {LanguageName: { eq: "ja", boost: 1 }, RelativePath: { in: ["/ja/"] } } }
      ]
    }
    orderBy: {
      _ranking: BOOST_ONLY,
      FallbackLanguageContents: {SortIndex: ASC}
    }
    locale: ALL
  ) {
    items: item {
      ...IContentData
      ...PageData
    }
  }
}
fragment PageData on IContent {
  ...IContentData
}
fragment IContentData on IContent {
  contentType: ContentType
  _metadata: ContentLink {
    id: Id
    version: WorkId
    key: GuidValue
  }
  locale: Language {
    name: Name
  }
  path: RelativePath
  _type: __typename
}

Please login to comment.
Latest blogs
Optimizely CMS 13: A Strategic Reset for Content, AI, and Composable Marketing

Optimizely CMS 13 is not just another version upgrade—it represents a deliberate shift toward a connected, AI-enabled, and API-driven content...

Augusto Davalos | Apr 14, 2026

The 74% Problem: Why Most Companies Are Getting AI Wrong

I’ve seen this before… The pattern. The rush, the excitement, the scramble to adopt something new before anyone has stopped to ask what problem it...

Mark Welland | Apr 14, 2026

Scheduled jobs with parameters

Scheduled jobs is an integral part of most Optimizely solution but the UI has, in my opinon, always been lacking in usability and features. Earlier...

Per Nergård (MVP) | Apr 14, 2026

Using Caching and Async to Improve Performance

Almost every CMS served website has some concept of dynamic content resolution. A means to serve up related or latest content links while minimisin...

Mark Stott | Apr 14, 2026