What is a headless CMS?
A headless CMS is a back-end only content management system (CMS) built from the ground up as a content repository that makes content accessible via a RESTful API or GraphQL API for display on any device.
The term “headless” comes from the concept of chopping the “head” (the front end, i.e. the website) off the “body” (the back end, i.e. the content repository). A headless CMS remains with an interface to manage content and a RESTful or GraphQL API to deliver content wherever you need it. Due to this approach, a headless CMS does not care about how and where your content gets displayed. It only has one focus: storing and delivering structured content and allowing content editors to collaborate on new content.
With Optimizely you can implement your headless solution using the Content Delivery API.
To filter the API response and remove the fields that you don’t need and/or remove the null field to get the nicer and clearer output you need to use IContentApiModelFilter which is explained in the Optimizely documentation.
But, that didn’t work for me how I wanted.
Here is an example of the default API response in JSON format:
{ "contentLink": { "id": 17, "workId": 0, "guidValue": "85d21fcb-6b86-47e2-8ac7-40561746f6b8", "providerName": null, "url": null, "expanded": null }, "name": "Rich Text", "language": { "link": null, "displayName": "English", "name": "en" }, "existingLanguages": [ { "link": null, "displayName": "English", "name": "en" } ], "masterLanguage": { "link": null, "displayName": "English", "name": "en" }, "contentType": [ "Block", "RichTextBlock" ], "parentLink": { "id": 16, "workId": 0, "guidValue": "ff300114-39a2-469e-9304-3703317a4894", "providerName": null, "url": "http://perficient.local/contentassets/ff30011439a2469e93043703317a4894/", "expanded": null }, "routeSegment": null, "url": null, "changed": "2022-08-05T14:59:05Z", "created": "2022-08-05T14:59:05Z", "startPublish": "2022-08-05T14:59:05Z", "stopPublish": null, "saved": "2022-08-11T12:33:39Z", "status": "Published", "category": { "value": [], "propertyDataType": "PropertyCategory" }, "globalStyle": { "value": "", "propertyDataType": "PropertyLongString" }, "mainBody": { "value": "<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium optio, eaque rerum! Provident similique accusantium nemo autem. Veritatis obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas aliquid. Reprehenderit, quia.</p>\n<p><img src=\"http://perficient.local/globalassets/crop.jpg\" alt=\"crop\" width=\"1280\" height=\"853\" /></p>\n<p> </p>\n<p><img src=\"http://perficient.local/contentassets/b20dbeb471b94ba18ba2f76cfdab5a05/caesars.png\" alt=\"caesars.png\" /></p>", "propertyDataType": "PropertyXhtmlString" } }
After adding the Custom Content API Model Filter how is explained in the documentation, you will get something like this:
{ "contentLink": { "id": 17, "workId": 0, "guidValue": "85d21fcb-6b86-47e2-8ac7-40561746f6b8", "providerName": null, "url": null, "expanded": null }, "name": "Rich Text", "language": { "link": null, "displayName": "English", "name": "en" }, "existingLanguages": [ { "link": null, "displayName": "English", "name": "en" } ], "masterLanguage": { "link": null, "displayName": "English", "name": "en" }, "contentType": [ "Block", "RichTextBlock" ], "parentLink": null, "routeSegment": null, "url": null, "changed": "2022-08-05T14:59:05Z", "created": null, "startPublish": null, "stopPublish": null, "saved": null, "status": null, "contentTypeGuid": "3c2ed8a86f1a41459d4498cfeb8a5652", "globalStyle": { "value": "", "propertyDataType": "PropertyLongString" }, "mainBody": { "value": "<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium optio, eaque rerum! Provident similique accusantium nemo autem. Veritatis obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas aliquid. Reprehenderit, quia.</p>\n<p><img src=\"http://perficient.local/globalassets/crop.jpg\" alt=\"crop\" width=\"1280\" height=\"853\" /></p>\n<p> </p>\n<p><img src=\"http://perficient.local/contentassets/b20dbeb471b94ba18ba2f76cfdab5a05/caesars.png\" alt=\"caesars.png\" /></p>", "propertyDataType": "PropertyXhtmlString" } }
For this example, I put the null values for these fields:
contentApiModel.StartPublish = null; contentApiModel.StopPublish = null; contentApiModel.ParentLink = null; contentApiModel.RouteSegment = null; contentApiModel.Created = null; contentApiModel.Saved = null; contentApiModel.Status = null;
You can see in the previous JSON response that those fields are still there but with the null value.
To actually remove those nulled fields, you need to add one settings option in Startup.cs services under the Content Delivery section.
// remove null values from serialized data services.ConfigureContentDeliveryApiSerializer(settings => { settings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; });
The only thing here is that the ConfigureContentDeliveryApiSerializer is in the Internal namespace but that won’t go anywhere any time soon.
Now we have this nice and clean JSON response:
{ "contentLink": { "id": 17, "workId": 0, "guidValue": "85d21fcb-6b86-47e2-8ac7-40561746f6b8" }, "name": "Rich Text", "language": { "displayName": "English", "name": "en" }, "existingLanguages": [ { "displayName": "English", "name": "en" } ], "masterLanguage": { "displayName": "English", "name": "en" }, "contentType": [ "Block", "RichTextBlock" ], "changed": "2022-08-05T14:59:05Z", "globalStyle": "", "mainBody": "<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium optio, eaque rerum! Provident similique accusantium nemo autem. Veritatis obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas aliquid. Reprehenderit, quia.</p>\n<p><img src=\"http://perficient.local/globalassets/crop.jpg\" alt=\"crop\" width=\"1280\" height=\"853\"></p>\n<p> </p>\n<p><img src=\"http://perficient.local/contentassets/b20dbeb471b94ba18ba2f76cfdab5a05/caesars.png\" alt=\"caesars.png\"></p>" }
For example, for some reason I want to have a content type GUID as a field in the JSON response.
Just add this code at the end of your Custom Content API Model Filter.
contentApiModel.Properties[CONTENT_TYPE_GUID] = Guid.Empty.ToString("N"); if (contentApiModel.ContentLink.Id != null && contentApiModel.ContentLink.Id.HasValue) { var content = contentLoader.Get<IContent>(new ContentReference(contentApiModel.ContentLink.Id.Value)); var contentType = content.ContentTypeID; var type = contentTypeRepository.Load(contentType); if (type != null && type.GUID != Guid.Empty) { contentApiModel.Properties[CONTENT_TYPE_GUID] = type.GUID.ToString("N"); } }
Now the response will be like this:
{ "contentLink": { "id": 17, "workId": 0, "guidValue": "85d21fcb-6b86-47e2-8ac7-40561746f6b8" }, "name": "Rich Text", "language": { "displayName": "English", "name": "en" }, "existingLanguages": [ { "displayName": "English", "name": "en" } ], "masterLanguage": { "displayName": "English", "name": "en" }, "contentType": [ "Block", "RichTextBlock" ], "changed": "2022-08-05T14:59:05Z", "contentTypeGuid": "3c2ed8a86f1a41459d4498cfeb8a5652", "globalStyle": "", "mainBody": "<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum numquam blanditiis harum quisquam eius sed odit fugiat iusto fuga praesentium optio, eaque rerum! Provident similique accusantium nemo autem. Veritatis obcaecati tenetur iure eius earum ut molestias architecto voluptate aliquam nihil, eveniet aliquid culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error, harum nesciunt ipsum debitis quas aliquid. Reprehenderit, quia.</p>\n<p><img src=\"http://perficient.local/globalassets/crop.jpg\" alt=\"crop\" width=\"1280\" height=\"853\"></p>\n<p> </p>\n<p><img src=\"http://perficient.local/contentassets/b20dbeb471b94ba18ba2f76cfdab5a05/caesars.png\" alt=\"caesars.png\"></p>" }
Here is our added field.
And that’s it!
Here is the code listing for this example.
CustomContentApiModelFilter.cs
using EPiServer; using EPiServer.ContentApi.Core.Serialization; using EPiServer.ContentApi.Core.Serialization.Internal; using EPiServer.ContentApi.Core.Serialization.Models; using EPiServer.Core; using EPiServer.DataAbstraction; using EPiServer.ServiceLocation; using System; namespace Perficient.Web.Middleware.ApiModelFilter { [ServiceConfiguration(typeof(IContentApiModelFilter), Lifecycle = ServiceInstanceScope.Singleton)] public class CustomContentApiModelFilter : ContentApiModelFilter<ContentApiModel> { private readonly IContentLoader contentLoader; private readonly IContentTypeRepository contentTypeRepository; private const string CONTENT_TYPE_GUID = "ContentTypeGuid"; public CustomContentApiModelFilter(IContentLoader contentLoader, IContentTypeRepository contentTypeRepository) { this.contentLoader = contentLoader; this.contentTypeRepository = contentTypeRepository; } public override void Filter(ContentApiModel contentApiModel, ConverterContext converterContext) { // To remove values from the output, set them to null. // thus the response output will not contain these "out of the box" fields contentApiModel.StartPublish = null; contentApiModel.StopPublish = null; contentApiModel.ParentLink = null; contentApiModel.RouteSegment = null; contentApiModel.Created = null; contentApiModel.Saved = null; contentApiModel.Status = null; // remove category as we don't need it at the API level contentApiModel.Properties.Remove("Category"); #region Add Content Type GUID to output // add a field called contentTypeGuid which has the ID of the content type in the output, this will be // useful for keying off of to understand what type of content is being delivered contentApiModel.Properties[CONTENT_TYPE_GUID] = Guid.Empty.ToString("N"); if (contentApiModel.ContentLink.Id != null && contentApiModel.ContentLink.Id.HasValue) { var content = contentLoader.Get<IContent>(new ContentReference(contentApiModel.ContentLink.Id.Value)); var contentType = content.ContentTypeID; var type = contentTypeRepository.Load(contentType); if (type != null && type.GUID != Guid.Empty) { contentApiModel.Properties[CONTENT_TYPE_GUID] = type.GUID.ToString("N"); } } #endregion } } }
Awesome Nenad
Why would you wanna remove the necessary fields? 😊
We had that requirement in the project. But again, why keep all the fields that you won’t use or consume on the front end? 🙂
Thanks Nenad, great post.
One note: You should have status fiels populated if you start to use Content Graph. The synchronization of content is using Content Delivery Serializer, and status field is mandatory when using singel key for tenant authentication.
Thank you for pointing this out. This is very good advice! We didn’t use Content Graph at that time but I am sure that we will in all future projects 🙂