Dynamic URL routing with Kontent.ai
Published: 2024-02-09 12:24:07

Headless CMS has been gaining popularity recently, and many common CMS platforms are developing their own headless versions. The Kentico platform is no exception. Its headless version is called Kontent AI. Interestingly, it has been around for quite some time, yet it's only recently that larger customers have started to take notice. 

In traditional CMS platforms because they are website-focused, content is organized in a tree structure, and URLs are often automatically assigned by a CMS based on the content's position in a tree.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ybr1mrblz12sct2lltu.jpg)

Since Kontent AI is a Headless CMS and designed to be multi-channel rather than website-centric, the concept of associating a URL with a content item is not implemented out of the box.

Similar to many other Headless CMS platforms, developers should implement custom code creating and storing URLs for content items, and vice-versa locating specific content based on a given URL. 
There are two approaches for modeling content relationships to implement something like a traditional CMS “tree”:

1. **Top-to-bottom**, where the piece of content stores references to its child pages, however, child pages are not aware of who their parent is

2. And **bottom-to-top**, where the content refers to its parent, but the parent may not know it’s children =)
 
Both approaches have pros and cons, however the implementation in code will be very similar. We’ll focus on the top-to-bottom approach, as it is more user-friendly for content editors working in the Kontent.ai admin interface. This approach allows viewing all child pages of a selected content and setting restrictions on the types of child pages.

Content is stored in a flat repository, and connections between content items are configured in the properties of the content itself.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sz3u1wkszasr4barvapa.jpg)

In addition to managing content connections for the tree, it's also necessary to link content with its generated URL. For large applications, storing this mapping in an index that's built and updated as new content is added makes sense. For smaller projects, storing the URL directly in the content is an option, which is also convenient for content editors.

For automatic URL generation, Kontent AI offers the ability to create a Custom Element as a content type property and fill it in via an external service. You should also add a URL Slug property to the content, which automatically generates an SEO-friendly URL segment based on one of the content properties, typically, content's name.

Essentially, you need to create:

- **A "Children" property** (Linked items type)to store all child pages;
- **A URL Slug** (URL Slug type)for generating the last part of the content's URL;
- **A "Tree URL"** (Custom Element type)for the final URL generation.

It's crucial to keep property names consistent across all content types since external code will rely on these property names. While using a Content-Type Snippet can unify common properties and expedite new type creation, it also limits the ability to customize these properties for different content types (to specify a different base property for URL Slug or set restrictions for Children). For example, to implement restrictions, such as allowing a child page titled "News Catalog" to only use the content of the "News" type. 

A "Children" property
![A "Children" property](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e4gb4okvl96ll2oljkv8.jpg)

A URL Slug
![A URL Slug](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l5e5lblxtzeaqop9g0ve.jpg)

A "Tree URL"
![A "Tree URL"n](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e6hc0f693nj5v3j3ueuo.jpg)

To generate the final URL, you need to create a code that accesses the Custom Element. The final URL will be constructed from the parent page's URL, combined with the current content's URL Slug. Since the URL Slug property isn't a system property, it must be passed as a parameter, i.e. to allow its reading (specify in **Allow the custom element to read values of specific elements**), the remaining necessary parameters are passed by default.

To locate the parent page, you need to request it from Kontent AI through the provided package **Kontent.Ai.Delivery** for .Net or **kontent-ai/delivery-sdk** for JS. These packages will allow you to create a Delivery Client with the specific ID and environment key.

Finding the parent page involves identifying content that includes the current page in its 'Children'. In a top-down tree, multiple pages might fit this criterion- in this case, you can select the first element from the list after sorting by name or ID. In a bottom-up tree, the parent page is identified by passing its ID or codename as a parameter. There may be no parent page if the current content is located at the root of the tree. In this case, the generated URL will be identical to the URL Slug of this page. 

```
const KontentDelivery = require('@kontent-ai/delivery-sdk');

function (context, req) {
    
	let urlSlug = req.body.urlSlug;
	const language = req.body.language;
	const codename = req.body.codename;

	//Get a client for searching content in Kontent AI
	const deliveryClient = KontentDelivery.createDeliveryClient({
    	environmentId: req.body.projectId,
    	previewApiKey: <ApiKey>,
    	defaultQueryConfig: {
        	usePreviewMode: true
    	}
	});    

	//Request content whose source page is a child page. In the case of a bottom-up tree, request the parent content by its ID
	const response = await deliveryClient.items().languageParameter(language).anyFilter('elements.children', [codename]).toPromise();

	let parentUrl = '/' + language;

    //Exclude the case where the page is a child of itself
	if (response.data.items.length) {
    	response.data.items = response.data.items.filter(x => x.system.codename !== codename);
	}

	//Get the full parent URL and substitute the current language in case of a language mismatch
	if (response.data.items.length) {
    	const item = response.data.items[0];

    	if (item.elements.treeurl) {
        	parentUrl = item.elements.treeurl.value;
    	}

    	if (item.system.language !== language) {
        	const tokens = parentUrl.split('/');
        	tokens[1] = language;
        	parentUrl = tokens.join('/');
    	}
	}

	//If URL Slug is not provided in the parameters, request it through the client; if unsuccessful, set URL Slug as codename
	if (!urlSlug || urlSlug === '') {
    	const urlSlugResponse = await deliveryClient.item(codename).languageParameter(language).toPromise();
    	urlSlug = urlSlugResponse.data.item.elements.url.value;
	}

	if (!urlSlug || urlSlug === '') {
    	urlSlug = codename;
	}

	// Generate the final URL by concatenating it with the parent URL and verify its uniqueness
	let pageUrl = parentUrl + '/' + urlSlug;
	const uniqueResponse = await deliveryClient.items()
    	.languageParameter(language)
    	.equalsFilter('elements.treeurl', pageUrl)
    	.notEqualsFilter('system.id', req.body.contentItemId)
    	.toPromise();

	//If the generated URL is not unique, append the content ID to the end of the URL
	if (uniqueResponse.data.items.length) {
    	pageUrl = pageUrl + '-' + req.body.contentItemId;
	}

	context.res = {
    	headers: { "Content-Type": "application/json" },
    	status: 200,
    	body: {
        	success: true,
        	pageUrl: pageUrl
    	}
	};
}
```
To display a Custom Element, you need an HTML markup that will set the value for this element upon initialization, using the generated code. To initialize a Custom Element and access its parameters, you need to import the [JS script](https://app.kontent.ai/js-api/custom-element/v1/custom-element.min.js) from Kontent AI and call the init function, which takes as parameters the element itself and the context containing data about the content and project.

For more details on working with Custom Elements, refer to the [documentation](https://kontent.ai/learn/docs/apis/custom-elements-js-api).

This approach replicates the URL creation process similar to traditional tree structures. The final step is to develop a mechanism to retrieve content based on a specific URL. Since the content already stores its URL, you can use the Delivery Client (JS or .net) to match the requested URL with the TreeUrl of the page.

In summary, while Headless CMS platforms are gaining popularity and have many advantages, they sometimes require additional development effort for features that were previously standard. However, they encourage a shift away from tree structures towards shorter, more flexible URLs. This is particularly useful for applications with uniform content types, such as news feeds, or when content is categorized by various criteria without a single "correct" parent. 

Autor: Maksim Zakharov