Sitecore XM Multisite switcher

Content Insights logo

Today, I’m excited to share a nifty little component I’ve created to simplify life not just for myself, but also for our clients. But before we dive into the solution, let’s take a moment to understand the problem that sparked this innovation.

We're assisting a client with migrating from their existing Sitecore DXP to the new XM Cloud platform. While these platforms may look similar, there are some significant differences. The most notable change is that the head is now decoupled from the Sitecore CMS. If you're curious about this, I highly recommend checking out the Sitecore documentation.

Our setup includes the XM Cloud platform paired with a Vercel head solution. This setup runs over 40 websites on a single Vercel project. Switching between sites is straightforward—you just add `?sc_site=[sitename]` to the URL, and the head solution loads data from the corresponding content node in XM Cloud. This works well for our development team, but content editors, who don’t want to remember all the different site names, found it cumbersome.

So, I thought, why not make this process easier? Enter the SiteSelection component—a simple dropdown menu rendered on top of a site. This dropdown is populated with all possible sites in XM Cloud, making site switching a breeze for everyone.

To implement this, I created the SiteSelection component and added it to the `Layout.tsx` file to ensure it renders on all pages. I also included the layoutData in its properties, which is necessary to check if we’re running in editor mode (Pages or Experience Editor*).

*Note: The Experience Editor should no longer be used with XM Cloud. Disable the editor to prevent significant performance issues on the XM Cloud platform if necessary. Even one person using it can cause a noticeable impact.

The SiteSelection component is pretty straightforward. Here’s a glimpse of the code:

import { LayoutServiceData } from '@sitecore-jss/sitecore-jss-nextjs';
import LoadingIndicator from 'legacy/legacy-react/components/20_Molecules/LoadingIndicator';
import { LoadingIndicatorType } from 'legacy/legacy-react/components/Types/LoadingIndicator';
import { ChangeEvent, useState } from 'react';
import {
  SitecoreSiteKey,
  CustomSiteKey,
  getCookie,
  getHostname,
  setCustomCookie,
} from 'src/util/siteUtilizations';

type InternalSiteSelectionProps = {
  isFeatureEnabled: boolean;
  isVercelDomain: boolean;
  currentSitecoreSite: string;
  sites: string[];
};

type SiteSelectionProps = {
  layoutData: LayoutServiceData;
};

export default function SiteSelection({ layoutData }: SiteSelectionProps) {
  const siteCoreContext = layoutData.sitecore.context;
  const isEditing = siteCoreContext.pageEditing || siteCoreContext.siteEditing;

  const props = initializeProps();

  const [selectedOption, setSelectedOption] = useState(
    decodeURIComponent(props.currentSitecoreSite)
  );
  const [isLoading, setIsLoading] = useState(false);

  return (
    <>
      {isLoading ? (
        <div
          style={{
            position: 'fixed',
            top: 0,
            left: 0,
            width: '100%',
            height: '100%',
            zIndex: '9999',
            background: 'rgba(0, 0, 0, 0.9)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <div
            style={{
              alignItems: 'center',
              justifyContent: 'center',
            }}
          >
            <LoadingIndicator type={LoadingIndicatorType.DEFAULT} />
            <p>
              Switching site to: <strong>{selectedOption}</strong>
            </p>
          </div>
        </div>
      ) : (
        <></>
      )}

      {props.isFeatureEnabled && !isEditing && props.isVercelDomain ? (
        <div
          style={{
            position: 'absolute',
            zIndex: '9998',
            right: '20px',
            top: '20px',
            background: 'rgba(0, 0, 0, 0.2)',
            padding: '20px',
          }}
        >
          <select title="Site selection" value={selectedOption} onChange={handleChange}>
            <option value=""></option>
            {props.sites.map((site) => (
              <option key={site} value={site}>
                {site}
              </option>
            ))}
          </select>
        </div>
      ) : (
        <></>
      )}
    </>
  );

  function handleChange(event: ChangeEvent<HTMLSelectElement>): void {
    event.preventDefault();
    setIsLoading(true);
    setSelectedOption(event.target.value);
    window.location.href = `${window.location.origin}?${SitecoreSiteKey}=${event.target.value}`;

    setCustomCookie(VwpfsSiteKey, event.target.value);
  }

  function initializeProps(): InternalSiteSelectionProps {
    const isFeatureEnabled = process.env.NEXT_PUBLIC_FEATURE_SITE_SELECTION_ENABLED === 'true';
    const currentHostname = getHostname();

    const currentSitecoreSite = getCookie(SitecoreSiteKey) || '';
    const isVercelDomain = currentHostname?.endsWith('.vercel.app') ?? currentHostname === 'localhost';

    let sites = [];
    try {
      if (process.env.NEXT_PUBLIC_SITES_LIST != undefined) {
        sites = JSON.parse(process.env.NEXT_PUBLIC_SITES_LIST);
        sites.sort();
      }
    } catch {}

    return {
      isFeatureEnabled,
      isVercelDomain,
      currentSitecoreSite,
      sites,
    };
  }
}

Is this code perfect? Not quite. It’s a quick and dirty solution ;) Here are some improvements you might consider:

  • Ditch the Inline Styling: Move the inline styles to a CSS class for better maintainability.
  • Dynamic Site List: Instead of reading the sites from an environment variable, consider querying Experience Edge for this information.
  • Custom sc_site Cookie: Due to a quirk in our code, the sc_site cookie was sometimes dropped, causing 404 errors when the head switched back to the default site. I created an override for the sc_site cookie in both the switcher and the request pipeline to address this.
  • Vercel Domain Check: If you’re not running on Vercel, adjust the `isVercelDomain` check. We want to ensure the component doesn’t render on a production site. Hence, we added checks to enable it, running on a `*.vercel.app` domain, and not using an editing host (since you can also run your own editing host).

I hope you find this idea useful! Please feel free to share your improved SiteSelection component with the community. Let's make site switching as effortless as possible for everyone.