Dynamic routing with Kentico Kontent and NextJS
TechFebruary 4, 2021

Dynamic routing with Kentico Kontent and NextJS

Most headless Content Management Systems don't have an integrated way to structure a site's navigation. Kentico Kontent has recently released Spotlight, which is designed to fill this gap. In this article we will look into Spotlight and what it can do for you. Followed by a technical in-depth example of how to integrate Spotlight with NextJS, which is a static site generator.

Spotlight

Most headless content management systems are build to be used as an omni-channel content hub. It is usually possible to implement a site's navigation structure by using linked items on the content items, but this is not intuitive and can be cumbersome to manage in terms of UX. This is where Kentico Kontent's Spotlight stands out. Kentico Kontent is also build as a content hub, but by installing the Spotlight add-on it adds a view that can be used to manage the navigation structure. It also give the ability to do in-context editing, which is a feature highly appreciated by content editors.

In-context editingIn-context editing

How it works

Spotlight adds a couple of new content types to your project. Most important is the Page content type. Among other fields, the Page content type has a linked items field called subpages, that is used to link to the sub pages of that page. Besides content types, Spotlight adds a new view that shows a tree of the navigation structure, which is based on this field. Clicking a page in the navigation structure will show the field editor, but it is also possible to open the page in a preview mode. This mode will show the actual page and allows you to do in-content editing of the page.

Another field added by the Page content type is the content field. This field points to the content item that should be shown on the page. As a best practice, pages should only be placeholders for the page and should point to a content item. Pages are specific for the site channel and separating the actual content from this page, will allow you to reuse that content for different channels.

As a best practice, pages should only be placeholders for the page and should point to a content item

NextJS

At Unplatform we are a big fan of NextJS, which is a React based Static Site Generator that also supports doing Server Side Rendering and Incremental Static Generation. NextJS has paged based routing, which allows you to add routes by adding pages to the source code. For example, adding a service.js to the pages folder will render the page at http://yoursite/service.

Dynamic routes

NextJS also supports dynamic routes. This is useful if the data is defined somewhere else than in the source code, for example in a Content Management System or in a Commerce System. As an example, let's create a simple product detail page that displays the name of the products. First we need to add the page. By creating a new product folder in the pages folder and adding a file named [id].js, the products will be available at http://yoursite/product/[id].

To display the name we add a simple React component, that receives the product name in the props:

export default function ProductDetailPage({name}) {
    return <div>{name}</div>
}

To inform NextJS what pages to generate for the products route, the file also needs to have a getStaticPaths function.

export async function getStaticPaths() {

    // Invoke the commerce system to get the product ids
    const productIds = GetProductsIds();

    return {
        paths: productIds.map(pId => ({
            params: {id: pId}
        }))
    };
}

NextJS invokes this function during build time and generate all the product pages. Finally, to pass the name prop to the ProductDetailPage component, we need to add a getStaticProps function to the file. This function is also invoked during build time, but for every product detail page. The product id that returned from the getStaticPaths function is passed in the params property.

export async function getStaticProps({params, preview}) {
    // Invoke commerce solution to get the product
    const product = getProduct(params.id)

    // The props field of the result is passed into the component
    return {
        props: {
            name: product.name
        }
    }
}

This is the final result of the product detail page, that will render the product name for every page and will be available on the dynamic products route:

export default function ProductDetailPage({name}) {
    return <div>{name}</div>
}

export async function getStaticProps({params, preview}) {
    // Invoke commerce solution to get the product
    const product = getProduct(params.id)

    // The props field of the result is passed into the component
    return {
        props: {
            name: product.name
        }
    }
}

export async function getStaticPaths() {

    // Invoke the commerce system to get the product ids
    const productIds = GetProductsIds();

    return {
        paths: productIds.map(pId => ({
            params: {id: pId}
        }))
    };
}

Combining spotlight with NextJS

Consider the following site structure in Spotlight: A homepage with a self-service sub page and a customer care sub page. Both of them have a number of additional sub pages:

Site structure in Kentico KontentSite structure in Kentico Kontent

Every page has a url property. You could just enter the entire url here, for example: self-service/stores for the stores page. This is easier to map in the code, but less convenient for the content editor. Let's enter stores for the stores page and solve the mapping with code.

Mapping the spotlight structure to NextJS pages

To map the urls to pages in NextJS, we need to modify the getStaticPaths to retrieve the pages. To retrieve the structure from Kontent, you can use the Kontent Delivery SDK. In the following code, the pages are retrieved from Kontent and are recursively mapped to build the urls:

function getContentUrlsRecursive (item) {
    const url = item.url ? [item.url.value] : []

    const urls = [url, ...item.subpages.value.map(subpage => {
        var subpageUrls = getContentUrlsRecursive(subpage)
        
        return subpageUrls.map(sUrl => {
            return [...url, ...(typeof sUrl === 'string' ? [sUrl] : sUrl)]
        })
    })].reduce((a, b) => a.concat(b))

    return urls
}

export async function getStaticPaths() {
    const homepage = await deliveryClient.item('homepage')
        .depthParameter(10)
        .queryConfig({
        usePreviewMode: previewMode
        })

    var urls = getContentUrlsRecursive(homepage.item)

    return {
        paths: urls.map(c => ({
            params: {slug: c}
        }))
    }
}

NextJS requires every url to be an array containing the individual url parts. /service/stores is mapped as ['service', 'stores']. The mapping result looks like this:

[
  [ 'service' ],
  [ 'service', 'stores' ],
  [ 'service', 'technical-support' ],
  [ 'service', 'customer-support' ],
  [ 'service', 'latest-news' ],
  [ 'service', 'for-companies' ],
  [ 'service', 'global-conditions' ],
  [ 'customer-care' ],
  [ 'customer-care', 'help-center' ],
  [ 'customer-care', 'payment' ],
  [ 'customer-care', 'how-to-buy' ],
  [ 'customer-care', 'shipping-delivery' ],
  [ 'customer-care', 'international-product-policy' ],
  [ 'customer-care', 'how-to-return' ],
  [ 'customer-care', 'faq' ]
]

Generating the pages

The params property that is passed to the getStaticProps function will now contain the slug we generated in the getStaticPaths. For example, the page /service/stores will receive a slug with ['service', 'stores']. The challenge now is to retrieve the page in Kentico Kontent that matches this route. One way would be to retrieve the home page item and resolve the page based on the slug. With a recursive function, the page could be resolved like this:

function getPage(item, slug) {
    const next = item.subpages.value.find(c => c.url.value === slug[0])

    if(!next) {
        return item
    }

    return getPage(next, slug.splice(1))
}

We could then use this function to retrieve the page and return the needed props to the page component:

export async function getStaticProps(slug, preview) {
    const homepage = await getItemByCode('homepage', preview, 10)

    const page = getPage(homepage.item, slug)   

    return {
        props: {
            title: page.title.value,
            description: page.description.value
        }
    }
}

Another way to find the page using the slug, would be to somehow pass the Kontent codename from the getStaticPaths method to the getStaticProps method. Unfortunately, it's not possible to directly pass other data than the slug. One way to make it work is to write a mapping to disk in the getStaticPaths method and then read it in the getStaticProps, as described in this github issue. This solution can be useful when you need to map from seo url to category id, which is impossible otherwise.

Supporting different types of content pages

The content editors in your team probably won't be very excited if they can only use one type of content page, so let's add support for multiple. Next to the subpages field, the page content type of Spotlight pages also has a content field. This content field is used to point to the content item that should be displayed on that page. Usually, different content pages also require different fields and you create multiple content types for the different pages. Let's say we have a normal content type: 'content' and we a content type a hero: 'content_with_hero'. We will create different React components for displaying these content types. Let's put these type of components in a separate templates folder for organization sake. To use these components, we modify the root page component in [...slug].js and choose a component based on the type of the content item:

import Content from '../templates/content'
import ContentWithHero from '../templates/contentwithhero'

export default function ContentPage({type, ...other}) {
    if(type === 'content_with_hero') {
        return <ContentWithHero {...other}/>
    }
    
    return <Content {...other}/>
}

In-context editing

To support Spotlight's in-content editing functionality you can use NextJS's preview feature. By configuring preview urls in Kontent that point to the API created by following the NextJS preview tutorial, Spotlight will be able to display a preview version of the selected page. By also adding special data attributes to the HTML, Kontent will also display a edit button next to the fields in the preview mode.

Content editors will love Spotlight

If your project requires content editors to manage the site navigation, they will be very happy with Spotlight. Kentico Kontent in combination with Spotlight gives you all the benefits of a headless CMS without the usual concessions in terms of usability. It makes it a lot easier to manage the navigation and will give them the flexibility they are used to from all-in-one platforms. With this NextJS implementation they can manage the site navigation without depending on developers.

Want to know more?

Do you have a question, would you like to know more or would you like to see our exciting Jamstack storefront demo? Use our contact form or drop us a mail: info@unplatform.io.

Jonne Kats
Written by Jonne Kats
On February 4, 2021