Be sure to check out the whole .Net Core 3.1 with Kentico Kontent Series:
- .Net Core 3.1 with Kentico Kontent Part 1: Clean Architecture Design
- .Net Core 3.1 with Kentico Kontent Part 2: Clean Architecture Implementation
- .Net Core 3.1 with Kentico Kontent Part 3: How to Route and Link to Content
- .Net Core 3.1 with Kentico Kontent Part 4: Helpful Tips When Using Linked Items in Kentico Kontent
.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.
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.
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.
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
- Using a repository layer to hold the Kentico Kontent GetItemsAsync() query instead of having it in the Controller.
- Having the repository methods return separate domain models in a core layer that contain no Kentico references (like Asset).
- 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.
<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.
Be sure to check out the whole .Net Core 3.1 with Kentico Kontent Series:
- .Net Core 3.1 with Kentico Kontent Part 1: Clean Architecture Design
- .Net Core 3.1 with Kentico Kontent Part 2: Clean Architecture Implementation
- .Net Core 3.1 with Kentico Kontent Part 3: How to Route and Link to Content
- .Net Core 3.1 with Kentico Kontent Part 4: Helpful Tips When Using Linked Items in Kentico Kontent