Experience Sitecore ! | December 2023

Experience Sitecore !

More than 200 articles about the best DXP by Martin Miles

A crash course of Next.js: UI-related and environmental (part 2)

In part 1 we covered some fundamentals of Next.js – rendering strategies along with the nuances of getStaticProps, getStaticPaths, getServerSideProps as well as data fetching.

Now we are going to talk about UI-related things, such as layouts, styles and fonts, serving statics, as well as typescript and environmental variables.

Built-in CSS support

Importing global styles

To add global styles, the corresponding table must be imported into the pages/_app.js file (pay attention to the underscore):

// pages/_app.js
import './style.css'
// This export is mandatory by default
export default function App({ Component, pageProps }){
    return <Component {...pageProps} />
}

These styles will be applied to all pages and components in the application. Please note: to avoid conflicts, global styles can only be imported into pages/_app.js.

When building the application, all styles are combined into one minified CSS file.

Import styles from the node_modules directory

Styles can be imported from node_modules, here’s an example of importing global styles:

// pages/_app.js
import 'bootstrap/dist/css/bootstrap.min.css'
export default function App({ Component, pageProps }){
    return <Component {...pageProps} />
}

Example of importing styles for a third-party component:

// components/Dialog.js
import { useState } from 'react'
import { Dialog } from '@reach/dialog'
import VisuallyHidden from '@reach/visually-hidden'
import '@reach/dialog/styles.css'
export function MyDialog(props){
    const [show, setShow] = useState(false)
    const open = () => setShow(true)
    const close = () => setShow(false)
    return (
        <div>
            <button onClick={open} className='btn-open'>Expand</button>
            <Dialog>
                <button onClick={close} className='btn-close'>
                    <VisuallyHidden>Collapse</VisuallyHidden>
                    <span>X</span>
                </button>
                <p>Hello!</p>
            </Dialog>
        </div>
    )
}

Adding styles at the component level

Next.js supports CSS modules out of the box. CSS modules must be named as [name].module.css. They create a local scope for the corresponding styles, which allows you to use the same class names without the risk of collisions. A CSS module is imported as an object (usually called styles) whose keys are the names of the corresponding classes.

Example of using CSS modules:

/* components/Button/Button.module.css */
.danger {
    background-color:red;
    color: white;
}
// components/Button/Button.js
import styles from './Button.module.css'
export const Button = () => (
    <button className={styles.danger}>
        Remove
    </button>
)

When built, CSS modules are concatenated and separated into separate minified CSS files, which allows you to load only the necessary styles.

SASS support

Next.js supports .scss and .sass files. SASS can also be used at the component level (.module.scss and .module.sass). To compile SASS to CSS you need to have SASS installed:

npm install sass

The behavior of the SASS compiler can be customized in the next.config.js file, for example:

const path = require('path')
module.exports = {
    sassOptions: {
        includePaths: [path.join(__dirname, 'styles')]
    }
}

CSS-in-JS

You can use any CSS-in-JS solution in Next.js. The simplest example is to use inline styles:

export const Hi = ({ name }) => <p style={{ color: 'green' }}>Hello, {name}!</p>

Next.js also has styled-jsx support built-in:

export const Bye = ({ name }) => (
    <div>
        <p>Bye, {name}. See you soon!</p>
        <style jsx>{`
      div {
        background-color: #3c3c3c;
      }
      p {
        color: #f0f0f0;
      }
      @media (max-width: 768px) {
        div {
          backround-color: #f0f0f0;
        }
        p {
          color: #3c3c3c;
        }
      }
    `}</style>
        <style global jsx>{`
      body {
        margin: 0;
        min-height: 100vh;
        display: grid;
        place-items: center;
      }
    `}</style>
    </div>
)

Layouts

Developing a React application involves dividing the page into separate components. Many components are used across multiple pages. Let’s assume that each page uses a navigation bar and a footer:

// components/layout.js
import Navbar from './navbar'
import Footer from './footer'
export default function Layout({ children }){
    return (
        <>
            <Navbar />
            <main>{children}</main>
            <Footer />
        </>
    )
}

Examples

Single Layout

If an application only uses one layout, we can create a custom app and wrap the application in a layout. Since the layout component will be reused when pages change, its state (for example, input values) will be preserved:

// pages/_app.js
import Layout from '../components/layout'
export default function App({ Component, pageProps }){
    return (
        <Layout>
            <Component {...pageProps} />
        </Layout>
    )
}

Page-Level Layouts

The getLayout property of a page allows you to return a component for the layout. This allows you to define layouts at the page level. The returned function allows you to construct nested layouts:

// pages/index.js
import Layout from '../components/layout'
import Nested from '../components/nested'
export default function Page(){
    return {
        // ...
    }
}
Page.getLayout = (page) => (
    <Layout>
        <Nested>{page}</Nested>
    </Layout>
)
// pages/_app.js
export default function App({ Component, pageProps }){
    // use the layout defined ata page level, if exists
    const getLayout = Component.getLayout || ((page) => page)
    return getLayout(<Component {...pageProps} />)
}

When switching pages, the state of each of them (input values, scroll position, etc.) will be saved.

Use it with TypeScript

When using TypeScript, a new type is first created for the page that includes getLayout. You should then create a new type for AppProps that overwrites the Component property to allow the previously created type to be used:

// pages/index.tsx
importtype{ ReactElement } from 'react'
import Layout from '../components/layout'
import Nested from '../components/nested'

export default function Page(){
    return {
        // ...
    }
}
Page.getLayout = (page: ReactElement) => (
    <Layout>
        <Nested>{page}</Nested>
    </Layout>
)


// pages/_app.tsx
importtype{ ReactElement, ReactNode } from 'react'
importtype{ NextPage } from 'next'
importtype{ AppProps } from 'next/app'

type NextPageWithLayout = NextPage & {
    getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
    Component: NextPageWithLayout
}
export default function App({ Component, pageProps }: AppPropsWithLayout){
    const getLayout = Component.getLayout ?? ((page) => page)
    return getLayout(<Component  {...pageProps} />)
}

Fetching data

The data in the layout can be retrieved on the client side using useEffect or utilities like SWR. Because the layout is not a page, it currently cannot use getStaticProps or getServerSideProps:

import useSWR from 'swr'
import Navbar from './navbar'
import Footer from './footer'

export default function Layout({ children }){
    const { data, error } = useSWR('/data', fetcher)
    if (error) return <div>Error</div>
    if (!data) return <div>Loading...</div>
    return (
        <>
            <Navbar />
            <main>{children}</main>
            <Footer />
        </>
    )
}

Image component and image optimization

The Image component, imported from next/image, is an extension of the img HTML tag designed for the modern web. It includes several built-in optimizations to achieve good Core Web Vitals performance. These optimizations include the following:

  • performance improvement
  • ensuring visual stability
  • speed up page loading
  • ensuring flexibility (scalability) of images

Example of using a local image:

import Image from 'next/image'
import imgSrc from '../public/some-image.png'

export default function Home(){
    return (
        <>
            <h1>Home page</h1>
            <Image
                src={imgSrc}
                alt=""
                role="presentation"
            />
        </h1 >
    )
}

Example of using a remote image (note that you need to set the image width and height):

import Image from 'next/image'

export default function Home(){
    return (
        <>
            <h1>Home page</h1>
            <Image
                src="/some-image.png"
                alt=""
                role="presentation"
                width={500}
                height={500}
            />
        </h1 >
    )
}

Defining image dimensions

Image expects to receive the width and height of the image:

  • in the case of static import (local image), the width and height are calculated automatically
  • width and height can be specified using appropriate props
  • if the image dimensions are unknown, you can use the layout prop with the fill value

There are 3 ways to solve the problem of unknown image sizes:

  • Using fill layout mode: This mode allows you to control the dimensions of the image using the parent element. In this case, the dimensions of the parent element are determined using CSS, and the dimensions of the image are determined using the object-fit and object-position properties
  • image normalization: if the source of the images is under our control, we can add resizing to the image when it is returned in response to the request
  • modification of API calls: the response to a request can include not only the image itself but also its dimensions

Rules for stylizing images:

  • choose the right layout mode
  • use className – it is set to the corresponding img element. Please note: style prop is not passed
  • when using layout="fill" the parent element must have position: relative
  • when using layout="responsive" the parent element must have display: block

See below for more information about the Image component.

Font optimization

Next.js automatically embeds fonts in CSS at build time:

// before
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet" />

// after
<style data-href="https://fonts.googleapis.com/css2?family=Inter">
  @font-face {font-family:'Inter';font-style:normal...}
</style>

To add a font to the page, use the Head component, imported from next/head:

// pages/index.js
import Head from 'next/head'

export default function IndexPage(){
    return (
        <div>
            <Head>
                <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&display=optional" />
            </Head>
            <p>Hello world!</p>
        </div>
    )
}

To add a font to the application, you need to create a custom document:

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDoc extends Document {
    render() {
        return (
            <Html>
                <Head>
                    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&display=optional" />
                </Head>
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        )
    }
}

Automatic font optimization can be disabled:

// next.config.js
module.exports = {
    optimizeFonts: false
}

For more information about the Head component, see below.

Script Component

The Script component allows developers to prioritize the loading of third-party scripts, saving time and improving performance.

The script loading priority is determined using the strategy prop, which takes one of the following values:

  • beforeInteractive: This is for important scripts that need to be loaded and executed before the page becomes interactive. Such scripts include, for example, bot detection and permission requests. Such scripts are embedded in the initial HTML and run before the rest of the JS
  • afterInteractive: for scripts that can be loaded and executed after the page has become interactive. Such scripts include, for example, tag managers and analytics. Such scripts are executed on the client side and run after hydration
  • lazyOnload: for scripts that can be loaded during idle periods. Such scripts include, for example, chat support and social network widgets

Note:

  • Script supports built-in scripts with afterInteractive and lazyOnload strategies
  • inline scripts wrapped in Script must have an id attribute to track and optimize them

Examples

Please note: the Script component should not be placed inside a Head component or a custom document.

Loading polyfills:

import Script from 'next/script'

export default function Home(){
    return (
        <>
            <Script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserverEntry%2CIntersectionObserver" strategy="beforeInteractive" />
        </>
    )
}

Lazy load:

import Script from 'next/script'

export default function Home(){
    return (
        <>
            <Script src="https://connect.facebook.net/en_US/sdk.js"strategy="lazyOnload" />
        </>
    )
}

Executing code after the page has fully loaded:

import { useState } from 'react'
import Script from 'next/script'

export default function Home(){
    const [stripe, setStripe] = useState(null)
    return (
        <>
            <Script
                id="stripe-js"
                src="https://js.stripe.com/v3/"
                onLoad={() => {
                    setStripe({ stripe: window.Stripe('pk_test_12345') })
                }}
            />
        </>
    )
}

Inline scripts:

import Script from 'next/script'

<Script id="show-banner" strategy="lazyOnload">
{`document.getElementById('banner').classList.remove('hidden')`}
</Script>

// or
<Script
  id="show-banner"
  dangerouslySetInnerHTML={{
    __html: `document.getElementById('banner').classList.remove('hidden')`
  }}
/>
Passing attributes:
import Script from 'next/script'

export default function Home(){
    return (
        <>
            <Script
                src="https://www.google-analytics.com/analytics.js"
                id="analytics"
                nonce="XUENAJFW"
                data-test="analytics"
            />
        </>
    )
}

Serving static files

Static resources should be placed in the public directory, located in the root directory of the project. Files located in the public directory are accessible via the base link /:

import Image from 'next/image'

export default function Avatar(){
    return <Image src="/me.png" alt="me" width="64" height="64" >
}

This directory is also great for storing files such as robots.txt, favicon.png, files necessary for Google site verification and other static files (including .html).

Real-time update

Next.js supports real-time component updates while maintaining local state in most cases (this only applies to functional components and hooks). The state of the component is also preserved when errors (non-rendering related) occur.

To reload a component, just add // @refresh reset anywhere.

TypeScript

Next.js supports TypeScript out of the box. There are special types GetStaticProps, GetStaticPaths and GetServerSideProps for getStaticProps, getStaticPaths and getServerSideProps:

import { GetStaticProps, GetStaticPaths, GetServerSideProps } from 'next'

export const getStaticProps: GetStaticProps = async (context) => {
    // ...
}
export const getStaticPaths: GetStaticPaths = async () => {
    // ...
}
export const getServerSideProps: GetServerSideProps = async (context) => {
    // ...
}

An example of using built-in types for the routing interface (API Routes):

import type{ NextApiRequest, NextApiResponse } from 'next'

export default(req: NextApiRequest, res: NextApiResponse)=> {
    res.status(200).json({ message: 'Hello!' })
}

There’s nothing prevents us from typing the data contained in the response:

import type{ NextApiRequest, NextApiResponse } from 'next'
type Data = {
    name: string
}

export default(req: NextApiRequest, res: NextApiResponse < Data >)=> {
    res.status(200).json({ message: 'Bye!' })
}

Next.js supports paths and baseUrl settings defined in tsconfig.json.

Environment Variables

Next.js has built-in support for environment variables, which allows you to do the following:

use .env.local to load variables

extrapolate variables to the browser using the NEXT_PUBLIC_ prefix

Assume we have .env.local file, as below:

DB_HOST=localhost
DB_USER=myuser
DB_PASS=mypassword

This will automatically load process.env.DB_HOST, process.env.DB_USER and process.env.DB_PASS into the Node.js runtime

// pages/index.js
export async function getStaticProps(){
    const db = await myDB.connect({
        host: process.env.DB_HOST,
        username: process.env.DB_USER,
        password: process.env.DB_PASS
    })
    // ...
}

Next.js allows you to use variables inside .env files:

HOSTNAME=localhost
PORT=8080
HOST=http://$HOSTNAME:$PORT

To pass an environment variable to the browser, you need to add the NEXT_PUBLIC_ prefix to it:

NEXT_PUBLIC_ANALYTICS_ID=value
// pages/index.js
import setupAnalyticsService from '../lib/my-analytics-service'
setupAnalyticsService(process.env.NEXT_PUBLIC_ANALYTICS_ID)

function HomePage() {
    return <h1>Hello world!</h1>
}

export default HomePage

In addition to .env.local, you can create .env for both modes, .env.development (for development mode), and .env.production (for production mode) files. Please note: .env.local always takes precedence over other files containing environment variables.

This concludes part 2. In part 3 we’ll talk about routing with Next.js – pages, API Routes, layouts, Middleware, and caching.









A crash course of Next.js: rendering strategies and data fetching (part 1)

This series is my Next.js study resume, and despite it’s keen to a vanilla Next.js, all the features are applicable with Sitecore SDK. It is similar to the guide I recently wrote about GraphQL and aims reducing the learning curve for those switching to it from other tech stack.

Next.js is a React-based framework designed for developing web applications with functionality beyond SPA. As you know, the main disadvantage of SPA is problems with indexing pages of such applications by search robots, which negatively affects SEO. Recently the situation has begun to change for the better, at least the pages of my small SPA-PWA application started being indexed as normal. However, Next.js significantly simplifies the process of developing multi-page and hybrid applications. It also provides many other exciting features I am going to share with you over this course.

Pages

In Next.js a page is a React component that is exported from a file with a .js, .jsx, .ts, or .tsx extension located in the pages directory. Each page is associated with a route by its name. For example, the page pages/about.js will be accessible at /about. Please note that the page should be exported by default using export default:

export default function About(){
  return <div>About us</div>
}

The route for pages/posts/[id].js will be dynamic, i.e. such a page will be available at posts/1, posts/2, etc.

By default, all pages are pre-rendered. This results in better performance and SEO. Each page is associated with a minimum amount of JS. When the page loads, JS code runs, which makes it interactive (this process is called hydration).

There are 2 forms of pre-rendering: static generation (SSG, which is the recommended approach) and server-side rendering (SSR, which is more familiar for those working with other serverside frameworks such as ASP.NET). The first form involves generating HTML at build time and reusing it on every request. The second is markup generation on a server for each request. Generating static markup is the recommended approach for performance reasons.

In addition, you can use client-side rendering, where certain parts of the page are rendered by client-side JS.

SSG

It can generate both pages with data and without.

Without data case is pretty obvious:

export default function About(){
  return <div>About us</div>
}

There are 2 potential scenarios to generate a static page with data:

  1. Page content depends on external data: getStaticProps is used
  2. Page paths depend on external data: getStaticPaths is used (usually in conjunction with getStaticProps)

1. Page content depends on external data

Let’s assume that the blog page receives a list of posts from an external source, like a CMS:

// TODO: requesting `posts`
export default function Blog({ posts }){
    return (
        <ul>
            {posts.map((post) => (
                <li key={post.id}>{post.title}</li>
            ))}
        </ul>
    )
}

To obtain the data needed for pre-rendering, the asynchronous getStaticProps function must be exported from the file. This function is called during build time and allows you to pass the received data to the page in the form of props:

export default function Blog({ posts }){
    // ...
}

export async function getStaticProps(){
    const posts = await(awaitfetch('https://site.com/posts'))?.json()
    // the below syntax is important
    return {
        props: {
            posts
        }
    }
}

2. Page paths depend on external data

To handle the pre-rendering of a static page whose paths depend on external data, an asynchronous getStaticPaths function must be exported from a dynamic page (for example, pages/posts/[id].js). This function is called at build time and allows you to define prerendering paths:

export default function Post({ post }){
    // ...
}
export async function getStaticPaths(){
    const posts = await(awaitfetch('https://site.com/posts'))?.json()
    // pay attention to the structure of the returned array
    const paths = posts.map((post) => ({
        params: { id: post.id }
    }))
    // `fallback: false` means that 404 uses alternative route
    return {
        paths,
        fallback: false
    }
}

pages/posts/[id].js should also export the getStaticProps function to retrieve the post data with the specified id:

export default function Post({ post }){
    // ...
}
export async function getStaticPaths(){
    // ...
}
export async function getStaticProps({ params }){
    const post = await(awaitfetch(`https://site.com/posts/${params.id}`)).json()
    return {
        props: {
            post
        }
    }
}

SSR

To handle server-side page rendering, the asynchronous getServerSideProps function must be exported from the file. This function will be called on every page request.

function Page({ data }){
    // ...
}
export async function getServerSideProps(){
    const data = await(awaitfetch('https://site.com/data'))?.json()
    return {
        props: {
            data
        }
    }
}

Data Fetching

There are 3 functions to obtain the data needed for pre-rendering:

  • getStaticProps (SSG): Retrieving data at build time
  • getStaticPaths (SSG): Define dynamic routes to pre-render pages based on data
  • getServerSideProps (SSR): Get data on every request

getStaticProps

The page on which the exported asynchronous getStaticProps is pre-rendered using the props returned by this function.

export async function getStaticProps(context){
    return {
        props: {}
    }
}

context is an object with the following properties:

    • params – route parameters for pages with dynamic routing. For example, if the page title is [id].js, the params will be { id: … }
    • preview – true if the page is in preview mode
    • previewData – data set using setPreviewData
    • locale – current locale (if enabled)
    • locales – supported locales (if enabled)
    • defaultLocale – default locale (if enabled)

getStaticProps returns an object with the following properties:

  • props – optional object with props for the page
  • revalidate – an optional number of seconds after which the page is regenerated. The default value is false – regeneration is performed only with the next build
  • notFound is an optional boolean value that allows you to return a 404 status and the corresponding page, for example:
export async function getStaticProps(context) {
    const res = awaitfetch('/data')
    const data = await res.json()
    if (!data) {
        return {
            notFound: true
        }
    }
    return {
        props: {
            data
        }
    }
}

Note that notFound is not required in fallback: false mode, since in this mode only the paths returned by getStaticPaths are pre-rendered.

Also, note that notFound: true means a 404 is returned even if the previous page was successfully generated. This is designed to support cases where user-generated content is removed.

  • redirect is an optional object that allows you to perform redirections to internal and external resources, which must have the form {destination: string, permanent: boolean}:
export async function getStaticProps(context){
    const res = awaitfetch('/data')
    const data = await res.json()
    if (!data) {
        return {
            redirect: {
                destination: '/',
                permanent: false
            }
        }
    }
    return {
        props: {
            data
        }
    }
}

Note 1: Build-time redirects are not currently allowed. Such redirects must be declared at next.config.js.

Note 2: Modules imported at the top level for use within getStaticProps are not included in the client assembly. This means that server code, including reads from the file system or from the database, can be written directly in getStaticProps.

Note 3: fetch() in getStaticProps should only be used when fetching resources from external sources.

Use Cases

  • rendering data is available at build time and does not depend on the user request
  • data comes from a headless CMS
  • data can be cached in plain text (and not user-specific data)
  • the page must be pre-rendered (for SEO purposes) and must be very fast – getStaticProps generates HTML and JSON files that can be cached using a CDN

Use it with TypeScript:

import { GetStaticProps } from 'next'
export const getStaticProps: GetStaticProps = async (context) => { }

To get the desired types for props you should use InferGetStaticPropsType<typeof getStaticProps>:

import { InferGetStaticPropsType } from 'next'
type Post = {
    author: string
    content: string
}
export const getStaticProps = async () => {
    const res = awaitfetch('/posts')
    const posts: Post[] = await res.json()
    return {
        props: {
            posts
        }
    }
}
export default function Blog({ posts }: InferGetStaticPropsType < typeof getStaticProps >){
    // posts will be strongly typed as `Post[]`
}

ISR: Incremental static regeneration

Static pages can be updated after the application is built. Incremental static regeneration allows you to use static generation at the individual page level without having to rebuild the entire project.

Example:

const Blog = ({ posts }) => (
    <ul>
        {posts.map((post) => (
            <li>{post.title}</li>
        ))}
    </ul>
)
// Executes while building on a server.
// It can be called repeatedly as a serverless function when invalidation is enabled and a new request arrives
export async function getStaticProps(){
    const res = awaitfetch('/posts')
    const posts = await res.json()
    return {
        props: {
            posts
        },
        // `Next.js` will try regenerating a page:
        // - when a new request arrives
        // - at least once every 10 seconds
        revalidate: 10// in seconds
    }
}
// Executes while building on a server.
// It can be called repeatedly as a serverless function if the path has not been previously generated
export async function getStaticPaths(){
    const res = awaitfetch('/posts')
    const posts = await res.json()
    // Retrieving paths for posts pre-rendering
    const paths = posts.map((post) => ({
        params: { id: post.id }
    }))
    // Only these paths will be pre-rendered at build time
    // `{ fallback: 'blocking' }` will render pages serverside in the absence of a corresponding path
    return { paths, fallback: 'blocking' }
}
export default Blog

When requesting a page that was pre-rendered at build time, the cached page is displayed.

  • The response to any request to such a page before 10 seconds have elapsed is also instantly returned from the cache
  • After 10 seconds, the next request also receives a cached version of the page in response
  • After this, page regeneration starts in the background
  • After successful regeneration, the cache is invalidated and a new page is displayed. If regeneration fails, the old page remains unchanged

Technical nuances

getStaticProps

  • Since getStaticProps runs at build time, it cannot use data from the request, such as query params or HTTP headers.
  • getStaticProps only runs on the server, so it cannot be used to access internal routes
  • when using getStaticProps, not only HTML is generated, but also a JSON file. This file contains the results of getStaticProps and is used by the client-side routing mechanism to pass props to components
  • getStaticProps can only be used in a page component. This is because all the data needed to render the page must be available
  • in development mode getStaticProps is called on every request
  • preview mode is used to render the page on every request

getStaticPaths

Dynamically routed pages from which the asynchronously exported getStaticPaths function will be pre-generated for all paths returned by that function.

export async function getStaticPaths(){
    return {
        paths: [
            params: {}
        ],
        fallback: true | false | 'blocking'
    }
}
Paths

paths defines which paths will be pre-rendered. For example, if we have a page with dynamic routing called pages/posts/[id].js, and the getStaticPaths exported on that page returns paths as below:

return {
    paths: [
        { params: { id: '1' } },
        { params: { id: '2' } },
    ]
}

Then the posts/1 and posts/2 pages will be statically generated based on the pages/posts/[id].js component.

Please note that the name of each params must match the parameters used on the page:

  • if the page title is pages/posts/[postId]/[commentId] then params should contain postId and commentId
  • if the page uses a route interceptor, for example, pages/[...slug], params must contain slug as an array. For example, if such an array looks as ['foo', 'bar'], then the page /foo/bar will be generated
  • If the page uses an optional route interceptor, using null, [], undefined, or false will cause the top-level route to be rendered. For example, applying slug: false to pages/[[...slug]], will generate the page /
Fallback
  • if fallback is false, the missing path will be resolved by a 404 page
  • if fallback is true, the behavior of getStaticProps will be:
      • paths from getStaticPaths will be generated at build time using getStaticProps
      • a missing path will not be resolved by a 404 page. Instead, a fallback page will be returned in response to the request
      • The requested HTML and JSON are generated in the background. This includes calling getStaticProps
      • the browser receives JSON for the generated path. This JSON is used to automatically render the page with the required props. From the user’s perspective, this looks like switching between the backup and full pages
    • the new path is added to the list of pre-rendered pages

Please note: fallback: true is not supported when using next export.

Fallback pages

In the fallback version of the page:

  • prop pages will be empty
  • You can determine that a fallback page is being rendered using the router: router.isFallback will be true
// pages/posts/[id].js
import { useRouter } from 'next/router'
function Post({ post }){
    const router = useRouter()
    // This will be displayed if the page has not yet been generated, 
    // Until `getStaticProps` finishes its work
    if (router.isFallback) {
        return <div>Loading...</div>
    }
    // post rendering
}
export async function getStaticPaths(){
    return {
        paths: [
            { params: { id: '1' } },
            { params: { id: '2' } }
        ],
        fallback: true
    }
}
export async function getStaticProps({ params }){
    const res = awaitfetch(`/posts/${params.id}`)
    const post = await res.json()
    return {
        props: {
            post
        },
        revalidate: 1
    }
}
export default Post

In what cases might fallback: true be useful? It can be useful with a truly large number of static pages that depend on data (for example, a very large e-commerce storefront). We want to pre-render all the pages, but we know the build will take forever.

Instead, we generate a small set of static pages and use fallback: true for the rest. When requesting a missing page, the user will see a loading indicator for a while (while getStaticProps doing its job), then see the page itself. After that, a new page will be returned in response to each request.

Please note: fallback: true does not refresh the generated pages. Incremental static regeneration is used for this purpose instead.

If fallback is set to blocking, the missing path will also not be resolved by the 404 page, but there will be no transition between the fallback and normal pages. Instead, the requested page will be generated on the server and sent to the browser, and the user, after waiting for some time, will immediately see the finished page

Use cases for getStaticPaths

getStaticPaths is used to pre-render pages with dynamic routing. Use it with TypeScript:

import { GetStaticPaths } from 'next'

export const getStaticPaths: GetStaticPaths = async () => { }

Technical nuances:

  • getStaticPaths must be used in conjunction with getStaticProps. It cannot be used in conjunction with getServerSideProps
  • getStaticPaths only runs on the server at build time
  • getStaticPaths can only be exported in a page component
  • in development mode getStaticPaths runs on every request

getServerSideProps

The page from which the asynchronous getServerSideProps function is exported will be rendered on every request using the props returned by this function.

export async function getServerSideProps(context){
    return {
        props: {}
    }
}

context is an object with the following properties:

  • params: see getStaticProps
  • req: HTTP IncomingMessage object (incoming message, request)
  • res: HTTP response object
  • query: object representation of the query string
  • preview: see getStaticProps
  • previewData: see getStaticProps
  • resolveUrl: a normalized version of the requested URL, with the _next/data prefix removed and the original query string values included
  • locale: see getStaticProps
  • locales: see getStaticProps
  • defaultLocale: see getStaticProps

getServerSideProps should return an object with the following fields:

  • props – see getStaticProps
  • notFound – see getStaticProps
    export async function getServerSideProps(context){
        const res = awaitfetch('/data')
        const data = await res.json()
        if (!data) {
            return {
                notFound: true
            }
        }
        return {
            props: {}
        }
    }
  • redirect — see getStaticProps
    export async function getServerSideProps(context){
        const res = awaitfetch('/data')
        const data = await res.json()
        if (!data) {
            return {
                redirect: {
                    destination: '/',
                    permanent: false
                }
            }
        }
        return {
            props: {}
        }
    }

For getServerSideProps there are the same features and limitations as getStaticProps.

Use cases for getServerSideProps

getServerSideProps should only be used when you need to pre-render the page based on request-specific data. Use it with TypeScript:

import { GetServerSideProps } from 'next'
export const getServerSideProps: GetServerSideProps = async () => { }

To get the expected types for props you should use InferGetServerSidePropsType<typeof getServerSideProps>:

import { InferGetServerSidePropsType } from 'next'
type Data = {}
export async functiong etServerSideProps(){
    const res = awaitfetch('/data')
    const data = await res.json()
    return {
        props: {
            data
        }
    }
}
function Page({ data }: InferGetServerSidePropsType <typeof getServerSideProps>){
    // ...
}
export default Page

Technical nuances:

  • getServerSideProps runs only serverside
  • getServerSideProps can only be exported in a page component

Client-side data fetching

If a page has frequently updated data, but at the same time this page doesn’t need to be pre-rendered (for SEO reasons), then it is pretty much possible to fetch its data directly at client-side.

The Next.js team recommends using their useSWR hook for this purpose, which provides features such as data caching, cache invalidation, focus tracking, periodic retrying, etc.

import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((res) => res.json())
function Profile(){
    const { data, error } = useSWR('/api/user', fetcher)
    if (error) return <div>Error while retrieving the data</div>
    if (!data) return <div>Loading...</div>
    return <div>Hello, {data.name}!</div>
}

However, you’re not limited to it, old good React query fetch() functions also perfectly work for this purpose.

This concludes part 1. In part 2 we’ll talk about UI-related things coming to OOB with Next.js – layouts, styles, and fonts powerful features, Image and Script components, and of course – TypeScript.

A full guide to creating a multi-language sites with Sitecore XM Cloud and Next.js

Historically, it was quite challenging to add custom languages to the sitecore, as it was dependent on the cultures registered in the .net framework on the OS level. Of course, there were a few workarounds like registering the custom culture on Windows, but it only added other challenges for scenarios such as having more than one Content Delivery Server.

Luckily, both XM Cloud and SXA changed the way we deal with it, not to mention retiring CD servers. I am going to show the entire walkthrough, in action – everything you need to do on the CM side of things and your Next.js head application, on an example of a custom component. So, here we go!

In the beginning, I only had a basic website running in a multi-site SXA-based environment. Because of that it benefits from a responsive Header component with navigation items. It will be a good place to locate a language dropdown selector, as that’s where users traditionally expect it to be. But first, let’s add languages into the system as English is the default and the only one I have so far. Since I live in North America, I am going to add two most popular languages – French and Spanish, as commonly spoken in Quebec and Mexico correspondingly.

Adding Languages

In XM Cloud, languages are located under /sitecore/system/Languages folder. If a language is not present there, you won’t be able to use it, which is my case. I like the functional language selector dialog provided by the system:

Adding language into the system

Pro tip: don’t forget to add languages into serialization.

After the language lands into a system, we can add it to a specific website. I enjoy plenty of scaffolding in SXA is based on SPE scripts and here’s a case. To add the site language, choose Scripts from a context menu, then select Add Site Language:

02

Then specify the language of choice, as well as some parameters, including the language fallback option to the defaults.

03

In XM Cloud you can find a new Custom Culture section on the language item, which has two important fields:

  • Base ISO Culture Code: the base language you want to use, for example: en
  • Fallback Region Display Name: display name that can be used in the content editor or in the Sitecore pages.

Now both the system and website have these new languages. The next step would be introducing a drop-down language selector, at the top right corner of a header.

Unlike the traditional non-headless versions of XP/XM platforms, XM Cloud is fully headless and serves the entire layout of a page item with all the components via Experience Edge, or a local GraphQL endpoint running on a local CM container with the same schema. Here’s what it looks like in GraphQL IDE Playground:

04

There are two parts to it: context which contains Sitecore-context useful information, such as path, site, editing mode, current language, and route with the entire layout of placeholders, components, and field values. Since the language selector is a part of the header and is shown on every single page, that would be really great (and logical) to provide the list of available languages to feed this component with a context. How can we achieve that?

The good news is pretty doable through Platform customization by extending getLayoutServiceContextpipeline and adding ContextExtension processor:

<?xml version="1.0"?>
<configuration
	xmlns:patch="http://www.sitecore.net/xmlconfig/"
	xmlns:set="http://www.sitecore.net/xmlconfig/set/"
	xmlns:role="http://www.sitecore.net/xmlconfig/role/">
	<sitecore>
		<pipelines>
			<groupgroupName="layoutService">
				<pipelines>
					<getLayoutServiceContext>
						<processortype="JumpStart.Pipelines.ContextExtension, JumpStart"resolve="true"/>
					</getLayoutServiceContext>
				</pipelines>
			</group>
		</pipelines>
	</sitecore>
</configuration>

and the implementation:

using System.Collections.Generic;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.JavaScriptServices.Configuration;
using Sitecore.LayoutService.ItemRendering.Pipelines.GetLayoutServiceContext;
namespace JumpStart.Pipelines
{
    public class ContextExtension : Sitecore.JavaScriptServices.ViewEngine.LayoutService.Pipelines.
            GetLayoutServiceContext.JssGetLayoutServiceContextProcessor
    {
        public ContextExtension(IConfigurationResolver configurationResolver) : base(configurationResolver)
        {
        }

        protected override void DoProcess(GetLayoutServiceContextArgs args, AppConfiguration application)
        {
            Assert.ArgumentNotNull(args, "args");
            var langVersions = new List<Language>();
            Item tempItem = Sitecore.Context.Item;
            foreach (var itemLanguage in tempItem.Languages)
            {
                var item = tempItem.Database.GetItem(tempItem.ID, itemLanguage);
                if (item.Versions.Count > 0 || item.IsFallback)
                {
                    langVersions.Add(itemLanguage);
                }
            }
            args.ContextData.Add("Languages", langVersions);
        }
    }
}

To make this code work we need to reference Sitecore.JavaScriptServices package. There was an issue that occurred after adding a package: the compiler errored out demanding to specify the exact version number of this package. It should be done at packages.props at the root of a mono repository as below:

<PackageReference Update="Sitecore.JavaScriptServices.ViewEngine" Version="21.0.583" />

After deploying, I am receiving site languages as a part of Sitecore context object for every single page:

08

If for some reason you do not see language in the graphQL output, but the one exists in both system and your site – make sure it has language fallback specified:

05

You also need to configure language fallback on a system, as per the official documentation. I ended up with this config patch:

<?xml version="1.0"?>
<configuration
	xmlns:patch="http://www.sitecore.net/xmlconfig/"
	xmlns:set="http://www.sitecore.net/xmlconfig/set/"
	xmlns:role="http://www.sitecore.net/xmlconfig/role/">
	<sitecore>
		<settings>
			<settingname="ExperienceEdge.EnableItemLanguageFallback"value="true"/>
			<settingname="ExperienceEdge.EnableFieldLanguageFallback"value="true"/>
		</settings>
		<sites>
			<sitename="shell">
				<patch:attributename="contentStartItem">/Jumpstart/Jumpstart/Home
				</patch:attribute>
				<patch:attributename="enableItemLanguageFallback">true
				</patch:attribute>
				<patch:attributename="enableFieldLanguageFallback">true
				</patch:attribute>
			</site>
			<sitename="website">
				<patch:attributename="enableItemLanguageFallback">true
				</patch:attribute>
				<patch:attributename="enableFieldLanguageFallback">true
				</patch:attribute>
			</site>
		</sites>
	</sitecore>
</configuration>

I also followed the documentation and configured language fallback on a page item level at the template’s standard values:

10

Now when I try to navigate to my page by typing https://jumpstart.localhost/es-MX/Demo, browser shows me 404. Why so?

07

This happens because despite we added languages in XM Cloud CM and even specified the fallback, the next.js head application knows nothing about these languages and cannot serve the corresponding routes. The good news is that the framework easily supports that by just adding served languages into next.config.js of a relevant JSS application:

i18n:{
  locales:[
    'en',
    'fr-CA',
    'es-MX',
],
  defaultLocale: jssConfig.defaultLanguage,
  localeDetection:false,
}

After the JSS app restarts and upon refreshing a page, there’s no more 404 error. If done right, you might already see a header.

But in my case the page was blank: no error, but no header. The reason for this is pretty obvious – since I benefit from reusable components by a truly multisite architecture of SXA, my header component belongs to a Partial Layout, which in turn belongs to a Shared website. Guess what? It does not have installed languages, so need to repeat language installation for the Shared site as well. Once done – all works as expected and you see the header from a shared site’s partial layout:

09

Dropdown Language Selector

Now, I decided to implement a dropdown Language Selector component at the top right corner of a header that picks up all the available languages from the context and allows switching. This will look something like the below:

import { SitecoreContextValue } from '@sitecore-jss/sitecore-jss-nextjs';
import { useRouter } from 'next/router';
import { ParsedUrlQueryInput } from 'querystring';
import { useEffect, useState } from 'react';
import { ComponentProps } from 'lib/component-props';
import styles from './LanguageSelector.module.css';

export type HeaderContentProps = ComponentProps & {
    pathname?: string;
    asPath?: string;
    query?: string | ParsedUrlQueryInput;
    sitecoreContext: SitecoreContextValue;
};
const LanguageSelector = (props: HeaderContentProps): JSX.Element => {
    const router = useRouter();
    const [languageLabels, setLanguageLabels] = useState([]);
    const sxaStyles = `${props.params?.styles || ''}`;
    const languageNames = new Intl.DisplayNames(['en'], { type: 'language' });
    const languageList = props.sitecoreContext['Languages'] as NodeJS.Dict<string | string>[];
    useEffect(() => {
        const labels = languageList.map((language) => languageNames.of(language['Name']));
        setLanguageLabels(labels);
    const changeLanguage = (lang: string) => {
    }, []);
        if (props.pathname && props.asPath && props.query) {
            router.push(
                {
                    pathname: props.pathname,
                    query: props.query,
                },
                props.asPath,
                {
                    locale: lang,
                    shallow: false,
                }
            );
        }
    };
    const languageSelector = languageList && languageLabels.length > 0 && (
        <select
            onChange={(e) => changeLanguage(e.currentTarget.value)}
            className="languagePicker"
            value={props.sitecoreContext.language}
        >
            {languageList.map((language, index) => (
                <option
                    key={index}
                    value={language['Name']}
                    label={languageLabels[index]}
                    className="languageItem"
                >
                    {languageNames.of(language['Name'])}
                </option>
            ))}
        </select>
    );
    return (
        <>
            <div className={`${styles.selector}${sxaStyles}`}>{languageSelector}</div>
        </>
    );
};
export default LanguageSelector;

Since I made the header responsive with a “hamburger” menu seen on mobiles, I am also referencing responsive styles for this component:

.selector {
    float: right;
    position: relative;
    top: 13px;
    right: 40px;
}

@mediascreenand (max-width: 600px) {
    .selector {
        right: 0px;
    }
}

Now it can be used from the header as:

<LanguageSelector pathname={pathname} asPath={asPath} query={query} sitecoreContext={sitecoreContext}{...props} />

and it indeed looks and works well:

11

switching to Spanish correctly leverages next.js for switching the language context and changing the URL:

12

Now, let’s progress with a multi-language website by adding a demo component and playing it over.

Adding a Component

For the sake of the experiment, I decided to gith something basic – an extended Rich Text component that in addition to a datasource also receives background color from Rendering Parameters. There are 3 lines with it:

  • the top line in bold is always static and is just a hardcoded name of the component, should not be translated
  • the middle line is internal to the component rendering, therefore I cannot take it from the datasource, so use Dictionary instead
  • the bottom one is the only line editable in Pages/EE and comes from the datasource item, the same as with the original RichText

Here’s what it looks like on a page:

13

And here’s its code (ColorRichText.tsx):

import React from 'react';
import { Field, RichText as JssRichText } from '@sitecore-jss/sitecore-jss-nextjs';
import { useI18n } from 'next-localization';
interface Fields {
    Text: Field<string>;
}
export type RichTextProps = {
    params: { [key: string]: string };
    fields: Fields;
};
export const Default = (props: RichTextProps): JSX.Element => {
    const text = props.fields ? (
        <JssRichText field={props.fields.Text} />
    ) : (
        <span className="is-empty-hint">Rich text</span>
    );
    const id = props.params.RenderingIdentifier;
    const { t } = useI18n();
    return (
        <div
            className={`component rich-text ${props.params.styles?.trimEnd()}`}
            id={id ? id : undefined}
        >
            <div className="component-content">
                <h4>Rich Text with Background Color from Rendering Parameters</h4>
                <span>{t('Rendering Parameters') || 'fallback content also seen in EE'}: </span>
                {text}
                <style jsx>{`
          .component-content {
            background-color: ${props.params.textColor
                        ? props.params.textColor?.trimEnd()
                        : '#FFF'};
          }
        `}</style>
            </div>
        </div>
    );
};

What is also special about this component, I am using I18n for reaching out to Dictionary items, see these lines:

import{ useI18n } from 'next-localization';
const{ t } = useI18n();
<span>{t('Rendering Parameters') || 'fallback content, it is also seen in EE when defaults not configured'}: </span>

Next, create a version of each language for the datasource item and provide the localized content. You have to create at least one version per language to avoid falling back to the default language – English. The same also applies to the dictionary item:

14

The result looks as below:

16

15

Rendering Parameters

Now, you’ve probably noticed that the English version of the component has a yellow background. That comes from rendering parameters in action configured per component so that editors can choose a desired background color from a dropdown (of course, it is a very oversimplified example for demo purposes).

22

What is interesting in localization is that you can also customize Rendering Parameters per language (or keep them shared by the language fallback).

Rendering Parameters are a bit tricky, as they are stored in the __Renderings and __Final Renderings fields of a page item (for Shared and Versioned layouts correspondingly), derived from Standard template (/sitecore/Templates/System/Templates/Standard template). That means when you come to a page with a language fallback, you cannot specify Rendering Parameters for that language unless you create a version of the entire page. Both Content Editor and EE will prevent you from doing that while there is a language fallback for the page item:

Content Editor

Experience Editor

Creating a new language version can be very excessive effort as by default it will make you set up the entire page layout add components (again) and re-assigning all the datasources for that version. It could be simplified by copying the desired layout from the versioned __Final Renderings field to the shared __Renderings field, so that each time you create a new language version for a page item, you “inherit” from that shared design and not create it from scratch, however, that approach also has some caveats – you may find some discussions around that (here and there).

In any case, we’ve got the desired result:

19

English version

20

French version

21

Spanish version

Hope you find this article helpful!