Multisite configuration in Next.js application using Middleware

This is a very common requirement where we need to achieve the multisite/multitenant capability while developing Next JS frontend application using Sitecore CMS in headless manner.

Next.js application uses Next.js Middleware to serve the correct Sitecore site based on the incoming hostname. 

Middleware

Middleware allows you to run code before a request is completed. It allows you to add additional functionality or perform tasks such as modify response by redirecting, rewriting, modifying the headers.

Multisite add-on

By default, the Next.js Multisite add-on fetches a list of site information directly from your Sitecore instance, and all sites are statically generated at build time. It also uses Next.js Middleware to serve the correct Sitecore site. But Multisite add-on have there own limitations where config-based site definitions are not currently included. Only SXA-based sites will be fetched. 

You can add this add-on using the JSS initializer. An XM Cloud project based on a Starter Kit automatically includes this add-on. For more information please refer the Sitecore documentation.

Configure Multisite

The above diagram shows the general flow whether using the Next.js multisite add-on or manually configuring the sites in a multisite Next.js application. 

This approach is useful for non-SXA sites since the config-based site definitions are currently not supported by this add-on but we can still do it easily using Middleware.

Step 1 - Create a sites.ts file in your project src/lib/multisite folder. In this file, we will  maintain a list of sites.

Note - We can also fetch the Site Info directly from Sitecore using the GraphQL. This will help to manage the site collection dynamically to support the future sites without any modification in the Next Js multisite configuration. 

The Next.js Multisite add-on uses GraphQLSiteInfoService to fetch site information directly from Sitecore.

Code snippet - sites.ts

interface Hostname {
  siteName: string;
  description: string;
  subdomain: string;
  rootItemId: string;
  languages: string[];
  customDomain: string;
  defaultForPreview: boolean;
}

const hostnames: Hostname[] = [
  {
    siteName: 'Site1',
    description: 'Site 1',
    subdomain: '',
    rootItemId: '{33E0826A-F8F6-5961-A24A-2F4CC7FA946E}',
    languages: ['en'],
    customDomain:'www.site1.localhost',
    defaultForPreview: true,
  },
  {
    siteName: 'Site2',
    description: 'Site 2',
    subdomain: '',
    rootItemId: '{F4C787E3-FE05-4211-BD62-1FA238E50A57}',
    languages: ['en', 'da-DK', 'fr-FR', 'fr-CA', 'es-US'],
    customDomain: 'www.site2.localhost',
    defaultForPreview: false,
  }
];

// Returns the default site (Global)
const DEFAULT_HOST = hostnames.find((h) => h.defaultForPreview) as Hostname;

/**
 * Returns the data of the hostname based on its subdomain or custom domain
 * or the default host if there's no match.
 *
 * This method is used by middleware.ts
 */
export async function getHostnameDataOrDefault(
  subdomainOrCustomDomain?: string
): Promise {
  if (!subdomainOrCustomDomain) return DEFAULT_HOST;

  // check if site is a custom domain or a subdomain
  const customDomain = subdomainOrCustomDomain.includes('.');

  // fetch data from mock database using the site value as the key
  return (
    hostnames.find((item) =>
      customDomain
        ? item.customDomain.split('|').includes(subdomainOrCustomDomain)
        : item.subdomain === subdomainOrCustomDomain
    ) ?? DEFAULT_HOST
  );
}

export default hostnames;

Step 2 - Now create a middleware.ts file in your project src folder.

import { NextRequest, NextResponse } from 'next/server';
import { getHostnameDataOrDefault } from './lib/multisite/sites';

export const config = {
  matcher: [
    '/',
    '/((?!api/editing|_next/static|sc_logo.svg|-/media/|-/jssmedia/|-/icon/|sitemap).*)',
  ],
};

export default async function middleware(req: NextRequest): Promise {
  const url = req.nextUrl.clone();

  const hostname = req.headers.get('host');
  
  const currentHost = hostname?.replace(`.${process.env.ROOT_DOMAIN}`, '');

  const data = await getHostnameDataOrDefault(currentHost?.toString());
  
  if (
    url.pathname.startsWith('/_next') ||
    url.pathname.includes('/api/') ||
    url.pathname.includes('-/media/') ||
    url.pathname.includes('-/jssmedia/') ||
    url.pathname.includes('-/icon/') ||
    url.pathname.includes('/sitemap/')
  ) {
    return NextResponse.next();
  }
  
  if (url.pathname.startsWith(`/_sites`)) {
    url.pathname = `/404`;
  } 
  else {
    // rewrite to the current subdomain
    url.pathname = `/_sites/${data?.subdomain}${data?.siteName}${url.pathname}`;
  }

  return NextResponse.rewrite(url);
}
This middleware code will map the hostname with the respective path and serve the required page. 

Some important notes from the above middleware code snippet

Matcher - 
  • Matcher allows you to filter Middleware to run on specific paths.
  • You can match a single path or multiple paths with an array syntax.
  • The matcher config allows full regex support.
  • The matcher values need to be constants so they can be statically analyzed at build-time. 
  • Dynamic values such as variables will be ignored.
Conditional Statements -
  • Conditional statements might vary as per the requirements but the main purpose of this in the middleware is to handle the request redirection or skip the path to navigate wherever needed. For example - in the above code snippet there is a condition if url start with '/_sites', it will redirect to the 404 page. Similarly, path which includes '/media', '/jssmedia', '/icon' etc. will be skipped for any process and will continue the middleware chain using NextResponse.next() function call.
NextResponse -
  • Redirect the incoming request to a different URL
  • Rewrite the response by displaying a given URL
  • Set response cookies
  • Set response headers
  • NextResponse.next() to continue the middleware chain.
That's it! Hope you like this article and it might help you to understand the use of Middleware.

Comments

Popular posts from this blog

Setup Sitecore XM Cloud Dev Environment using Docker

Sitecore Content Hub - Triggers, Actions and Scripts

All Blog Posts - 2023