Composing Navigation and Site Structure in Kontent.ai using Web Spotlight

Unlock the potential of Kontent.ai’s Web Spotlight to seamlessly bridge the gap between content and implementation, empowering content editors with a tactile experience in visualizing and structuring navigation.

Navigating through the world of headless CMS can be complex, especially when content is entirely detached from the website’s implementation. Traditional concepts of visualizing and structuring navigation become abstract for content editors without a direct view of the site they are building. However, Kontent.ai’s Web Spotlight serves as a bridge between conventional and headless CMS approaches, offering a way to bring content and implementation closer together.

Web Spotlight Features

Web Spotlight provides a visual representation of your site, creating a more tactile experience for content editors. It facilitates easy webpage creation, content updates, and component rearrangement without the need for developers. It also offers device-specific previews to ensure your pages look great before they go live.

Kontent.ai's Web Spotlight

In an article by fellow Kontent.ai MVP Andy Thompson, various strategies for leveraging Web Spotlight in Kontent.ai are discussed. This blog post aims to consolidate some of those strategies, focusing on configuring Web Spotlight to model both the pages and navigation of your website.

One significant advantage of separating site structure and navigation is the freedom it provides in visually constructing menus. Using Web Spotlight, we can create both navigation and site structure, and these ideas can be extended for more intricate scenarios with additional content models. For instance, having separate models for header and footer navigation or channel-specific sitemaps.

Code Implementation

The provided code snippets demonstrate the implementation using Next.js, but the concepts are applicable across different frameworks. One popular option is to utilize Web Spotlight to create a sitemap that mirrors the site’s hierarchical structure. This sitemap then becomes a powerful tool for routing and link retrieval. The code below includes a function that recursively traverses all subpages of a page, yielding paths to the leaves of the tree.

Additionally, there’s a mechanism for defining routes, retrieving paths for static generation, and obtaining specific page data in the getStaticProps function. For simplifying data fetching, the post introduces SWR (Stale-While-Revalidate) in the context of fetching navigation data. SWR will initially return the “stale” data and also send a fetch request to retrieve the data normally. Then, once that request completes, you get the new data. This ensures that the navigation is always up-to-date and responsive to changes.

The following content models will need to be created:

  • Navigation
    • (Subpages) Navigation Item
  • Sitemap
    • (Subpages) Page
  • Navigation Item
    • (Linked item) Page
  • Page
    • URL Slug
Content models in Kontent.ai

First, we will retrieve our sitemap with all of its linked items. Next, we need to find all the possible paths of our sitemap. Finally, we can find all the unique subpaths and create a map with keys as the URL slug and values as the page. The MAXIMUM_DEPTH constant is used to limit how far traversal will go. Both the Delivery API request and traversal will be limited to this value. Doing so ensures that we do not leave ourselves open the opportunity of infinite recursion. In particular, this pops up when a Page content item is reused in the navigation, causing a nesting loop.

				
					/**
 * Recursively traverses all subpages of a page and returns all paths to the leaves of the tree
 * @param page Page to traverse
 * @param linkedItems Linked items list to reference for Pages
 */
function* traversePaths(
  page: Page,
  linkedItems: IContentItemsContainer,
  depth = 0,
): Generator<Array<Page>, void, unknown> {
  if (!page.elements.Pages.value.length) {
    yield [page];
  }


  if (depth >= MAXIMUM_DEPTH) {
    return;
  }


  const children = page.elements.Pages.value
    .map((codename) => linkedItems[codename])
    .filter((item): item is Page => item.system.type === contentTypes.page.codename);


  for (const child of children) {
    for (const path of traversePaths(child as Page, linkedItems, depth + 1)) {
      yield [page, ...path];
    }
  }
}


export const getAllPagePaths = async (isPreview: boolean) => {
  const {
    data: { items, linkedItems },
  } = await kontentDeliveryApi
    .items<Sitemap>()
    .queryConfig({
      usePreviewMode: isPreview,
    })
    .type(contentTypes.sitemap.codename)
    .elementsParameter([
      contentTypes.sitemap.elements.pages.codename,
      contentTypes.page.elements.pages.codename,
      contentTypes.page.elements.url.codename,
      contentTypes.page.elements.content.codename,
    ])
    .depthParameter(MAXIMUM_DEPTH)
    .limitParameter(1)
    .toPromise();


  const sitemap = items?.[0];


  if (!sitemap) {
    return new Map<string, Page>();
  }


  // Create an array of all paths created from Pages
  const paths = sitemap.elements.Pages.value
    // Get the linked item associated with the codename
    .map((codename) => linkedItems?.[codename])
    // Filter out any items that aren't pages (they should all be pages, but just in case)
    .filter((page): page is Page => page.system.type === contentTypes.page.codename)
    // Traverse the page tree and get all leaves from the tree
    .flatMap((page) => Array.from(traversePaths(page, linkedItems)));


  // Get all unique subpaths and don't add paths that don't have content
  const uniqueSubpaths = new Map<string, Page>();


  Array.from(paths)
    // Get each possible subpath. e.g. [1, 2, 3] => [[1], [1, 2], [1, 2, 3]]
    .flatMap((path) => path.map((_, index) => path.slice(0, index + 1)))
    // Filter out any subpaths where the final page doesn't have Content
    .filter((subpath) => subpath.slice(-1)?.[0].elements.Content?.value?.[0])
    // Add each subpath to the map, keeping unique subpaths
    .forEach((subpath) => {
      const finalPage = subpath.slice(-1)[0];
      const slug = subpath.map((page) => page.elements.Url.value).join('/');
      uniqueSubpaths.set(slug, finalPage);
    });


  return uniqueSubpaths;

				
			

To define routes, we can retrieve all the paths and return them from getStaticPaths. Then, in getStaticProps we get the specific page data we need for static generation.

				
					export const getStaticPaths = (async () => {
  if (process.env.NODE_ENV === 'development') {
    return {
      paths: [],
      fallback: 'blocking',
    } satisfies GetStaticPathsResult;
  }


  const pagePaths = await getAllPagePaths(true);


  const paths = Object.keys(pagePaths).map((path) => ({
    params: { urlSlug: path.split('/') },
  }));


  return {
    paths,
    fallback: 'blocking',
  } satisfies GetStaticPathsResult;
}) satisfies GetStaticPaths;


export const getStaticProps = (async (context) => {
  const isPreview = context.draftMode === true;


  const urlSlugParts = context.params?.urlSlug
    ? Array.isArray(context.params.urlSlug)
      ? context.params.urlSlug
      : [context.params.urlSlug]
    : [];


  const urlSlug = urlSlugParts.join('/');
  const pagePaths = await getAllPagePaths(isPreview);
  const pageId = pagePaths.get(urlSlug)?.system.id;


  if (!pageId) {
    return {
      notFound: true,
    };
  }


  const page = await getPage(pageId, isPreview);
  
  // ...


  return {
    props: {
      // ...
    },
    revalidate: 10,
  };
}) satisfies GetStaticProps<PageProps>;



				
			

For links, we can simply find a matching ID to a linked page and return the path.

				
					const getUrlByPageId = (id: string) => {
  const pagePath = Object.entries(pagePaths).find(([, page]) => page.system.id === id);


  return pagePath?.[0];
};

				
			

The navigation is as simple as querying the Delivery API for your data and rendering it as needed.

				
					export const getHeaderNavigation = async (isPreview: boolean) => {
  const { data: navigation } = await kontentDeliveryApi
    .items<NavigationType>()
    .queryConfig({
      usePreviewMode: isPreview,
    })
    .type(contentTypes.navigation.codename)
    .elementsParameter([
      ...mapElementCodenames('navigation', 'nav_item', 'image'),
      contentTypes.page.elements.url.codename,
    ])
    .depthParameter(4)
    .limitParameter(1)
    .toAllPromise();


  return navigation?.items?.[0] ?? null;
};


export const Navigation = () => {
  const router = useRouter();
  const { data: navigation } = useSWR(
    [contentTypes.navigation.codename, router.isPreview],
    ([, isPreview]) => getNavigation(isPreview),
  );


  return (
    // ...
  );
}

				
			

Final Thoughts

Web Spotlight by Kontent.ai simplifies the challenge of separating content and website design. It provides flexibility in creating visually appealing menus and site structures. The code examples, showcased with Next.js, illustrate the ease of mapping out site hierarchy for efficient routing. Web Spotlight is a valuable tool for modern web development, allowing developers to break free from traditional CMS constraints.

About the Author

Nick Kooman

Nick began as an intern at BizStream, not even a year after graduating from high school, and is now a full-time developer! Nick thinks that growth stagnates when you are comfortable, and the best moments in life happen when you are uncomfortable. He believes he would not have had the chance to make a connection with BizStream if he had not stepped out of his comfort zone during school. When he’s not working, Nick spends time with friends, plays video games, and watches YouTube.

Subscribe to Our Blog

Stay up to date on what BizStream is doing and keep in the loop on the latest in marketing & technology.