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 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.
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.
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:
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, 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()
.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();
}
// 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();
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;
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()
.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 (
// ...
);
}
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.
We love to make cool things with cool people. Have a project you’d like to collaborate on? Let’s chat!
Stay up to date on what BizStream is doing and keep in the loop on the latest in marketing & technology.