Skip to main content

Command Palette

Search for a command to run...

Extending Sitecore Personalize with a Custom Geolocation Service

Published
4 min read

Introduction

For Geo-based personalization rules in XM Cloud embedded personalization, Sitecore uses the request.geo object set by frontend applications. If Vercel or Netlify are used as frontend applications, the request.geo object is set by the respective hosting provider. This object is read-only and cannot be changed by custom logic. Some organizations may need a proxy/CDN in front of the frontend applications. In that case, the request.geo object is based on the proxy's location rather than the actual end user's location which provides incorrect results for personalization. For Sitecore versions 22.7 and above, Sitecore allows passing a custom geo object in the personalize middleware. However, if the Sitecore JSS version is earlier than 22.7, there is no option to pass a custom geo object from the personalize middleware. We may not be able to upgrade Sitecore JSS due to Next.js and React compatibility issues. This blog focuses on how we can use our custom Personalize Middleware to achieve the expected functionality.

Implementation

1) Under the Middleware folder create a class called CustomPersonalizeMiddleware which inherits from the PersonalizeMiddleware

2) In the class override the initPersonalizeServer and personalize methods by refering the code from sitecore jss 22.7.

 import { NextRequest, NextResponse } from 'next/server';
import { debug } from '@sitecore-jss/sitecore-jss';
import { PersonalizeMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/middleware';
import { CloudSDK } from '@sitecore-cloudsdk/core/server';
import { personalize } from '@sitecore-cloudsdk/personalize/server';

export type PersonalizeOptions = {
  /**
   * Geolocation data used for personalization
   */
  geo?: PersonalizeGeoData;
};
/**
 * Represents the geolocation data used for personalization
 */

export type PersonalizeGeoData = {
  city?: string;
  country?: string;
  region?: string;
};

/**
 * Object model of Experience Context data
 */

export type ExperienceParams = {
  referrer: string;
  utm: {
    [key: string]: string | undefined;
    campaign: string | undefined;
    source: string | undefined;
    medium: string | undefined;
    content: string | undefined;
  };
};
export class CustomPersonalizeMiddleware extends PersonalizeMiddleware {
  options: PersonalizeOptions | undefined;

  protected async initPersonalizeServer({
    hostname,
    siteName,
    request,
    response,
  }: {
    hostname: string;
    siteName: string;
    request: NextRequest;
    response: NextResponse;
  }): Promise<void> {
    await CloudSDK(request, response, {
      sitecoreEdgeUrl: this.config.cdpConfig.sitecoreEdgeUrl,
      sitecoreEdgeContextId: this.config.cdpConfig.sitecoreEdgeContextId,
      siteName,
      cookieDomain: hostname,
      enableServerCookie: true,
    })
      .addPersonalize({ enablePersonalizeCookie: true })
      .initialize();
  }

  protected async personalize(
    {
      params,
      friendlyId,
      language,
      timeout,
      variantIds,
      options,
    }: {
      params: ExperienceParams;
      friendlyId: string;
      language: string;
      timeout?: number;
      variantIds?: string[];
      options?: PersonalizeOptions;
    },
    request: NextRequest
  ) {
    options = this.options;
    debug.personalize('executing experience for %s %o', friendlyId, params);

    return (await personalize(
      request,
      {
        channel: this.config.cdpConfig.channel || 'WEB',
        currency: this.config.cdpConfig.currency ?? 'USD',
        friendlyId,
        params,
        language,
        pageVariantIds: variantIds,
        geo: options?.geo,
      },
      { timeout }
    )) as {
      variantId: string;
    };
  }
}

3) Update the Personalize.ts middleware plugin to use CustomPersonalizeMiddleware instead of PersonalizeMiddleware and use you custom logic to fill the personalize middleware geo options

import { NextRequest, NextResponse } from 'next/server';
import { PersonalizeMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/middleware';
import { MiddlewarePlugin } from '..';
import clientFactory from 'lib/graphql-client-factory';
import config from 'temp/config';
import { siteResolver } from 'lib/site-resolver';
import { getUserLocation } from 'your-custom-implementation';
import { CustomPersonalizeMiddleware } from '../CustomPersonalizeMiddleware';


/**
 * This is the personalize middleware plugin for Next.js.
 * It is used to enable Sitecore personalization and A/B testing of pages in Next.js.
 *
 * The `PersonalizeMiddleware` will
 *  1. Call Sitecore Experience Edge to get the personalization information about the page.
 *  2. Based on the response, call Sitecore Personalize (with request/user context) to determine the page / component variant(s).
 *  3. Rewrite the response to the specific page / component variant(s).
 */
class PersonalizePlugin implements MiddlewarePlugin {
  private personalizeMiddleware: CustomPersonalizeMiddleware;

  // Using 1 to leave room for things like redirects to occur first
  order = 1;

  constructor() {
    this.personalizeMiddleware = new CustomPersonalizeMiddleware({
      // Configuration for your Sitecore Experience Edge endpoint
      edgeConfig: {
        clientFactory,
        timeout:
          (process.env.PERSONALIZE_MIDDLEWARE_EDGE_TIMEOUT &&
            parseInt(process.env.PERSONALIZE_MIDDLEWARE_EDGE_TIMEOUT)) ||
          400,
      },
      // Configuration for your Sitecore CDP endpoint
      cdpConfig: {
        sitecoreEdgeUrl: config.sitecoreEdgeUrl,
        sitecoreEdgeContextId: config.sitecoreEdgeContextId,
        timeout:
          (process.env.PERSONALIZE_MIDDLEWARE_CDP_TIMEOUT &&
            parseInt(process.env.PERSONALIZE_MIDDLEWARE_CDP_TIMEOUT)) ||
          400,
      },
      // Optional Sitecore Personalize scope identifier.
      scope: process.env.NEXT_PUBLIC_PERSONALIZE_SCOPE,
      // This function determines if the middleware should be turned off.
      // IMPORTANT: You should implement based on your cookie consent management solution of choice.
      // You may wish to keep it disabled while in development mode.
      disabled: () => process.env.NODE_ENV === 'development',
      // This function determines if a route should be excluded from personalization.
      // Certain paths are ignored by default (e.g. files and Next.js API routes), but you may wish to exclude more.
      // This is an important performance consideration since Next.js Edge middleware runs on every request.
      excludeRoute: () => false,
      // Site resolver implementation
      siteResolver,
    });
  }

  async exec(req: NextRequest, res?: NextResponse): Promise<NextResponse> {
   const locationData = getUserLocation(req);
     this.personalizeMiddleware.options = {
      geo: {
        city: locationData?.city,
        country: locationData?.country,
        region: locationData?.region,
      },
    };
    return this.personalizeMiddleware.getHandler()(req, res);
  }
}

export const personalizePlugin = new PersonalizePlugin();

4) With this custom middleware implementation, we can pass accurate geo details to the personalization service, ensuring the correct experience for the end user.

Conclusion

In conclusion, extending Sitecore Personalize with a custom geolocation service allows organizations to overcome limitations associated with default geo-based personalization. By implementing a custom Personalize Middleware, businesses can ensure accurate geolocation data is used, enhancing the personalization experience for end users. This approach is particularly beneficial for those unable to upgrade to the latest Sitecore JSS version due to compatibility issues, providing a flexible solution to maintain effective personalization strategies.