Retrieve reusable data with Headless channels
We currently have a project and are trying to add a headless channel. In the channel we have headless items and can add items in a list to provide to the application. Lets call it articles. Can we directly access Reusable content items from a headless channel or do we always have to manually include them in some sort of list.
If we add a new article I would like them to be directly available on the headless channel without having to manually configure this. Would this be possible? Or is it only possible to collect the data from headless items?
Environment
- Xperience by Kentico version: [28.4.0]
- .NET version: [8]
- Deployment environment: [SaaS|Azure]
- We followed the following documentation Retrieve headless content
Answers
@Danny-Paul
The answer to this question is easiest to explain by looking at the other channel types.
If you want to expose a reusable content item over a website or email channel, can you just "expose it"?
Nope! You need to create a web page or email to deliver that reusable content item through in that channel. This is intentional.
Headless channels work the same way. You need to create a headless item that exposes the reusable content.
Headless channels are not meant to be a tunnel into the database for developers. Instead, they are meant to give marketers complete control over what content is exposed through the channel.
All that said... yes, today headless channels are not flexible enough to expose a "set" of reusable content items and instead they much each be explicitly selected.
However, we have a planned way to resolve this issue. Smart folders, which we will be delivering soon, will allow marketers to create dynamic sets of content and then select the smart folder as the source of related content for a headless item's related content field.
In the meantime, I recommend creating a global event handler that updates a headless item's related content items every time a new article is published.
You might be able to find inspiration in the Community Portal source code 👍.
Although you do need Content Items that use your reusable content, it IS possible (with a bit of a 'hack') to get just your Reusable content data without needing to know the inheriting class.
Reusable Schemas are part of the ContentItemCommonData which is retrieved when you do the normal ContentItemQueryBuilder().ForContentTypes. However, in code they are an Interface with no object. The IWebPageQueryResultMapper and IContentItemQueryResultMapper can map your page to a given Content Type, but the trouble of course is if you are grabbing MANY Content types, you won't know which to map to.
The solution? Make a Fake Content Type class that inherits your interface, register it and map to that type! The mapper doesn't know it's mapping the 'wrong' content type, and since you're only mapping the Reusable Schema, it won't care because that data is returned regardless if it's one class or another, it's in the Common Data.
Here's some code samples:
The Reusable Schema
public interface IBaseMetadata
{
/// <summary>
/// Code name of the reusable field schema.
/// </summary>
public const string REUSABLE_FIELD_SCHEMA_NAME = "Base.Metadata";
/// <summary>
/// MetaData_PageName.
/// </summary>
public string MetaData_PageName { get; set; }
/// <summary>
/// MetaData_Title.
/// </summary>
public string MetaData_Title { get; set; }
/// <summary>
/// MetaData_Description.
/// </summary>
public string MetaData_Description { get; set; }
/// <summary>
/// MetaData_Keywords.
/// </summary>
public string MetaData_Keywords { get; set; }
/// <summary>
/// MetaData_NoIndex.
/// </summary>
public bool MetaData_NoIndex { get; set; }
/// <summary>
/// MetaData_OGImage.
/// </summary>
public IEnumerable<IGenericHasImage> MetaData_OGImage { get; set; }
}
My "Fake" Class for mapping:
/// <summary>
/// This page type doesn't actually exist, and is only for mapping
/// </summary>
[RegisterContentTypeMapping(CONTENT_TYPE_NAME)]
public partial class BaseMetadataForMapping : IWebPageFieldsSource, IBaseMetadata
{
/// <summary>
/// Code name of the content type.
/// </summary>
public const string CONTENT_TYPE_NAME = "Base.Metadata.ForMapping";
/// <summary>
/// Represents system properties for a web page item.
/// </summary>
[SystemField]
public WebPageFields SystemFields { get; set; }
/// <summary>
/// MetaData_PageName.
/// </summary>
public string MetaData_PageName { get; set; }
/// <summary>
/// MetaData_Title.
/// </summary>
public string MetaData_Title { get; set; }
/// <summary>
/// MetaData_Description.
/// </summary>
public string MetaData_Description { get; set; }
/// <summary>
/// MetaData_Keywords.
/// </summary>
public string MetaData_Keywords { get; set; }
/// <summary>
/// MetaData_NoIndex.
/// </summary>
public bool MetaData_NoIndex { get; set; }
/// <summary>
/// MetaData_OGImage.
/// </summary>
public IEnumerable<IGenericHasImage> MetaData_OGImage { get; set; }
}
And here's where I take the WebPageContentQueryDataContainer / ContentQueryDataContainer (that's actually of type Generic.Home) and map it:
// Even though the result has a content type of Generic.Home, this works!
var mappedResult = _webPageQueryResultMapper.Map<BaseMetadataForMapping>(result);
if(mappedResult is IBaseMetadata metadata) {
// You now have your metadata mapped!
}
To answer this question, you have to login first.