Binh Nguyen Thi
Nov 15, 2025
  568
(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 PaaS + Figma + AI: Auto‑Generate Blocks with Cursor

What if your design handoff wrote itself? In this end‑to‑end demo, I use an AI Agent (inside Cursor) to translate a Figma design into an... The pos...

Naveed Ul-Haq | Feb 5, 2026 |

Graph access with only JS and Fetch

Postman is a popular tool for testing APIs. However, when testing an API like Optimizely Graph that I will be consuming in the front-end I prefer t...

Daniel Halse | Feb 4, 2026

Best Practices for Implementing Optimizely SaaS CMS: A Collective Wisdom Guide

This guide compiles collective insights and recommendations from Optimizely experts for implementing Optimizely SaaS CMS, focusing on achieving...

David Knipe | Feb 4, 2026 |

A day in the life of an Optimizely OMVP: Learning Optimizely Just Got Easier: Introducing the Optimizely Learning Centre

On the back of my last post about the Opti Graph Learning Centre, I am now happy to announce a revamped interactive learning platform that makes...

Graham Carr | Jan 31, 2026

Scheduled job for deleting content types and all related content

In my previous blog post which was about getting an overview of your sites content https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/sche...

Per Nergård (MVP) | Jan 30, 2026

Working With Applications in Optimizely CMS 13

💡 Note:  The following content has been written based on Optimizely CMS 13 Preview 2 and may not accurately reflect the final release version. As...

Mark Stott | Jan 30, 2026