Customizing Sitecore Next JS Static Path Build Time By Extending Multisite Plugin

In this blog, we’ll explore how to customize the build-time behavior during Static Site Generation (SSG) in an XM Cloud–based application. As you may know, most of our pages are statically generated during the frontend build process and are then served from Vercel's servers or CDN. This significantly boosts page load speed and improves overall site performance. But the key question is — how can we customize this process?

Often, we have pages in the CMS that are quite old — for example, blog posts that were published 10 years ago. The goal is to exclude such outdated pages from the SSG process by omitting their routes during build time. As a result, these pages won’t be pre-rendered; instead, they will be generated on-demand when a user requests them, allowing the server to handle the rendering dynamically.We will divide this blog into 2 part.

Part 1: First part will be how to get the pages that we want to exlude and store it some where.

Part 2: Pass these list of pages to graphql-sitemap-service where it has one method which it take array of paths that will be excluded during SSG build.

Let's the code part without waisting a second!!!!

To achieve our first goal, we will extend our multisite plugin to get all paths and store it in temp.config which is build everytime a new build is created by next. Below is the code. Before that, let's create a multilist field which will allow content author's to add the pages that should be restricted during next build. Check the below image where i have created that template. Once template is created, Add that to the template of Site item under Site Grouping as part of inheritance.

Open multisite.ts plugin file under scripts --> config --> plugins --> multisite.ts

In above image you can see, we have GraphQLSiteInfoService class which does the logic of getting all sites information during next build. We will extend this class to achieve our fist part.

Let's create our CustomGraphQLSiteInfoService class that will extend the GraphQLSiteInfoService class. This class will have all the necessary methods inherited from parent where we will be able to pass our own GQL to complete the requirement.

       
import { gql } from 'graphql-request';
import { Field, GraphQLSiteInfoService, type SiteInfo } from '@sitecore-jss/sitecore-jss-nextjs';
import { type PageInfo } from '@sitecore-jss/sitecore-jss/graphql';

interface SiteInfoItem {
  name: string;
  rootPath: string;
}

interface SiteRootsQuery {
  site: {
    siteInfoCollection: SiteInfoItem[];
  };
}

interface SiteInfoFragment {
  defaultLanguage: Field<string>;
  displayName: Field<string>;
  hostName: Field<string>;
  language: { name: string };
  name: Field<string>;
  excludePaths: { jsonValue: { url: string }[] };
}

type GraphQLSiteInfoResponse = {
  search: {
    pageInfo: PageInfo;
    results: SiteInfoFragment[];
  };
};

export default class CustomGraphQLSiteInfoService extends GraphQLSiteInfoService {
  private static SITE_ROOTS_QUERY = gql`
    query SiteRootsQuery {
      site {
        siteInfoCollection {
          name
          rootPath
        }
      }
    }
  `;

  protected get query() {
    return gql`
      query ($pageSize: Int = 10, $after: String) {
        search(
          where: {
            AND: [
              {
                name: "_templates"
                value: "E46F3AF2-39FA-4866-A157-7017C4B2A40C"
                operator: CONTAINS
              }
              { name: "_path", value: "0DE95AE4-41AB-4D01-9EB0-67441B7C2450", operator: CONTAINS }
            ]
          }
          first: $pageSize
          after: $after
        ) {
          pageInfo {
            endCursor
            hasNext
          }
          results {
            ... on Item {
              name: field(name: "SiteName") {
                value
              }
              hostName: field(name: "Hostname") {
                value
              }
              defaultLanguage: field(name: "Language") {
                value
              }
              language {
                name
              }
              displayName
              excludePaths: field(name: "excludePaths") {
                jsonValue
              }
            }
          }
        }
      }
    `;
  }

  public override async fetchSiteInfo(): Promise<SiteInfo[]> {
    const sites = await this.fetchSites();
    const sitesRoots = await this.fetchSiteRoots();

    return sites.map((value) => ({
      defaultLanguage: value.defaultLanguage?.value,
      displayName: value.displayName ?? '',
      name: value.name.value,
      hostName: value.hostName.value,
      language: value.language.name,
      rootPath: sitesRoots.find((root) => root.name === value.name?.value)?.rootPath ?? '',
      excludePaths: value.excludePaths?.jsonValue?.map((item) => item.url) ?? [],
    }));
  }

  private async fetchSiteRoots(): Promise<SiteInfoItem[]> {
    const response = await this.getGraphQLClient().request<SiteRootsQuery>(
      CustomGraphQLSiteInfoService.SITE_ROOTS_QUERY
    );
    return response.site.siteInfoCollection;
  }

  protected async fetchSites(): Promise<SiteInfoFragment[]> {
    const results: SiteInfoFragment[] = [];
    let hasNext = true;
    let after = '';

    while (hasNext) {
      const response = await this.getGraphQLClient().request<GraphQLSiteInfoResponse>(this.query, {
        after,
        pageSize: 10,
      });
      const items = response?.search?.results;
      if (items) {
        results.push(...items);
      }
      hasNext = response.search.pageInfo.hasNext;
      after = response.search.pageInfo.endCursor;
    }
    return results;
  }
}

	   

Let's add code for using the CustomGraphQLSiteInfoService in our mulitiste plugin.

       
import chalk from 'chalk';
import { SiteInfo } from '@sitecore-jss/sitecore-jss-nextjs';
import { createGraphQLClientFactory } from 'lib/graphql-client-factory/create';
import { JssConfig } from 'lib/config';
import { ConfigPlugin } from '..';
import CustomGraphQLSiteInfoService from '../CustomGraphQLSiteInfoService';

/**
 * This plugin will set the "sites" config prop.
 * By default this will attempt to fetch site information directly from Sitecore (using the GraphQLSiteInfoService).
 * You could easily modify this to fetch from another source such as a static JSON file instead.
 */
class MultisitePlugin implements ConfigPlugin {
  order = 11;

  async exec(config: JssConfig) {
    let sites: SiteInfo[] = [];
    // eslint-disable-next-line no-console
    console.log('Fetching site information');
    try {
      const siteInfoService = new CustomGraphQLSiteInfoService({
        clientFactory: createGraphQLClientFactory(config),
      });
      sites = await siteInfoService.fetchSiteInfo();
    } catch (error) {
      // eslint-disable-next-line no-console
      console.error(chalk.red('Error fetching site information'));
      throw error;
    }

    return Object.assign({}, config, {
      sites: JSON.stringify(sites),
      excludedPath:
        sites.length > 0
          ? JSON.stringify(this.getExcludedPath(sites, ['{SiteName}']))
          : undefined,
      supportedLocales:
        sites.length > 0
          ? JSON.stringify(this.getSupportedLocales(sites, config.defaultLanguage))
          : undefined,
    });
  }

  getSupportedLocales(sites: SiteInfo[], defaultLanguage = 'en') {
    const locales = [defaultLanguage, ...sites.map((site) => site.language).filter(Boolean)];
    return Array.from(new Set(locales));
  }

  getExcludedPath(sites: SiteInfo[], siteNames: string[]) {
    const allExcludePaths = (sites.filter((site) => !siteNames.includes(site.name)) ?? []).flatMap(
      (site) => site.excludePaths
    );

    if (allExcludePaths.length > 0) {
      return Array.from(new Set(allExcludePaths));
    }

    return [];
  }
}

export const multisitePlugin = new MultisitePlugin();
       
	   

The code is simple, we are using our own GQL search query to fetch all needed information for sites along with excluded paths.This code is then utilized by the multisite plugin to prepare the Site information in temp config. Also if you multiple repoistory of FE then you can add specific Site to be excluded by passing SiteName herethis.getExcludedPath(sites, ['{SiteName}'])

Now, we can check our temp config whether the needed data is available or not.

Now let's concentrate on second part where we will focus on, how we can utilize the excludPaths property from temp config and pass it to FastMultisiteGraphQLSitemapService which has inbuilt method to exclude pages from SSG during next build. Below is the code for the same.

       
import {
  StaticPath,
  constants,
  MultisiteGraphQLSitemapService,
} from '@sitecore-jss/sitecore-jss-nextjs';
import config from 'temp/config';
import { SitemapFetcherPlugin } from '..';
import { GetStaticPathsContext } from 'next';
import clientFactory from 'lib/graphql-client-factory';
import { FastMultisiteGraphQLSitemapService } from 'lib/sitecore-jss';

class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin {
  _graphqlSitemapService: MultisiteGraphQLSitemapService;

  constructor() {
    this._graphqlSitemapService = new FastMultisiteGraphQLSitemapService({
      clientFactory,
      excludedPaths: JSON.parse(config.excludedPath ?? '[]'),
    });
  }

  async exec(context?: GetStaticPathsContext): Promise<StaticPath[]> {
    if (process.env.JSS_MODE === constants.JSS_MODE.DISCONNECTED) {
      return [];
    }
    return process.env.EXPORT_MODE
      ? this._graphqlSitemapService.fetchExportSitemap(config.defaultLanguage)
      : this._graphqlSitemapService.fetchSSGSitemap(context?.locales || []);
  }
}

export const graphqlSitemapServicePlugin = new GraphqlSitemapServicePlugin();
       
	   

You can verify the same in vercel build in vercel dasboard by adding a log. In ISR you should not see those pages any more.

Now, it's give us the flexibility to exclude pages during SSG build which is controlled by CMS. Hope you liked this blog and thanks for reading.

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

Point's To Remember

  • excludedPaths : this can take Wildcards also. So if you add the parent folder to exclude, it can exclude all the pages automatically under that folder.

References

  • https://doc.sitecore.com/xmc/en/developers/jss/latest/jss-xmc/the-next-js-multisite-add-on.html

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