Implementing a Custom 404 Error Page in a Sitecore XM Cloud Multi-Site Project

Hello everyone, in this blog we will see how we can implement our custom 404 error page in XM Cloud project. This blog is very helpful when you are working in multisite solution with single Front End project. It means in front end, we have single repository only.

XM Cloud starter repo comes up with default 400 and 500 pages but there is limitation with next js implementation. You can find detail discussion for error page limitation in this github channel here

In Sitecore documentation also, its clearly mention about the same. Let me give a small information about what's the limitaion. When any page you search which is not available on your site, the page is redirected to Next JS 404 page. This 404 page then internally calls the GraphQLErrorPagesService class which internally calls grapghql query to fetch the error page configured for your site in Sitecore. This is implemented using component level data fetching. So when the page build happens for SSG, it is unable to identify which site error page to serve as this point when actual 404 is encounter. Please refer the below code to get more closer idea.

If you focus on line number 31 in code, it is clearly getting the site name from config. So what ever SITECORE_SITE_NAME is available in env file, it will take that site only and the GQL which is need to be executed to get the data for error page will always be specific for the site coming from config.And in one mono repo we can have only on site name.

       
  const site = siteResolver.getByName(config.sitecoreSiteName);      
	   

To overcome this and have site specific 404 error page, follow below steps.

Step 1: Create a page-not-found page in Sitecore for respective site. Change the display name of the page to 404.

Step 2: Assign the newly created page to Page not found link field in Error Handling Section for respective sites.

Step 3: Now comes the biggest part and our logic that needs to be executed is at Front End. Our logic is simple, instead of fetching site specific error page using the site name, we will fetch error pages from all site and will store them as collection where your sitename will be key and the error page return from GQL query will your actual data.

This data will be fetched at run time using component level data fetching which will be later passed to our default Custom404 component as an additional property. So in real time, when any page request come which is not available in our Sitecore, we will take the hostname from the url. By using hostname we will then call the SiteResolver inbuilt method by passing hostname as parameter to fetch the site name.

Once the sitename is available, we will filter the sitename from additional property return to us from getstaticprops method. Then we can filter the exact error page for the site using site name and show it accordingly. Please refer the below code for your reference as this is tested code working fine in live project.

       
import config from 'temp/config';
import {
  GraphQLErrorPagesService,
  SitecoreContext,
  ComponentPropsContext,
} from '@sitecore-jss/sitecore-jss-nextjs';
import { SitecorePageProps } from 'lib/page-props';
import { componentBuilder } from 'temp/componentBuilder';
import Layout from 'src/Layout';
import { GetStaticProps } from 'next';
import { siteResolver } from 'lib/site-resolver';
import clientFactory from 'lib/graphql-client-factory';
import { useEffect, useState, JSX } from 'react';
import { sitecorePagePropsFactory } from 'lib/page-props-factory';
import NotFound from 'src/NotFound';

interface SiteInfoList {
  name: string;
  hostName: string;
  language: string;
}
type ErrorPageMap = Record<string, SitecorePageProps | null>;
const Custom404 = (props: SitecorePageProps & { resultErrorPages: ErrorPageMap }): JSX.Element => {
  const [pageProps, setPageProps] = useState<SitecorePageProps | null | 'loading'>('loading');

  // We need to fetch determine the site name client-side because NextJS doesn't provide the ability
  // to determine which site we're on due to middleware rewrites not executing for the 404 handler.
  useEffect(() => {
    if (typeof window === 'undefined') {
      console.log('window is undefined');
      return;
    }
    const domain = window.location.hostname;
    const siteName = siteResolver.getByHost(domain ?? '')?.name ?? config.sitecoreSiteName;
    const errorPages = props?.resultErrorPages?.[siteName ?? ''] ?? null;
    setPageProps(errorPages);
  }, [props?.resultErrorPages]);
  if (pageProps === 'loading') {
    return <></>;
  }
  if (pageProps === null) {
    return <NotFound />;
  }
  return (
    <ComponentPropsContext value={pageProps?.componentProps || {}}>
      <SitecoreContext
        componentFactory={componentBuilder.getComponentFactory()}
        layoutData={pageProps?.layoutData}
      >
        <Layout
          layoutData={
            pageProps?.layoutData ?? {
              sitecore: {
                context: {},
                route: null,
              },
            }
          }
          headLinks={props.headLinks}
        />
      </SitecoreContext>
    </ComponentPropsContext>
  );
};

export const getStaticProps: GetStaticProps = async (context) => {
  const resultErrorPages: ErrorPageMap = {};

  const siteList = config.sites;
  // Handle config.sites being either string or array
  const sites: SiteInfoList[] = typeof siteList === 'string' ? JSON.parse(siteList) : siteList;

  for (const site of sites) {
    const errorPagesService = new GraphQLErrorPagesService({
      clientFactory,
      siteName: site.name,
      language: context.locale || config.defaultLanguage,
      retries:
        (process.env.GRAPH_QL_SERVICE_RETRIES &&
          parseInt(process.env.GRAPH_QL_SERVICE_RETRIES, 10)) ||
        0,
    });
    if (process.env.DISABLE_SSG_FETCH?.toLowerCase() !== 'true') {
      try {
        const errorPages = await errorPagesService.fetchErrorPages();
        if (errorPages?.notFoundPagePath) {
          const paths = [
            `_site_${site.name}`,
            ...(errorPages?.notFoundPagePath?.split('/')?.filter((x) => x.length > 0) ?? []),
          ];
          context.params = {
            ...context.params,
            path: paths,
          };
          const props = await sitecorePagePropsFactory.create(context);
          resultErrorPages[site.name] = props;
        } else {
          console.warn('no error pages found for site', site.name);
          resultErrorPages[site.name] = null;
        }
      } catch (error) {
        console.error('Error occurred while fetching error pages');
        console.error(error);
      }
    } else {
      console.warn('SSG fetch is disabled for site', site.name);
      resultErrorPages[site.name] = null;
    }
  }
  return {
    props: {
      headLinks: [],
      resultErrorPages,
    },
  };
};

export default Custom404;
       
	   

This way you can still utilize the SSG functionality and display the error page Site specific.

GQL which is executed to fetch Error page

       
query ErrorPagesQuery($siteName: String!, $language: String!) {
    site {
      siteInfo(site: $siteName) {
        errorHandling(language: $language) {
          notFoundPage {
            rendered
          }
          notFoundPagePath
          serverErrorPage {
            rendered
          }
          serverErrorPagePath
        }
      }
    }
  }       
	   

You can check my other blogs too if interested. Blog Website

Important Points:

  • Make sure your Sitecore page name should not be 404 or 500 which is default pages of next js as you can see in first image. The problem will be, when you open the same page in page builder, it will not understand which page to show for editing, whether Sitecore 404 page or next js 404 page. So it will execute the logic of next js 404 page only.
  • When you work with multisite solution with single FE project, SITECORE_SITE_NAME is not required to be configured in ENV variable in higher environments.
  • Make sure you have properly updated the target hostname value to your actual domain as there should be an entry in hostname field in SiteSettings. This is needed for fetching site name that can be used later to fetch site specific error page.

Reference Links

  • https://doc.sitecore.com/xmc/en/developers/xm-cloud/configure-a-custom-static-error-page.html
  • https://www.sitecorecoding.com/site-specific-error-pages-in-sitecore-xm-cloud-and-netx-js-and-vercel-face7d2baafa

Comments

Popular posts from this blog

Automate RSS Feed to Sitecore XM Cloud: Logic App, Next.js API & Authoring API Integration

Sitecore XM Cloud Form Integration with Azure Function as Webhook

Sitecore 10.2 Installation using Windows PowerShell