.Net Core 3.1 with Kentico Kontent Part 3: How to Route and Link to Content
Be sure to check out the whole .Net Core 3.1 with Kentico Kontent Series:
  1. .Net Core 3.1 with Kentico Kontent Part 1: Clean Architecture Design 
  2. .Net Core 3.1 with Kentico Kontent Part 2: Clean Architecture Implementation
  3. .Net Core 3.1 with Kentico Kontent Part 3: How to Route and Link to Content
  4. .Net Core 3.1 with Kentico Kontent Part 4:  Helpful Tips When Using Linked Items in Kentico Kontent  (Coming soon)

.NET CORE 3.1 WITH KENTICO KONTENT SERIES PART 3

Headless CMS systems like Kentico Kontent are what everyone’s talking about. 

In a traditional CMS, you create a new page and the system controls creating the route to this page. If you need to link to the page from another page, you can load the destination page from a database and a URL property is available to you. They even provide features like aliases, so users can customize the URL for each page type for things like SEO.

Headless CMS tools break apart the traditional coupling from the front end UI and the back end. They provide a back end database, a REST API to create, get and edit data from the app, and an admin UI for content editors to cultivate the content. The beauty of headless is that it’s just a database that you can consume from anywhere, a mobile app, a website, reporting or BI systems, a data indexer for a search index, or to train a model for an AI system. 😍

Say you create a new product, an orange chair, and you want to add a link to your new chair from your home page. Since the database has no knowledge of the web application, how does the system know the link URL needs to be ~/products/detail/orange-chair, in order to get to the chair’s product detail page? There is no longer a URL property returning from the API call!

You could just create the URL by hand: “~/products/detail/” + LinkedProductName. But what happens when you want to allow your content editors to add links to products, galleries, blog posts, or news articles? You could just add a giant if statement that pumps out the right URL depending on the type, but then every time the content editors want to link to a new type, you have to make a code change and publish! This violates the Open Closed Principle of the SOLID Design Principles. 😒

To solve this problem we’ll use
  • .Net Core MVC named routing
  • Kontent’s URL slug content element
  • A .Net core <LinkItemTagHelper> we will create
And hit on
  • .Net Kentico Kontent API querying via content slug
  • .Net reflection for code reusability
  • Dependency injection in .Net Core

Step 1: Adding Link Fields to Kentico Kontent

First, add a URL Slug content element to a content type. This field will auto-generate a URL safe slug based on the data entered in a field of your choosing on the content type. Here we selected Product Name in a dropdown as the field it will generate its slug from.

Adding a URL Slug content element to a content type

You’ll also need to add text fields to hold link data: LinkName, LinkTarget. These three fields will need to be added to each piece of content you want to link to.

On the content that you want to list your links to your product detail pages in Kontent, add a new Linked Item type and select Product. Then add some linked products. For this, we’ll put it on the /products Index page. Also add a link to another type. Here we’ll add a link to the Portfolio Gallery. Now there are two links to two different content types, each with a detail page of their own that we need to figure out how to route to.

Example of adding a link to the Portfolio Gallery

Step 2: Generate C# model classes

To use these in .Net, have the Kentico Kontent model generator generate the classes. They will look something like this. Notice that the list of linked items is of type IEnumerable<object>. Items in this list could be ProductDetail types or Gallery types.
public partial class Products
{
    public const string Codename = "products";
    //property codenames
    public string FriendlyUrl { get; set; }
    public IEnumerable<object> AssociatedProducts { get; set; }
    public ContentItemSystemAttributes System { get; set; }
    ...
}
public partial class ProductDetail
{
    public const string Codename = "product_detail";
    //property codenames
    public string FriendlyUrl { get; set; }
    public string LinkName { get; set; }
    public string LinkTarget { get; set; }
    public ContentItemSystemAttributes System { get; set; }
    ...
}
Now create some supporting classes to return linked data in a consistent format.
public class LinkItem
{
    public string Name { get; set; }
    public string Target { get; set; } = "_self";
    public string TypeCode { get; set; }
    public string Url { get; set; }
}
public class ContentRouteValues
{
    public string Slug { get; set; }
} 

Step 3: Create product controller

We’ll need a Products controller to handle displaying product index and product details pages. Here’s what a product controller’s index page might look like manually building the URLs based on the type of content included in the link. Here, /products will display links for the content we linked in associated products above.
[Route("products")]
public class ProductsController : Controller
{
    protected IDeliveryClient deliveryClient { get; set; }
    public ProductsController(IDeliveryClient deliveryClient)
    {
        this.deliveryClient = deliveryClient;
    }

    [Route("", Name = "products")]
    public async Task<ActionResult> Index()
    {
        // Query for the products page, we should only have one
        var response = await this.deliveryClient.GetItemsAsync<Products>(
            new EqualsFilter("system.type", "products"),
            new LimitParameter(1));

        var productLinks = response.Items.FirstOrDefault();
        var productLinkViewModel = new List<LinkItem>();               

        // Loop through all linked items and add a LinkItem for each
        foreach ( var link in productLinks.AssociatedProducts ) 
        {
            if(link is ProductDetail) {
                var productDetail = (ProductDetail)link;
                productLinkViewModel.Add(new LinkItem {
                    Name = productDetail?.ProductName,
                    TypeCode = productDetail?.System.Type,
                    Url = "/products/detail/" + productDetail?.FriendlyUrl;
                });
            }

            if(link is PortfolioGallery) {
                var gallery = (PortfolioGallery)link;
                productLinkViewModel.Add(new LinkItem {
                    Name = gallery?.ProductName,
                    TypeCode = gallery?.System.Type,
                    Url = "/design-hub/" + gallery?.FriendlyUrl;
                });
            }
        }
        return View("Products", productLinkViewModel);
    }
}

Step 4: Refactor product controller

Above, every time content editors want to link to a different piece of content for a page that you already have set up, you’d have to add a new if statement. 😭 Instead lets refactor this to work for any type of content. Here we’re using attribute routing, specifying the name of the route as the System.CodeName of the product content type we added the URL slug too. We’ll add a detail endpoint, /product/detail/[ProductName], to display the product detail page. We’ll also add an extension method, GetPropertyValue(), that uses reflection in Index() to build the links for any type.
using Microsoft.AspNetCore.Mvc;
using KenticoCloud.Delivery;
using System.Reflection;
[Route("products")]
public class ProductsController : Controller
{
    protected IDeliveryClient deliveryClient { get; set; }
    public ProductsController(IDeliveryClient deliveryClient)
    {
        this.deliveryClient = deliveryClient;
    }

    // Name is the codename of the products type in Kontent
    [Route("", Name = "products")]
    public async Task<ActionResult> Index()
    {

        // Query for the products page, we should only have one
        var response = await this.deliveryClient.GetItemsAsync<Products>(
            new EqualsFilter("system.type", "products"),
            new LimitParameter(1));

        var productLinks = response.Items.FirstOrDefault();
        var productLinkViewModel = new List<LinkItem>();                

        foreach ( var product in productLinks.AssociatedProducts ) 
        {
            // Use reflection to look for properties on the object
            // and get their values
            productLinkViewModel.Add(new LinkItem {
                Name = product?.GetPropertyValue("LinkName"),
                Target = product?.GetPropertyValue("LinkTarget"),
                // These should match the route name so we know how to 
                // generate the routes for each type. ex: product_detail
                TypeCode = product?.GetPropertyValue<ContentItemSystemAttributes>("System").Type;,
                Url = product?.GetPropertyValue("FriendlyUrl");
            });
        }

        return View("Products", productLinkViewModel);
    }

    public static T GetPropertyValue<T>(this object item, string propertyName, T defaultValue = default(T))
    {
        if (item == null){ return defaultValue; }
        var prop = item.GetType().GetProperty(propertyName);
        return (T)(prop.GetValue(item) ?? defaultValue);
    }

    // Name is the codename of the product detail type in Kontent
    [Route("detail/{slug}", Name = "product_detail")]
    public async Task<ActionResult> Detail(string slug)
    {

        // Query ProductDetail types in Kontent where slug=orange-chair
        var response = await this.deliveryClient.GetItemsAsync<ProductDetail>(
            new EqualsFilter("elements.friendly_url", slug),
            new EqualsFilter("system.type", "product_detail"),
            new LimitParameter(1));

        var productDetail = response.Items.FirstOrDefault(); 

        // If we didn't find any products with this slug, return a 404
        if (productDetail == null) { return NotFound(); }

        // Create a ProductDetail ViewModel from the results
        return View("Detail", productDetailViewModel);
    }
}

You can find the codename of a content type in Kontent by clicking the {#} in the upper right of the content type page.

Image showing how to find the  codename of a content type in Kentico Kontent

This sets you up for the Products and Product Details pages. You’ll want to add the endpoints in the Products controller to every controller where you’d want to link items to. As long as the content that you’re linking to has the same fields in Kontent with the same names (LinkName, LinkTarget, FriendlyUrl), this will work. 😁 

Tip: I’d recommend 
  1. Using a repository layer to hold the Kentico Kontent GetItemsAsync() query instead of having it in the Controller.
  2. Having the repository methods return separate domain models in a core layer that contain no Kentico references (like Asset).
  3. A mapper like AutoMapper to map the Kentico Models to the core models.🌟

Step 5: Creating a tag helper

Now we’ll work on the code to display the links. You’ll need to create a new TagHelper responsible for creating the HTML for the anchor tag to link to the item.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Razor.TagHelpers;

public class LinkItemTagHelper : TagHelper
{
    private readonly IUrlHelper urlHelper;
    public string Class { get; set; }
    public LinkItem Link { get; set; } 

    public LinkItemTagHelper(IUrlHelper urlHelper)
    {

        this.urlHelper = urlHelper;
    }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        // Get child content for the tag currently being processed
        var content = await output.GetChildContentAsync();
        var contentMarkup = content.GetContent();

        // Create an <a> as the output of this helper
        output.TagName = "a";
        output.Attributes.Add("class", Class);

        // Only add main attributes if the link is not null
        if (Link != null)
        {
            // Default the slug to url we’re linking to
            var routeValues = new ContentRouteValues { Slug = Link.Url };

 
            // generate the route for the item using MVC built in named
            // routing by the System.TypeCode of the item we’re linking
            // to.
            string route = !string.IsNullOrWhiteSpace(Link.TypeCode)
                ? urlHelper.RouteUrl(Link.TypeCode, routeValues)
                : Link.Url;

            // Add the href, target and title attributes to the <a>
            output.Attributes.Add("href", route);
            output.Attributes.Add("target", Link?.Target);
            output.Attributes.Add("title", $"Link to {Link?.Name}");
        } 

        output.Content.AppendHtml(contentMarkup);
    }
}

To inject IDeliveryClient on the ProductsController and IUrlHelper on the LinkItemTagHelper, we’ll need to add this code in startup.cs inside ConfigureServices(IServiceCollection services). With .Net Core, dependency injection is built in now! πŸ€“
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>()
    .AddScoped<IUrlHelper>(
        serviceProvider => new UrlHelper(
            serviceProvider.GetRequiredService<IActionContextAccessor>()
                .ActionContext
        )
    );

services.AddSingleton<IDeliveryClient>( serviceProvider => 
        DeliveryClientBuilder.WithOptions(_ => serviceProvider.GetRequiredService<IOptions<DeliveryOptions>>().Value).WithTypeProvider(serviceProvider.GetRequiredService<ITypeProvider>()).Build()
    )
);

To make the LinkItemTagHelper available for use in any of your views, add this line in ~/Views/_ViewImports.cshtml.
@addTagHelper *, [InsertWebProjectName]

Step 6: Using the tag helper in the view

The Products.cshtml view displays one LinkItemTagHelper for each link in the list. Because we imported all the tag helpers in our project in the default viewImport above, we can now reference our tag helper with <link-item>. The link=”@link” will set the LinkItem property on the tag helper to the link in the list.
@model IEnumerable<LinkItem>

@foreach (var link in Model)
{
    <link-item link="@link" />
}
You can see that our page now has a list of product links with the data populated. Notice how, depending on the type of item linked (product or gallery), the href’s generated by the tag helper reflect that, routing each to their page types.

List of product links with the data populated
<a href="/products/detail/orange-chair" target="_self" title="Link to Orange Chair">Orange Chair</a>
<a href="/design-hub/portfolio-page" target="_self" title="Link to Portfolio Page">Portfolio Page</a>
<a href="/products/detail/test-product" target="_self" title="Link to Test Product Chair">Test Product</a>
Now when you click on those links, the URLs will direct you to each page that you linked to, no matter what type it is!

More awesomeness
  • Now you should be able to add linked items to any type of content as long as the content has the proper fields defined!
  • If content editors want to level up their SEO game, they can change the default generated slug text of any page to anything you need. /products/detail/orange-chair can become /products/detail/[Seo-specific-slug-for-better-search-results!] without changing any of the code. 
gif of fireworks

Be sure to check out the whole .Net Core 3.1 with Kentico Kontent Series:
  1. .Net Core 3.1 with Kentico Kontent Part 1: Clean Architecture Design 
  2. .Net Core 3.1 with Kentico Kontent Part 2: Clean Architecture Implementation
  3. .Net Core 3.1 with Kentico Kontent Part 3: How to Route and Link to Content
  4. .Net Core 3.1 with Kentico Kontent Part 4:  Helpful Tips When Using Linked Items in Kentico Kontent  (Coming soon)

Share This Post:

Twitter Pinterest Facebook Google+
Click here to read more Kentico posts
Start a Project with Us
Photo of the author, Joel Burke

About the author

Born in Ohio, Joel is an energetic programmer with over 12 years of Agile experience as a Full Stack Developer. He loves his brilliant wife Rebekah, documentation, communication, food, self-improvement, dressing up, doing things for people and learning. Incredibly passionate about faith, family, agile processes, people, software development and solving hard problems in each of these areas. Outside of work you’ll find him gaming with Rebekah, relaxing and enjoying great food.

View other posts by Joel

Subscribe to Updates

Stay up to date on what BizStream is doing and keep in the loop on the latest with Kentico.