Layout Service is used in headless Sitecore development to get the content from Sitecore items as JSON. In Sitecore JSS, the usage of Layout Service is related with page rendering, whenever you change the route either on client or with server side rendering, JSS app calls Layout Service passing new URL in ?item=
query string parameter. Using JSS SDK in your React, Vue, Angular, or Next application you can leverage this data inside your components by passing props.fields.myfield
to components from SDK like <Text field=''/>
or <Image field=''/>
.
But what if you need to do additional client side’s call to Sitecore to fetch more data after the page is rendered? You would probably create your own MVC Controller, which reads the item and returns necessary data as JSON, which of course make sense.
In this blog I will show how to reuse base Layout Service classes, so your custom controller would return the data in similar way as Layout Service would, so your front-end code should work with this data without too many modifications.
Implementation
If we decompile Layout Service dlls we can check that in the top layer there is a LayoutServiceController
MVC controller which process the items and return it as JSON (internally it calls content resolvers for each rendering used on the page).
We need first to create a service which can serialize Sitecore items using JSSItemSerializer
and return it as JObject
or JArray
, similar to content resolver:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
namespace SmartSitecore.Foundation.Content.Services { using System.Collections.Generic; using Newtonsoft.Json.Linq; using Sitecore.Data.Items; using Sitecore.JavaScriptServices.ViewEngine.LayoutService; using Sitecore.LayoutService.Serialization.Pipelines.GetFieldSerializer; /// <summary> /// Item Processing Service. /// </summary> public class ItemProcessingService : IItemProcessingService { private readonly JssItemSerializer itemSerializer; /// <summary> /// Initializes a new instance of the <see cref="ItemProcessingService"/> class. /// </summary> /// <param name="fieldSerializer">Field Serializer.</param> public ItemProcessingService(IGetFieldSerializerPipeline fieldSerializer) { this.itemSerializer = new JssItemSerializer(fieldSerializer) { AlwaysIncludeEmptyFields = true, }; } /// <inheritdoc/> public JObject ProcessItem(Item item) { if (item == null) { return null; } return JObject.Parse(this.itemSerializer.Serialize(item)); } /// <inheritdoc/> public JArray ProcessItems(IEnumerable<Item> items) { var jArray = new JArray(); foreach (var item in items) { JObject value = this.ProcessItem(item); JObject jItem = new JObject { ["id"] = item.ID.Guid.ToString(), ["name"] = item.Name, ["displayName"] = item.DisplayName, ["fields"] = value, }; jArray.Add(jItem); } return jArray; } } } |
and interface for it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
namespace SmartSitecore.Foundation.Content.Services { using System.Collections.Generic; using Newtonsoft.Json.Linq; using Sitecore.Data.Items; /// <summary> /// Interface for Item Processing Service. /// </summary> public interface IItemProcessingService { /// <summary> /// Change item to JObject. /// </summary> /// <param name="item">Item.</param> /// <returns>JObject.</returns> JObject ProcessItem(Item item); /// <summary> /// Change items to JArray. /// </summary> /// <param name="items">Items.</param> /// <returns>JArray.</returns> JArray ProcessItems(IEnumerable<Item> items); } } |
We can register it in DI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
namespace SmartSitecore.Foundation.Content.DI { using Microsoft.Extensions.DependencyInjection; using Sitecore.DependencyInjection; using SmartSitecore.Foundation.Content.Services; /// <summary> /// Register container class. /// </summary> public class RegisterContainer : IServicesConfigurator { /// <summary> /// Configure container. /// </summary> /// <param name="serviceCollection">IServiceCollection object parameter.</param> public void Configure(IServiceCollection serviceCollection) { serviceCollection.AddTransient<IItemProcessingService, ItemProcessingService>(); } } } |
and config patch:
1 2 3 4 5 6 7 |
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <services> <configurator type="SmartSitecore.Foundation.Content.DI.RegisterContainer, SmartSitecore.Foundation.Content" /> </services> </sitecore> </configuration> |
Finally we can create our base controller, which mimics Layout Service’s Content Resolver functionalities (keep in mind that we can also use same attributes, meaning that client’s will have to pass sc_apikey
parameter to call the API):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
namespace SmartSitecore.Foundation.Content.Controllers { using System.Collections.Generic; using System.Web.Mvc; using Newtonsoft.Json.Linq; using Sitecore.Data.Items; using Sitecore.LayoutService.Mvc.Caching.ETag; using Sitecore.LayoutService.Mvc.Security; using SmartSitecore.Foundation.Content.Services; /// <summary> /// Base controller which mimics behaviour of Layout Service and /// returns item's data as json in structure similar to layout service. /// </summary> [RequireSscApiKey(Order = 1)] [ImpersonateApiKeyUser(Order = 2)] [ImpersonateJwtTokenUser(Order = 3)] [EnableApiKeyCors(Order = 4)] [SuppressFormsAuthenticationRedirect(Order = 5)] [ETagFilter(Order = 6)] public abstract class BaseContentController : Controller { private readonly IItemProcessingService itemProcessingService; /// <summary> /// Initializes a new instance of the <see cref="BaseContentController"/> class. /// </summary> /// <param name="itemProcessingService">Item Processing Service.</param> public BaseContentController(IItemProcessingService itemProcessingService) { this.itemProcessingService = itemProcessingService; } /// <summary> /// Change item to JObject. /// </summary> /// <param name="item">Item.</param> /// <returns>JObject.</returns> protected virtual JObject ProcessItem(Item item) { return this.itemProcessingService.ProcessItem(item); } /// <summary> /// Change items to JArray. /// </summary> /// <param name="items">Items.</param> /// <returns>JArray.</returns> protected virtual JArray ProcessItems(IEnumerable<Item> items) { return this.itemProcessingService.ProcessItems(items); } /// <summary> /// Change JObject to ActionResult and pack it into "fields" node. /// </summary> /// <param name="results">object with results.</param> /// <returns>results in "fields" node.</returns> protected virtual ActionResult ResultsAsJson(JObject results) { if (results == null) { return null; } JObject jItem = new JObject(); jItem["fields"] = results; return this.Content(jItem.ToString(), "application/json"); } } } |
Usage
We can use our base controller like this in custom controller (this sample will return item and subitems for ID passed to the action):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
namespace SmartSitecore.Feature.CustomFeature.Controllers { using System.Web.Mvc; using SmartSitecore.Foundation.Content.Controllers; using SmartSitecore.Foundation.Content.Services; /// <summary> /// Customer Controller with custom Action. /// </summary> public class MyCustomController : BaseContentController { /// <summary> /// Initializes a new instance of the <see cref="MyCustomController"/> class. /// </summary> /// <param name="itemProcessingService">Item Processing Service.</param> public MyCustomController(IItemProcessingService itemProcessingService) : base(itemProcessingService) { } /// <summary> /// Do something custom. /// </summary> /// <param name="itemId">Item Id</param> /// <returns>Custom result as JSON</returns> public ActionResult CustomAction(string itemId) { var item = Sitecore.Context.Database.GetItem(new Sitecore.Data.ID(itemId)); var contextItemJson = this.ProcessItem(item); contextItemJson["children"] = this.ProcessItems(item.Children); return this.ResultsAsJson(contextItemJson); } } } |
and register it as API route:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
namespace SmartSitecore.Feature.CustomFeature.Pipelines { using System.Web.Mvc; using System.Web.Routing; using Sitecore.Mvc.Pipelines.Loader; using Sitecore.Pipelines; /// <summary> /// Class registers routes for API controller. /// </summary> public class InitializeMyRoutes : InitializeRoutes { /// <inheritdoc/> public override void Process(PipelineArgs args) { var routes = RouteTable.Routes; routes.MapRoute("MyAction", "myapi/myaction/{itemId}", new { controller = "MyCustomController", action = "CustomAction" }); } } } |
with config patch:
1 2 3 4 5 6 7 8 9 |
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/"> <sitecore> <pipelines> <initialize> <processor type="SmartSitecore.Feature.MyFeature.Pipelines.InitializeMyRoutes, SmartSitecore.Feature.MyFeature" patch:before="processor[@type='Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc']"/> </initialize> </pipelines> </sitecore> </configuration> |
Additionally for server side rendering we need to remember to exclude /myapi/
path from SSR, so JSS will not try to call layout service for it, but proxy it directly to Sitecore server.
Now we can call our custom API like this from JSS app:
https://<host>/myapi/myaction/{116ae27f-4f9c-445e-b82e-9cb0ef9fa2b8}?sc_apikey={75be4c38-ac59-47db-9098-d4b319fef46a}
and results will be similar to what Layout Service would return:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
{ "fields": { "textField": { "value": "This is text" }, "linkField": { "value": { "href": "" } }, "children": [ { "id": "5908340c-493a-40fe-9a40-1ce2211cd46a", "name": "SubItem1", "displayName": "SubItem1", "fields": { "subField1": { "value": "Value" }, "subField2": { "value": "Some value" } } }, { "id": "3af6d5d5-a122-42cd-bb56-aef494f15b56", "name": "SubItem2", "displayName": "SubItem2", "fields": { "subField1": { "value": "Other value" }, "subField2": { "value": "Some other value" } } } ] } } |