Sitecore XM Cloud Embedded Personalization's variants limitation and resolution Part 2

 In Part 1 of the article we have seen the Sitecore XM Cloud Limitations of allowing only 8 variants to do personalization, In this article we will see how to overcome the limitation with the workaround.

Considering the requirement that we have an existing multisite in Sitecore XM Cloud which has separate sites based on each country, now the client looking to remove the country sites and use one site for all countries but they want to personalize the home page component based on the country from the user visiting the page.

The other requirement is that the content author should be in full control of personalization like assigning the datasource for the components etc. 

So now the requirement is clear, we need to personalize the home page components based on all the available countries that the client has requested.

So, In order to make this achievable, we have the following options.

  • Directly use Sitecore Personalize instead of depending on Embedded personalization (here content authors don't have control over personalization from XM Cloud Pages)
  • Use Embedded Personalization by customizing the middleware to achieve this personalization behavior, This approach will still provide the content author full control of item personalization from XM Cloud Pages
We will be using the middleware approach to overcome this limitation.

Embedded Personalization the middleware way:
  • In Sitecore XM Cloud we create the default home page, which is responsible for providing personalized components to the user based on the country the user visiting.
  • Create audience rule for 8 countries and provide the personalized datasource for the components on the home page
  • Now we have already created the home page with all 8 variants for the countries, but still we have more countries available to provide personalized home page content to the user,

    So we will be creating multiple home sub-pages under the main home item page, and create each home sub-page with 8 audience rules each for the remaining countries.
    By doing the above step we will end up with many home pages each having 8 audience variants like below.



    Default Home Page (Home):



    Other Home Sub Page 1 (Home2)



  • These sub-home pages should not be visible in sitemap.xml, so we will be ignoring them from the sitemap.xml
  • Now we have all the variants created with multiple home pages
  • Now we will configure the middleware that dynamically loads the home page along with its personalized content without the users knowing that they are viewing different personalized home pages. As there won't be any change in the browser URLs
  • Now we have created the home pages with variants for all the countries, let's see how the middleware will resolve the pages based on the user country, In order to achieve it we will create a settings item in Sitecore with a key-value pair of Country code and home path, as this is necessary to provide the mapping between the country and the home page.
  • Based on the above mapping, the middleware will load the proper home page based on the user country.

  • Now we have configured the settings for the mapping, let's start creating the middleware which will be responsible for loading the proper home page and letting the Personalize addon automatically do its job of personalizing the page based on the variant that it is received using the Callflows API for the current resolved home page.
  • If the middleware resolves the mapping of the country as Home2 then the personalize addon will check for any variants created for Home2 and trigger the Callflows API to identify the variant based on the audience rule created to match the country and provide the personalization automatically for all the components in the Home2
Sample Middleware code:

Go to middleware.ts in the src folder and add the below code.

import { NextResponse, NextRequest, NextFetchEvent } from 'next/server';
import middleware from 'lib/middleware';

let countriesCache: Map<string, string> | null = null;
let countryPathCache: Map<string, string> = new Map();
// Fetch the Home page based on Country settings item from XM Cloud
async function getCountriesFromSitecore(): Promise<Map<string, string>> {
  const query = `
  {
    item(language: "en", path: "{91CEB4EA-3EB0-4C5D-A25D-3E6801C46A9F}") {
      field(name: "CountryMapping") {
        jsonValue
      }
    }
  }
  `;
  const response = await fetch('https://edge.sitecorecloud.io/api/graphql/v1', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-GQL-TOKEN':
        'nRhbGRkZGY2LXRyYWluaW5nMDgyYjAwOS1kZXY1NDE0LWM4MzE=',
    },
    body: JSON.stringify({ query }),
  });

  if (!response.ok) {
    throw new Error('Failed to fetch Home page based country settings item from Sitecore');
  }

  const data = await response.json();
  const countriesString: string = data?.data?.item?.field?.jsonValue?.value;

  if (!countriesString) {
    throw new Error('Invalid or missing country data from Sitecore');
  }

  //Map all the country and the matched home page path
  const parsedCountries = new Map<string, string>(
    countriesString.split('&').map((pair: string): [string, string] => {
      const [country, path] = pair.split('=');
      if (!country || !path) {
        throw new Error('Invalid country-path pair');
      }
      return [country, decodeURIComponent(path)];
    })
  );
  countriesCache = parsedCountries;
  return countriesCache;
}
// Function to get country path (either from cache or Sitecore)
async function getCountryPath(userCountry: string): Promise<string | undefined> {
  // Check if the country path is in the cache
  if (countryPathCache.has(userCountry)) {
    return countryPathCache.get(userCountry);
  }

  // If not in cache, fetch from Sitecore
  const countries = await getCountriesFromSitecore();
  const homePagePath = countries.get(userCountry);

  // Add to cache if found
  if (homePagePath) {
    countryPathCache.set(userCountry, homePagePath);
  }
  return homePagePath;
}
export default async function middlewareHandler(req: NextRequest, ev: NextFetchEvent) {
  let response = NextResponse.next();
  let rewrittenUrl: URL | null = null;

  // Check if the 'middleware-rewrite' cookie exists and delete it
  if (req.cookies.has('middleware-rewrite')) {
    response.cookies.delete('middleware-rewrite');
  }

  // Only run country check on the home page and if the rewrite hasn't happened yet
  if (req.nextUrl.pathname === '/') {
    try {
      // Get the country from the request  (assuming code deployed to Vercel)
      //Vercel will provide this geo location information automatically
      const userCountry = req?.geo?.country || 'AE';
     
      if (userCountry) {
        // Get country path (from cache or Sitecore)
        const homePagePath = await getCountryPath(userCountry);

        // Check if the request country matches any country from Sitecore
        if (homePagePath) {
          console.log('=========Country ==' + userCountry + ' ==== and homepage path ==='+homePagePath+'===========');
          rewrittenUrl = req.nextUrl.clone();
         
          //Naviagting to the proper home page based on the identified country
          if (homePagePath && homePagePath !== '/') {
            rewrittenUrl.pathname = `/countryhome${homePagePath}`;
            console.log('==========' + rewrittenUrl.pathname + '=========');
          }

          // Pass the original URL as a query parameter
          rewrittenUrl.searchParams.set('originalPath', req.nextUrl.pathname);
          response = NextResponse.rewrite(rewrittenUrl);

          response.cookies.set('middleware-rewrite', 'true', {
            httpOnly: true,
            secure: process.env.NODE_ENV === 'test',
            maxAge: 60 * 5, // 5 minutes
            path: '/',
          });
        }
      }
    } catch (error) {
      console.error('Error in country check middleware:', error);
      // Continue with the original request in case of error
    }
  }

  const newReq = rewrittenUrl ? new NextRequest(rewrittenUrl, req) : req;
  // Execute the original middleware with the potentially modified response
  return middleware(newReq, ev, response);
}

export const config = {
  matcher: [
    '/', // Explicitly match the home page
    '/home2',
    '/home3',
    // Match other paths, excluding the ones specified
    '/((?!api/|_next/|feaas-render|healthz|sitecore/api/|-/|favicon.ico|sc_logo.svg).*)',
  ],
};

   

Note:

  • We are going to use Geo Location from the Vercel function, which is out of the box, instead of using a separate service for geolocation. Vercel provides 2 character country code which is the same thing we have mapped in the Country settings in Sitecore.
  • Once the request is made the middleware will execute and it will check the user location from the request object and compare then get the mapping settings item from Sitecore, if the country matches with the settings item then the relevant home page path will be executed by URL rewrite method
  • We are going to execute this logic only on the home page, as this is the common middleware that will be executed for all the requests.
  • We will also cache the settings mapping instead of executing every time the home page is called.


Results: User Visits from Qatar - Audience rule created in Home2 Page

The user visits from Qatar, the relevant home page gets loaded and the Text component also gets personalized based on the country. In this example, Home2 gets loaded as the Qatar audience rule is created on this page only.





Results: User Visits from Brazil - Audience rule created in Home3 Page

The user visits from Brazil, the relevant home page gets loaded and the Text component also gets personalized based on the country. In this example, Home3 gets loaded as the Brazil audience rule is created on this page only.




I hope this workaround will help in overcoming the Sitecore XM Cloud Pages variant limitation.

Sitecore XM Cloud Pages Embedded Personalization variant limitation and resolution Part 1
Let's learn and grow together, happy programming 😊

Comments

Popular posts from this blog

Sitecore Upgrade from 8.1 XP to 10.4 XM Scaled - Part 1

Custom Item Url and resolving the item in Sitecore - Buckets

Fixing Sitecore Buckets folder path - Items created after 12 AM server time zone