Sitecore Headless Multiple Layouts using Next.js

This post is inspired by https://www.techguilds.com/blog/how-to-use-dynamic-layouts-in-headless-sitecore-applications

Problem Statement

It is very common to use more than one layout in Sitecore and there could be many reasons. Back in the old MVC days, you would simply link the layout to separate MVC or ASPX files. With Sitecore headless and Next.js you need some code customization as well to the default template provided by Sitecore JSS. In this post, I will quickly walk you through those steps using Next.js and Typescript.

Steps

1. Create a layout in Sitecore

You need to create a new layout in Sitecore, the easiest way is to copy the existing/default layout and rename it. For the purpose of this demo, we will call it SecondLayout. We do not need to update any fields in the layout, just need the layout ID for the next steps.
Note: This is only valid for this template /sitecore/templates/Foundation/JavaScript Services/JSS Layout

2. Create Layout Factoy

Create a file src\layout-factory.ts

import { RouteData } from '@sitecore-jss/sitecore-jss-nextjs';
import Layout from 'src/layouts/Layout';
import SecondLayout from 'src/layouts/SecondLayout';

const layoutMap = new Map();
layoutMap.set('{F7D5F0B5-CAB0-417D-AA18-E2A4E2BFF4BF}', SecondLayout);
layoutMap.set('default', Layout);

export function resolveLayout(routeData: RouteData) {
  const layoutId = `{${routeData?.layoutId?.toUpperCase()}}`;
  const layout = layoutMap.get(layoutId);
  return layout || layoutMap.get('default');
}

https://gist.github.com/zaheer-tariq/08710c74ea6de74c975b7ba5aca9b7ed

3. Create layout folder, move existing layout and create new layout file

  1. Create a new layouts folder src\layouts
  2. Move the existing src\Layout.tsx file to src\layouts folder and duplicate this file to create new src\layouts\SecondLayout.tsx
  3. Update the src\layouts\SecondLayout.tsx to rename const and export
import React from 'react';
import Head from 'next/head';
import {
  Placeholder,
  getPublicUrl,
  LayoutServiceData,
  Field,
} from '@sitecore-jss/sitecore-jss-nextjs';
import Navigation from 'src/Navigation';
import Scripts from 'src/Scripts';

// Prefix public assets with a public URL to enable compatibility with Sitecore editors.
// If you're not supporting Sitecore editors, you can remove this.
const publicUrl = getPublicUrl();

interface LayoutProps {
  layoutData: LayoutServiceData;
}

interface RouteFields {
  [key: string]: unknown;
  pageTitle: Field;
}

const SecondLayout = ({ layoutData }: LayoutProps): JSX.Element => {
  const { route } = layoutData.sitecore;

  const fields = route?.fields as RouteFields;

  return (
    <>
      <Scripts />
      <Head>
        <title>{fields.pageTitle.value.toString() || 'Page'}</title>
        <link rel="icon" href={`${publicUrl}/favicon.ico`} />
      </Head>

      <Navigation />
      {/* root placeholder for the app, which we add components to using route data */}
      <div className="container">{route && <Placeholder name="jss-main" rendering={route} />}</div>
    </>
  );
};

export default SecondLayout;

4. Update paths file to use the layout factory

Update src\pages[[…path]].tsx so that it uses the layout to determine the layout at runtime. Changes are on Line 17 and 34

import { useEffect } from 'react';
import { GetStaticPaths, GetStaticProps } from 'next';
import NotFound from 'src/NotFound';
import {
  RenderingType,
  SitecoreContext,
  ComponentPropsContext,
  handleEditorFastRefresh,
  EditingComponentPlaceholder,
  StaticPath,
} from '@sitecore-jss/sitecore-jss-nextjs';
import { SitecorePageProps } from 'lib/page-props';
import { sitecorePagePropsFactory } from 'lib/page-props-factory';
// different componentFactory method will be used based on whether page is being edited
import { componentFactory, editingComponentFactory } from 'temp/componentFactory';
import { sitemapFetcher } from 'lib/sitemap-fetcher';
import { resolveLayout } from 'src/layout-factory';

const SitecorePage = ({ notFound, componentProps, layoutData }: SitecorePageProps): JSX.Element => {
  useEffect(() => {
    // Since Sitecore editors do not support Fast Refresh, need to refresh editor chromes after Fast Refresh finished
    handleEditorFastRefresh();
  }, []);

  if (notFound || !layoutData.sitecore.route) {
    // Shouldn't hit this (as long as 'notFound' is being returned below), but just to be safe
    return <NotFound />;
  }

  const isEditing = layoutData.sitecore.context.pageEditing;
  const isComponentRendering =
    layoutData.sitecore.context.renderingType === RenderingType.Component;

  const Layout = resolveLayout(layoutData.sitecore?.route);
  return (
    <ComponentPropsContext value={componentProps}>
      <SitecoreContext
        componentFactory={isEditing ? editingComponentFactory : componentFactory}
        layoutData={layoutData}
      >
        {/*
          Sitecore Pages supports component rendering to avoid refreshing the entire page during component editing.
          If you are using Experience Editor only, this logic can be removed, Layout can be left.
        */}
        {isComponentRendering ? (
          <EditingComponentPlaceholder rendering={layoutData.sitecore.route} />
        ) : (
          <Layout layoutData={layoutData} />
        )}
      </SitecoreContext>
    </ComponentPropsContext>
  );
};

// This function gets called at build and export time to determine
// pages for SSG ("paths", as tokenized array).
export const getStaticPaths: GetStaticPaths = async (context) => {
  // Fallback, along with revalidate in getStaticProps (below),
  // enables Incremental Static Regeneration. This allows us to
  // leave certain (or all) paths empty if desired and static pages
  // will be generated on request (development mode in this example).
  // Alternatively, the entire sitemap could be pre-rendered
  // ahead of time (non-development mode in this example).
  // See https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration

  let paths: StaticPath[] = [];
  let fallback: boolean | 'blocking' = 'blocking';

  if (process.env.NODE_ENV !== 'development' && !process.env.DISABLE_SSG_FETCH) {
    try {
      // Note: Next.js runs export in production mode
      paths = await sitemapFetcher.fetch(context);
    } catch (error) {
      console.log('Error occurred while fetching static paths');
      console.log(error);
    }

    fallback = process.env.EXPORT_MODE ? false : fallback;
  }

  return {
    paths,
    fallback,
  };
};

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation (or fallback) is enabled and a new request comes in.
export const getStaticProps: GetStaticProps = async (context) => {
  const props = await sitecorePagePropsFactory.create(context);

  // Check if we have a redirect (e.g. custom error page)
  if (props.redirect) {
    return {
      redirect: props.redirect,
    };
  }

  return {
    props,
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every 5 seconds
    revalidate: 5, // In seconds
    notFound: props.notFound, // Returns custom 404 page with a status code of 404 when true
  };
};

export default SitecorePage;