Advanced NextJs To Consume Layout Service

code

This is the third article in a series that explores the idea of creating a headless application, without the luxury of Sitecore Headless Services Module (Sitecore JSS).  It is a continuation from NextJs App to Consume Layout Service, where we created a NextJs from a boilerplate scaffold. Then added the minimum components and data functions to be able to call our Custom Layout Service and output each of the returned components in the required placeholders.

In this article, we will add the necessary functionality to pull out data for each component (from a variety of places) and insert it into meaningful HTML to deliver a headless site. If all goes well, our application will output similar results to the MVC equivalent shown here: Personalisation Demo Site – Dean OBrien


Supporting code for this article can be found here: https://github.com/deanobrien/nextjs-personalisation-for-sitecore


The steps we will take in this article are as follows:

  1. Add public assets
  2. Update the global layout file
  3. Add type definitions
  4. Update/Add data fetching functions

Add public assets

In order to make assets available outside of the normal NextJs App Routing, you need to place them in the public folder, which can be found in the root of the scaffold solution.

Therefore we need to create js and css sub folders and add any required files.

Update the global layout file

In the app folder you will find a layout.tsx file. This is the global layout file for the NextJs application. The file provides a shared layout for every page file in the application, where the {children} element is replaced with the relevant content coming from the page file.

Note: The only exception to this is if an additional layout file is added in a sub folder. That layout will then be used for pages in the same folder.

Whats cool about layout files, is that they are not reloaded when a user navigates between routes. Instead, only the contents of the page file is reloaded. So if you place all of the common components in that file (i.e. navigation and sidebars etc) then they will not refresh when people navigate. The added benefit is there is less data getting sent in the response, so load times are greatly improved.

To make the required changes, edit this file and add the following:

... to the imports 
import PlaceHolder from './Components/Core/PlaceHolder';
import { fetchPath } from '@/app/lib/data';

... update function props and get pathName
export default async function RootLayout({children,slug}: Readonly<{children: React.ReactNode;}>) {
	  const pathName = await fetchPath(slug)

... to the head ...
<link href="/css/bootstrap.min.css" rel="stylesheet"/>
<link href="/css/custom.css" rel="stylesheet"/>
        
        ... bring header and footer from page file
	    <PlaceHolder placeHolderName="BootStrap-Header" pathName={pathName} />
        {children}
		<PlaceHolder placeHolderName="bootstrap-footer" pathName={pathName} />

... before the closing body tag ...
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"></script>
<script src="/js/vendor/popper.min.js"></script>
<script src="/js/bootstrap.min.js"></script>

You can also update the meta data section at the top, providing a title (default and template), as well as a description.

Add type definitions

It is good practice to add type definitions for all the objects you use within your NextJs application. To do this, create a new file called definitions.ts and add definitions for SimpleNav, Component, ArticleProps, RenderingModel and PageItem (full details in github), Example below:

 export type PageItem = {
    Name: string;
    DisplayName: string;
    ItemId: string;
    TemplateName: string;
    TemplateID: string;
    Path: string;
    Parents: SimpleNav[];
    Children: SimpleNav[];
    Siblings: SimpleNav[];
    Fields: any;
};

Note: we still use ‘any’ (which is default) for things like Fields because at build time we dont know the format of these fields.

Update/Add data fetching functions

Add the following new functions to data.ts

fetchComponentFromCached – this function calls the layout service for the given path and then finds the desired component (from the returned collection of components) and returns it.
Note: its called ‘FromCached’ because each ‘fetch’ is cached within NextJs, so here we are using the cached call for full page data (rather than calling the API and only asking for single component).

export const fetchComponentFromCached = async (path: string, uid: string): Promise<Component> => {
	try {
		// Rather than make multiple calls for each component - make one that is cached
		const data = await getData(path,'',true,true,true,true);

        // Then query result
		return data.Route.Components.find((item: Component) => item.UID.endsWith(uid));	
        } catch (error) {
		throw ('Error on fetchComponent ' + uid + ' :' + error);
	}
};

fetchPageItemFromCached – this function calls the layout service for the given path and returns the response (non essential properties are stripped out – depending on params).

 export const fetchPageItemFromCached = async (path: string, includeParents: boolean, includeChildren: boolean, includeSiblings: boolean): Promise<PageItem> => {
	try {
		const data = await getData(path,'',true,true,true,true);
		if(!includeParents) data.Route.Parents=null;
		if(!includeChildren) data.Route.Children=null;
		if(!includeSiblings) data.Route.Siblings=null;
		data.Route.Components=[]
		return data.Route;
	} catch (error) {
		throw ('Error on fetchPageItem ' + path + ' :' + error);
	}
};

getQueryVariable – this function retrieves a parameter from a query string.
Note: In sitecore rendering parameters are stored as key value pairs in query string format. So we use this function to extract the values we need.

export const getQueryVariable = async(variable: any, queryString: any) : Promise<any> => {
	try {
             var vars = queryString.split('&');
             for (var i = 0; i < vars.length; i++) {
                 var pair = vars[i].split('=');
                 if (decodeURIComponent(pair[0]) == variable) {
                     return decodeURIComponent(pair[1]);
                 }
             }
	} catch (error) {
		throw ('Error on getQueryVariable '+variable+' :'+error);
	}
}

Summary

In this article we looked at using layout files to pull together common components and public assets. We also looked at type definitions and some additional functions we will need to dissect the relevant data from the layout services response.

In the next article, we will look at some examples that show these functions in action, together with some other common scenarios I came across when exploring creating a NextJs application.

Leave a Reply

Your email address will not be published. Required fields are marked *