Building a CMS-Agnostic Layout Service Response in Sitecore

Building a CMS-Agnostic Layout Service Response in Sitecore 

 Introduction

In modern headless architectures, frontend teams (especially Next.js) expect clean, predictable, and CMS-agnostic APIs. However, Sitecore Layout Service by default returns:

  • Nested fields.value structures

  • Media, links, and other field types follow Sitecore-specific structures

  • CMS-specific field types

This makes frontend consumption complex and tightly coupled to Sitecore.

In this blog, I’ll walk through how I built a CMS-agnostic Layout Service response, including solving one of the trickiest problems:

👉 Expanding Multilist fields dynamically with full field data


 Problem Statement

Sitecore Layout Service provides a powerful way to expose content for headless applications. However, the default response is highly CMS-dependent and not ideal for modern frontend frameworks like Next.js.

For example:

  • Fields are wrapped in fields.value
  • Field names are not normalized
  • Media, links, and other field types follow Sitecore-specific structures
  • Not frontend-friendly
  • Tight coupling with Sitecore








Goal

Transform this into:




  • CMS-agnostic
  • Ready for Next.js
  • Clean


 Solution Approach

We implemented a custom Rendering Contents Resolver to:

  1. Intercept Layout Service response
  2. Transform field structure
  3. Expand multilist items
  4. Normalize field types


Step 1: Custom Rendering Contents Resolver

   public class CleanJsonResolver : RenderingContentsResolver
   {

       public override object ResolveContents(Rendering rendering, IRenderingConfiguration config)
       {
           var item = GetContextItem(rendering, config);

           var data = MapFields(item);

           return new
           {
               component = rendering.RenderingItem.Name,
               data = data
           };
       }
}

 Step 2: Map Items

 private Dictionary<string, object> MapFields(Item item)
 {
     var dict = new Dictionary<string, object>();

     foreach (Field field in item.Fields)
     {
         if (field == null) continue;
         if (field.Name.StartsWith("__")) continue;

         var fieldName = item.Template.GetField(field.ID)?.Name;

         if (string.IsNullOrWhiteSpace(fieldName)) continue;

         var key = ToCamelCase(fieldName);

         var value = GetFieldValue(field);

         if (value == null) continue;

         dict[key] = value;
     }

     return dict;
 }

 Step 3: Handle Field Types

 private object GetFieldValue(Field field)
 {
     if (field == null) return null;

     var type = field.TypeKey?.ToLower() ?? "";

     //MULTILIST / TREELIST/DropList
     if (type.Contains("list") || type.Contains("tree"))
     {
         var multilist = new MultilistField(field);
         var items = multilist.GetItems();

         if (items == null || !items.Any())
             return null;

         return items.Select(x => MapFields(x)).ToList();
     }

     // IMAGE
     if (type == "image")
     {
         var image = new ImageField(field);

         if (image?.MediaItem == null)
             return null;

         return new
         {
             url = Sitecore.Resources.Media.MediaManager.GetMediaUrl(image.MediaItem),
             alt = image.Alt ?? ""
         };
     }

     //CHECKBOX
     if (type == "checkbox")
     {
         return new CheckboxField(field).Checked;
     }

     //  LINK
     if (type == "general link" || type == "link")
     {
         var link = new LinkField(field);

         return new
         {
             url = link.GetFriendlyUrl(),
             text = link.Text,
             target = link.Target
         };
     }

     // DEFAULT (text, rich text, number etc.)
     return string.IsNullOrWhiteSpace(field.Value) ? null : field.Value;
 }

Step 4: Normalize Field Names

private string ToCamelCase(string input)
{
    if (string.IsNullOrWhiteSpace(input)) return null;

    var cleaned = input.Replace(" ", "").Replace("-", "");

    return char.ToLowerInvariant(cleaned[0]) + cleaned.Substring(1);
}

Step 5: Create Patch Configuration

Now register your resolver in Sitecore using a patch config file.

📁 Path:  /App_Config/Include/Project/YourProject/LayoutService.CleanJson.config

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore> <layoutService> <renderingContentsResolvers> <resolver id="CleanJsonResolver" type="YourNamespace.CleanJsonResolver, YourAssembly" /> </renderingContentsResolvers> </layoutService> </sitecore> </configuration>


Step 6: Register the Resolver in Sitecore (Item Level)

After adding the patch config, you also need to create/register the resolver

Navigate in Sitecore Content Editor

Go to:

/sitecore/system/Modules/Layout Service/Rendering Contents Resolvers

Create New Resolver Item

  • Right-click → Insert
  • Select Rendering Contents Resolver
  • Name it:CleanJsonResolver(Or give your custom resolver name)
  • Configure the Resolver Item : Type: YourNamespace.CleanJsonResolver, YourAssembly



Step 7: Assign Resolver to Rendering

Go to Sitecore:

/sitecore/layout/Renderings/...

  • Open your rendering (e.g., testjson)
  • Find “Rendering Contents Resolver”
  • Select: CleanJsonResolver


Challenges Faced (Important Learnings)

 1. Multilist fields return GUIDs by default

Solution: Use MultilistField.GetItems()

2. System fields noise

Filtered using:

field.Name.StartsWith("__")

Key Takeaways

  • Layout Service is not frontend-ready by default

  • Multilist requires manual expansion

  • Custom resolvers unlock true headless power


🚀 Final Result

You now have:

✔ CMS-agnostic JSON
✔ Fully expanded multilist
✔ Clean structure for frontend
✔ No GUIDs
✔ No Sitecore coupling


💬 Conclusion

This approach bridges the gap between:

👉 Traditional Sitecore → Modern Headless Architecture

By customizing the Layout Service response, you empower frontend teams to work independently with clean APIs — exactly what headless is meant for.


If you're working on Sitecore + Next.js and struggling with Layout Service responses — this pattern will save you a lot of time.

Happy coding! 🚀

Comments

Popular posts from this blog

XM Cloud Basics – Part 1

Sitecore 10.4 + Docker + Next.js: A Complete Setup Guide for JSS Developers

Deploying Next.js + Sitecore JSS: Real-World Azure PaaS Setup