Binh Nguyen Thi
Nov 15, 2025
  659
(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
Disabling the scheduler also stops event-based indexing in Optimizely

If you disable the scheduler in Optimizely CMS, should event-based indexing stop working too? It does!

Tomas Hensrud Gulla | Mar 13, 2026 |

Meet the latest OMVPs - H1 2026 Cohort

Meet the Latest Cohort of Optimizely Most Valuable Professionals (OMVPs) Every year, the Optimizely Most Valuable Professional (OMVP) program...

Satata Satez | Mar 13, 2026

Optimizely Commerce vs Composable Commerce: What Should You Do with CMS 13?

As organizations modernize their digital experience platforms, a common architectural question emerges: Should we continue using Optimizely Commerc...

Aniket | Mar 12, 2026

Missing Properties tool for Optimizely CMS

If you have been working with Optimizely CMS for a while you have probably accumulated some technical debt in your property definitions. When you...

Per Nergård (MVP) | Mar 10, 2026