Resolving Translated URLs in a Multi Domain Solution in Sitecore XM Cloud on Vercel

Discover strategies for managing multi-domain translations and language toggling in Sitecore XM Cloud on Vercel.

October 31, 2023

By Mike Payne

In this blog, I will teach you how to navigate between two domains associated with different languages in our Headless Sitecore XM Cloud solution hosted in Vercel. We do not want to be brought back to the home page when we toggle the language but rather stay on the same page but in the alternative language. In a previous blog, I walked through how to set up multiple Vercel Projects to host the unique domains. We also set the links to be driven by the Display Names to allow for translated URLs. The Next.JS code at the bottom of this blog was built into the Header component of my application.

The Problem

When we build to Vercel, our static pages are generated from our Sitecore XM Cloud environment. These pages are stored in folders representing the different language versions of the items from Sitecore. Using the translated URLs, we end up with URLs like /en/education and /fr/formation. Simply swapping the locales will yield 404 pages as /fr/education, for instance, does not exist. Vercel has no way of being able to map the en to the fr translated names. You can see the build output in the Source > Output area of a Deployment in your Vercel Project.

Resolving Translated URLs

Extending The Item Context

One solution is to leverage the fact that we are able to extend the Context of an item in Sitecore. Within the Context, we can include the en and fr translated paths that can be used in the href Link component that is swapping our locales. We will set up a Model to include the data we need and a Pipeline processor, ultimately giving us this required data in our Headless application.

Pipeline Processor Code

First, let's set up our Model to contain the data needed for one language version. The translated item path will be stored in the Path property. The Language of that Sitecore version will be stored in the Lang property. We can use the Lang as a sort of Key for our language version ‘dictionary’.

namespace Project.Platform.Models
{
    public class ItemLanguageVersion
    {
        public string Name { get; set; }
        public string Path { get; set; }
        public string Lang { get; set; }
    }
}

In our Context Extension Pipeline code, we will essentially be looping through each language version of the item and adding a new entry to a List of the custom Model defined above (ItemLanguageVersion).

using System.Collections.Generic;
using Project.Platform.Models;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.JavaScriptServices.Configuration;
using Sitecore.LayoutService.ItemRendering.Pipelines.GetLayoutServiceContext;
using Sitecore.Links;
using Sitecore.Links.UrlBuilders;

namespace Project.Platform.Pipelines
{
    public class ContextExtension : Sitecore.JavaScriptServices.ViewEngine.LayoutService.Pipelines.
        GetLayoutServiceContext.JssGetLayoutServiceContextProcessor
    {
        public ContextExtension(IConfigurationResolver configurationResolver) : base(configurationResolver)
        {
        }

        protected override void DoProcess(GetLayoutServiceContextArgs args, AppConfiguration application)
        {
            Assert.ArgumentNotNull(args, "args");

            var langVersions = new List<ItemLanguageVersion>();
            Item tempItem = Sitecore.Context.Item;

            if (tempItem.Versions.Count > 0)
            {
                foreach (Language language in LanguageManager.GetLanguages(Sitecore.Context.Database))
                {
                    Item languageVersion = tempItem.Versions.GetLatestVersion(language);

                    // Check if a version exists in the current language
                    if (languageVersion != null)
                    {
                        // Get the path of the language version
                        ItemLanguageVersion itemLanguageVersion = new ItemLanguageVersion {
                            Lang = languageVersion.Language.Name,
                            Path = GetTranslatedPath(languageVersion)
                                    .Replace("https://cm", "") // this is sometimes being included
                        };

                        // Add the path to the list
                        langVersions.Add(itemLanguageVersion);
                    }
                }
            }

            args.ContextData.Add("languageVersions", langVersions);

        }

        private string GetTranslatedPath(Item item)
        {
                        // These options must match what we have previuosly defined in our LinkManager
            return LinkManager.GetItemUrl(item, new ItemUrlBuilderOptions
            {
                LanguageEmbedding = LanguageEmbedding.Never,
                LowercaseUrls = true,
                UseDisplayName = true,
                EncodeNames = true,
                AlwaysIncludeServerUrl = false,
            });
        }
    }
}

Web Configuration Patch

We are then able to patch the getLayoutServiceContext to include our Pipeline code to extend the Context object returned by the Layout Service.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <pipelines>
            <group groupName="layoutService">
                <pipelines>
                    <getLayoutServiceContext>
                        <processor type="Project.Platform.Pipelines.ContextExtension, Project" resolve="true">
                        </processor>
                    </getLayoutServiceContext>
                </pipelines>
            </group>
        </pipelines>
    </sitecore>
</configuration>

Toggling The Page Language in Next.JS

In our Headless application, we now have access to the properties defined in the above code from within our Context item. I simplified the code below slightly, removed the reference to the GraphQL query I used for creating my Header navigation, and kept the parts required for language toggling. This is the getStaticProps section at the bottom of my TSX file for this component.

export const getStaticProps: GetStaticComponentProps = async (context, layoutData) => {
  const { dataSource } = context;

  if (dataSource == undefined) return;

  const language = layoutData?.sitecore?.context?.language ?? 'en';

  return {
    context: layoutData?.sitecore.context,
    language: language,
  };
};

This is the definition for our component, which makes reference to three different properties:

  • fields - the fields defined in Sitecore
  • context - the context item returned from the getStaticProps method above
  • language - the language string returned from the getStaticProps method above
const MyComponent = React.memo(({ fields, context, language }: MyComponentProps): JSX.Element => {

We have defined a property which essentially equals the opposite language to what we are currently viewing.

const toggleLang = language.toLowerCase() === 'en' ? 'fr' : 'en';

The above property is used to get the language entry in our context that we will switch to when required.

function getLanguageToggleHref() {
    return context?.languageVersions?.find((entry) => entry.lang === toggleLang)?.path ?? '/';
}

This is the language toggle snippet from our Header. Visually, it displays “EN” when we are in the French language domain or “FR” when we are in the English language domain. The href value is driven by the method we created above, which gets the path from the applicable language version entry in our context item. The locale property is driven by the opposite locale we are currently viewing (the locale we would like to switch to).

<Link
        href={getLanguageToggleHref()}
        locale={toggleLang}
        >
            <span>
                {language === 'fr' ? 'EN' : 'FR'}
        </span>
</Link>

In Conclusion

We now have a EN/FR language toggle in our Header. This will switch domains with the correctly translated URL path that is driven by the Display Name in Sitecore. My next entry in this Multi Domain Solution series will detail how we can modify the Sitemap to display the correct domains from where we are viewing it.



Mike Headshot

Mike Payne

Development Team Lead

Mike is a Development Team Lead who is also Sitecore 9.0 Platform Associate Developer Certified. He's a BCIS graduate from Mount Royal University and has worked with Sitecore for over seven years. He's a passionate full-stack developer that helps drive solution decisions and assist his team. Mike is big into road cycling, playing guitar, working out, and snowboarding in the winter.