Sitecore and Single Page Applications a match not made in heaven, but let's do it anyway

Sitecore and Single Page Applications: a match not made in heaven, but let's do it anyway!

Introduction

A hot topic these days in our company is how to make use of the latest and the greatest of Frontend frameworks like Angular and React and give the user a true Single Page Application (from now on referred to as SPA) experience, in combination with Sitecore to enable leveraging Sitecore's main strengths that are important to our clients:
  • Personalization
  • A/B testing
  • Engagement plans
  • Tracking / Analytics

This blog will show you one possible (I'm not saying perfect or ideal ;-)) solution that makes the combination possible.
For this blog I am assuming an implementation in Angular 2(+), but the same approach applies when using for example React.
Some sections may refer to implementations as explained in my previous blog.

Bootstrapping

In an SPA it is common to want the frontend implementation to take care of bootstrapping of the root component/application. So let's say we have application 'My SPA' (app-myspa) with two components: 'My Cool Header' (comp-header) and 'My cool content block' (comp-block). In Javascript the app and components are registered.
In a common SPA approach the frontend implementation determines the treeview of the app. In this case the tree could look like:

 <html>   
 <header></header>
 <body>
  <app-myspa>
    <comp-header></comp-header>
    <comp-smallblock></comp-smallblock>
  </app-myspa>
 </body>
</html>

In an implementation with Sitecore however it is common to give the content editor some control over what the app looks like and to change tons of things like content, personalization etc: so in this implementation the control over the app tree is delegated to Sitecore. To do this a simple template was created to be used in the Sitecore tree item structure containing two fields: DeclarationName and StaticUrlItem.
DeclarationName is used for serverside rendering of the root app's (or components') defining HTML tag. The StaticUrlItem is an optional field and is based on the idea that one can choose for an implementation where a component can have a different HTML structure implementation that may be used for personalization or A/B testing. By selecting a different view it is possible to instruct Angular to read the component's static HTML from a dynamic url attribute from the component's declaration element (read more about the Dynamic url implementation in my previous blog here).

The idea behind this tree definition in Sitecore is to create a rendering that returns the tree view as defined in Sitecore with the corresponding HTML structure to enable the Angular app to bootstrap the components on the correct location in the app.
Based on this a appInstance Sitecore item is created (or pageItem if you prefer to call it that)  with the rendering on it that generates this structure. The code for this rendering is as follows (please ignore the magic strings in this PoC code):


    public class AppSkeletonController : SitecoreController
    {
        public override ActionResult Index()
        {
            var model = new SkeletonStructure();

            if(RenderingContext.Current.Rendering.Item.TemplateName == "AppComponent")
            {
                model.RootComponent = new Component();
                model.RootComponent.DeclarationName = RenderingContext.Current.Rendering.Item.Fields["DeclarationName"].Value;
                model.RootComponent.StaticUrl = RenderingContext.Current.Rendering.Item.Fields["StaticUrlItem"].Item.Paths.ContentPath;
                GetSubComponents(RenderingContext.Current.Rendering.Item.Children, model.RootComponent);
            }

            return View("Skeleton", model);
        }

        private void GetSubComponents(ChildList children, Component component)
        {
            foreach (Sitecore.Data.Items.Item child in children)
            {
                if (child.TemplateName == "AppComponent")
                {
                    if (component.SubComponents == null)
                    {
                        component.SubComponents = new List<Component>();
                    }
                    var childComponent = new Component
                    {
                        DeclarationName = child.Fields["DeclarationName"].Value,
                        StaticUrl = child.Fields["StaticUrlItem"].Item.Paths.ContentPath
                    };

                    GetSubComponents(child.Children, childComponent);
                    component.SubComponents.Add(childComponent);
                }
            }

        }
    }
    public class Component
    {
        public string DeclarationName { get; set; }
        public string StaticUrl { get; set; }

        public List<Component> SubComponents { get; set; }
    }
    public class SkeletonStructure
    {
        public Component RootComponent { get; set; }
    }

Using this approach one can create different appInstances with different component structures. This can be used for examply for a/b tests of different combinations of components in the app. Keep in mind that obviously the extent to which components can be placed on a different location in the app tree depends on the limitations on this in the client (Angular in this case) implementation with regards to flow in the app (is the component shown at the right time at a logical location) and how adaptive is the HTML to correctly render on a different location.

Component rendering and tracking

So now that there is a app tree served by Sitecore and the Angular app can start bootstrapping the components, Angular needs to retrieve each component's HTML from somewhere. In this approach this is solved by having a separate content branch in the Sitecore content tree that has an entry per component. In the example the structure is quiet flat due to its limited size but in a large SPA with many components it is good to group them in separate folders.
Each component is linked to a component-template based item in Sitecore and is also available on the corresponding endpoint: https://{yoursite}/component/componentA
A component has a layout that merely consists of one line of code:

   @Html.Sitecore().Placeholder("component")

The Sitecore item can then contain one or more renderings of different types: controller, view, basically whatever is preferred. This will generate the HTML template for the specific component, adding all the Sitecore content, personalization rules etc: that is required. Just keep in mind that with each item load a 'page visit' is tracked. And with an SPA, this doesn't necessarily mean the user has actually seen this piece of HTML. So you might need to do something additional with pagevents, goals etc: to correctly measure use of the app. For example trigger them from Javascript events on button clicks.

The razor view to render skeleton with Components looks like this:
Skeleton.cshtml:
@model Website.Controllers.SkeletonStructure

@if(Model.RootComponent != null)
{
    @Html.Partial("Component", Model.RootComponent)
}



Component.cshtml

@model Website.Controllers.Component

@if (!string.IsNullOrEmpty(Model.StaticUrl))
{
    @Html.Raw(string.Format("<{0} url='{1}'>", Model.DeclarationName, Model.StaticUrl));
}
else
{
    @Html.Raw(string.Format("<{0}>", Model.DeclarationName));
}

@if (Model.SubComponents != null && Model.SubComponents.Count > 0)
{
    foreach (var component in Model.SubComponents)
    {
        @Html.Partial("Component", component);
    }
}

@Html.Raw(string.Format("</{0}>", Model.DeclarationName))




Now let's go full separation!

So what if an SPA is not enough and frontend developers want to be fully sepseerated with their development? Well there are options! 
With the above approach, experience editor support is very far away. While it is not impossible to make it work, it would require a huge amount of work, and one should consider whether it makes any sense, as the current version of the experience editor is about editing pages, and you have only one here.
Ok so let's ignore experience editor support for now.
To separate front- from backend development it is needed to feed the Angular app with content through HTML attributes. In this way Sitecore renders the app's tree (skeleton) with all the elements for each of the components. However in a content attribute all the content to be used in the component is added as a JSON structure (or whatever structure is easy to read). 
This requires a little bit more programming though. Controller- and view renderings are not used anymore for the component's HTML. This HTML is now maintained by frontend developers in separate files. The files can still be deployed to your Sitecore instance and made accessible through an ignored url, or they can even be hosted somewhere else, whatever is preffered.
In this case data templates should be added to each of the app tree component leaves and content can be added (title, content blocks, etc). The app skeleton is then extended with controller and view to read content in a generic way and a JSON structure is created from it that is inserted as an attribute (below code demonstrates this). In this way the frontend developers can develop their JS, CSS and HTML for all components anyway they like, while still feeding it with content from the JSON structure.
Changes to skeleton controller:

using System.Collections.Generic;
using System.Linq;
using Sitecore.Mvc.Controllers;
using System.Web.Mvc;
using Sitecore.Collections;
using Sitecore.Mvc.Presentation;

namespace Website.Controllers
{
    public class AppSkeletonController : SitecoreController
    {
        public override ActionResult Index()
        {
            var model = new SkeletonStructure();

            if(RenderingContext.Current.Rendering.Item.TemplateName == "AppComponent" || RenderingContext.Current.Rendering.Item.Template.BaseTemplates.Any(t => t.Name == "AppComponent"))
            {
                model.RootComponent = new Component();
                model.RootComponent.DeclarationName = RenderingContext.Current.Rendering.Item.Fields["DeclarationName"].Value;
                model.RootComponent.StaticUrl = RenderingContext.Current.Rendering.Item.Fields["StaticUrlItem"].Item.Paths.ContentPath;
                model.RootComponent.ItemId = RenderingContext.Current.Rendering.Item.ID;
                GetSubComponents(RenderingContext.Current.Rendering.Item.Children, model.RootComponent);
            }

            return View("Skeleton", model);
        }

        private void GetSubComponents(ChildList children, Component component)
        {
            foreach (Sitecore.Data.Items.Item child in children)
            {
                if (child.TemplateName == "AppComponent" || child.Template.BaseTemplates.Any(t => t.Name == "AppComponent"))
                {
                    if (component.SubComponents == null)
                    {
                        component.SubComponents = new List<Component>();
                    }
                    var childComponent = new Component
                    {
                        DeclarationName = child.Fields["DeclarationName"].Value,
                        StaticUrl = child.Fields["StaticUrlItem"].Item.Paths.ContentPath,
                        ItemId = child.ID
                    };

                    GetSubComponents(child.Children, childComponent);
                    component.SubComponents.Add(childComponent);
                }
            }

        }
    }
}

Changes to the component view which renders the different components' tags:


@using Newtonsoft.Json.Linq;
@using System.Dynamic;
@using Sitecore.Data.Items;
@using Sitecore.Mvc;
@model Website.Controllers.Component

@{ 
    string attr = string.Empty;
    if (RenderingContext.Current.Rendering.Item.TemplateName == "AppComponent" || RenderingContext.Current.Rendering.Item.Template.BaseTemplates.Any(t => t.Name == "AppComponent"))
    {
        var contentObject = new Dictionary<string, string>();
        var sitecoreItem = RenderingContext.Current.ContextItem.Database.GetItem(Model.ItemId);
        foreach (Sitecore.Data.Fields.Field field in sitecoreItem.Fields)
        {
            if (field.Name != "DeclarationName" && field.Name != "StaticUrlItem" && !field.Name.Contains("__")) // component properties and default sitecore fields
            {
                contentObject.Add(field.Name, Html.Sitecore().Field(field.Name, sitecoreItem).ToHtmlString());
            }
        }

        attr = Newtonsoft.Json.JsonConvert.SerializeObject(contentObject);
    }
}

@if (!string.IsNullOrEmpty(Model.StaticUrl))
{
    @Html.Raw(string.Format("<{0} url='{1}' data-content='{2}'>", Model.DeclarationName, Model.StaticUrl, attr));
}
else
{
    @Html.Raw(string.Format("<{0} data-content='{1}'>", Model.DeclarationName, attr));
}

@if (Model.SubComponents != null && Model.SubComponents.Count > 0)
{
    foreach (var component in Model.SubComponents)
    {
        @Html.Partial("Component", component);
    }
}

@Html.Raw(string.Format("</{0}>", Model.DeclarationName))


Here I made an extension of the AppComponent template for each component with content fields to be added to the JSON structure. As long as the components' templates inherit from the AppComponent template this works.
Output of the app tree now looks like this:

<html>
 <head>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <script src="/public/scripts/main.bundle.js"></script>
    
 </head>

 <body>
  <div class="container">
        

   <app-myspa url='/' data-content='{}'>
    <comp-header url='' data-content='{}'>

    </comp-header>
    <comp-smallblock url='' data-content='{"AnImage":"<img src=\"/-/media/Experience-Explorer/Presets/Emilie-128x128.ashx?h=128&amp;la=en&amp;w=128&amp;hash=DCBF4A21776154962A0229E48F6402F194441B8B\" alt=\"Emilie\" width=\"128\" height=\"128\" />","Title":"Some cool title","SomeRichText":"<h3>Some cool rich text</h3>","AnotherLine":"yet another content line"}'>

    </comp-smallblock>

   </app-myspa>

  </div>
 </body>
</html>



Considerations

Things to think about when using this approach:
  • Which components can live where in the structure: consider whether to create specific app-component templates or layouts with different placeholders to limit which components can be added where in the tree, to match what will work on the frontend implementation with regards to bootstrapping and layout. Creating an interaction design or at least a definition on possible 'app-flows' can help a developer in correctly creating such a structure
  • Which components are dependent on each other and should therefore not be placed independently in the structure: in such a case consider creating the components together in a fixed rendering with fixed component tag and a dynamicUrl reference to pinpoint the correct component view.
  • In this SPA setup, no page (re)loads will be done, this means you will have to make sure you load all the required javascript for all your components on the initialization of your app. The best approach here at the moment is to add one bundle to rule them all using something like webpack (until Sitecore starts supporting operating systems that support HTTP2 and you can load different packs efficiently on the app initialization)
  • If you want to use this setup in a open environment which needs search engine indexing, you have to consider server side rendering, or your different routes will be indexed with the same content, this approach does not solve this by itself
  • Using this setup you can create as many SPA's in your Sitecore instance as you like and even link them together. I would consider carefully here though whether you need a new SPA or page depending on your needs. It can be hard when moving to an SPA way of thinking when you were always used to create pages. In a true SPA setup, you should not need new instances or pages to do what you want, only an additional component in your app tree and app/angular logic to show it at appropriate events.

Reacties

Populaire posts van deze blog

I Robot - Sitecore JSS visitor identification

Sitecore campaigns and UTM tracking unified

Sitecore JSS - Sitecore first