Converting a Sitecore Website to a Single-Page App

partial rendering single page app Sitecore

Since the dawn of Javascript frameworks more and more websites are hopping on the Single-Page App wagon. And if you’re an old-fashioned Sitecore developer who doesn’t give a rat’s ass about SPAs, well, it’s time to shake things up, my friend, because the world (wide web) is changing for the better and so should you.

Now here comes the requirement to convert your existing Sitecore website to SPA without discarding your existing page items and you’re left scratching your head.

Say page A is our homepage and we need to replace page A’s content with page B’s content when the link to page B is clicked. Obviously, we’d be using XHR to get the main content of page B, and for this we could simply fetch page B’s entire content and use RegEx to grab whatever markup is rendered in the <main> tag – but that just ain’t sexy.

I found two approaches for this: you can either make use of a new device or the mvc.renderPlaceholder processor.

The Device Approach

A page item can have a different device that may use a new layout and set of renderings while serving the same content. This means you can potentially request a page’s content and force it to use a different device that has a trimmed down markup.

Let’s create a new device called ‘Partial’ that will be used instead of the ‘Default’ device when the parameter ‘partial=true’ exists in the query string.

partial device

Then in our example About page we set the presentation details for the Partial device to use a new Layout called Partial MVC Layout, and strip out all renderings except the One Column Grid and Rich Text.

presentation details

Now the Partial MVC Layout should use a layout file that only contains the placeholder for the main content.

partial layout

And when you view the source of the About page with ‘partial=true’ in the query string you should only see the grid container and the content of the rich text rendered in the main placeholder.

about partial page

The mvc.renderPlaceholder Processor Approach

In this approach we’ll create a pseudo Web API using an MVC controller because we need to access certain Context objects that are not available in a Web API Controller.

The Controller

Let’s create an MVC controller that accepts the id of the page item and the name of the placeholder, and emulates the page-item rendering. This controller has an attribute called ‘DisableTracking’ so that any call to the API will not register an interaction. I’ve mentioned this in my other blog about tracking client-side events.

public class PartialRenderingController : Controller
{
    [DisableTracking]
    public ActionResult GetPartialRendering(string itemId, string placeholder)
    {
        var item = Sitecore.Context.Database.GetItem(ID.Parse(itemId));

        PageContext.Current.Item = item;
        Sitecore.Context.Item = item;
 
        using (var sw = new StringWriter())
        {
            ContextService.Get()
                .Push<ViewContext>(
                    new ViewContext(
                        ControllerContext,
                        PageContext.Current.PageView,
                        ViewData, TempData, sw));
 
            var html = RenderPlaceholder(placeholder);
 
            return new FileContentResult(
                Encoding.UTF8.GetBytes(html), "text/html");
        }
    }
}

Rendering the placeholder

Here’s where the magic happens. Once we have a ViewContext we will run the default pipeline processor ‘mvc.renderPlaceholder’ to render the placeholder and return the rendered html markup.

public static string RenderPlaceholder(string placeholder)
{
    var sb = new StringBuilder();
 
    using (var sw = new StringWriter(sb))
    {
        var args = new RenderPlaceholderArgs(placeholder, sw)
        {
            PageContext = PageContext.Current
        };
 
        try
        {
            CorePipeline.Run("mvc.renderPlaceholder", args);
        }
        catch
        {
            // ignored
        }
 
        return sb.ToString();
    }
}

FYI: Since updating to Sitecore 8.2 I’ve been getting a runtime error when running the pipeline processor. Hence, I had to wrap it in a try-catch block. But this shouldn’t affect the result as far as I know. Feel free to comment about a better workaround.

Registering the Route

Though attribute routing is working for 8.2, I would advise against it for this purpose, since the good people from Sitecore told me that it does not support the initialization of the PageContext object. Therefore, we’re still gonna use the good ol’ MapRoute method.

First we need to create a pipeline processor that will register the controller route. I’m assuming that your main content’s placeholder is called ‘main’ so we’ll add it as a default.

public class RegisterPartialRenderingRoute
{
    public virtual void Process(PipelineArgs args)
    {
        RouteTable.Routes.MapRoute(
            name: "GetRendering",
            url: "api/partialrendering/{itemId}/{placeholder}",
            defaults: new {
                    controller = "PartialRendering",
                    action = "GetPartialRendering",
                    placeholder = "main" }
            );
    }
}

Then we’ll create a custom config file to register the processor. Make sure it will be run before ‘Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, Sitecore.Mvc’, and make sure the config will be patched after the default ones, therefore the config file’s directory path should be similar to this: App_Config/Include/Z.MyProject

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" 
   xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor
           type="MyProject.RegisterPartialRenderingRoute, MyProject"
           patch:before=
              "*[@type='Sitecore.Mvc.Pipelines.Loader.InitializeRoutes, 
                      Sitecore.Mvc']" />
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

Now call the API route with the About page item’s ID and the main placeholder name, and it should return the content of  the main placeholder.

partial rendering api

Be aware that this solution only works for outermost placeholders and it won’t work when you pass a path to an inner placeholder (e.g. “/main/tabs/first-tab”). For that you may have to reverse-engineer and replace the default mvc.renderPlaceholder pipeline processor.

The Sample Single-Page App

My good friend, Aleksej Dix, was kind enough to help me create a simple Vue.js app to demonstrate the markup-fetching using the API from the second approach. If you don’t know anything about Vue.js yet make sure to read about it and prepare to be blown away.

sitecore-vuejs

And here’s how the Vue.js template and script look like.

<template>
  <div>
    <nav>
      <a
        class="link"
        v-for="route in routes"
        :key="route.href"
        :href="route.href"
        @click.prevent=
          "fetchFragment('http://mysite.local/api/partialrendering/'
             + route.pageId)"
        >{{route.title}}</a>
    </nav>
    <main id="app" v-html="sitecore">
       // Loaded markup will be rendered here.
    </main>
  </div>
</template>

<script>
export default {
  name: 'app',
  data () {
    return {
      routes: [
        { 
          href: '#', 
          title: 'Home', 
	  pageId: '90ea7995-aa44-4b37-90c4-803cf675e4b8'
	},
        { 
	  href: '#about', 
	  title: 'About', 
	  pageId: 'b5eb3621-36c4-46f6-8acb-dc26738992ba'
        }
      ],
      sitecore: ``
    }
  },
  methods: {
    async fetchFragment (url) {
      const headers = new Headers({
        'Content-Type': 'text/plain',
        'redirect': 'follow'
      })
      const options = {
        headers
      }
      const request = await fetch(url, options)
      this.sitecore = await request.text()
    }
  },
  mounted () {
    this.fetchFragment('http://mysite.local/api/partialrendering/'
    + this.routes[0].pageId)
  }
}
</script>

Registering Page Interactions

It is a no-brainer that we should keep our analytics intact and meaningful. But since we have a single-page app, unfortunately all page interactions will be registered to that page, which in this case is our Home page.

To fix this we’ll create another pseudo Web API that will generate a new page interaction based on the item ID passed to it.

[HttpPost]
public ActionResult TrackPage(string pageItemId)
{
    var pageItem = Context.Database.GetItem(ID.Parse(pageItemId));
    var page = Interaction.GetOrCreateCurrentPage();
    var localPath = LinkManager.GetItemUrl(pageItem);

    // Setup PageContext
    page.SitecoreDevice.Id = Context.Device.ID.Guid;
    page.SitecoreDevice.Name = Context.Device.Name;
    page.Url.Path = localPath;
    page.SetItemProperties(
        pageItem.ID.Guid, pageItem.Language.Name, 
        pageItem.Version.Number);

    return new HttpStatusCodeResult(HttpStatusCode.OK);
}

Make sure to register the route as we did for the PartialRendeing API.

Now post to this API right after fetching the partial page markup and the PageContext will be set to whichever page item the loaded markup is taken from. If you’re trigerring client-side page events, rest assured they will be registered to this page.

Wrapping it up

The approaches I’ve demonstrated will help you achieve SPA migration without having to sacrifice certain Sitecore features like designing pages in the Experience Editor and content personalization. Personally, I prefer the mvc.renderPlaceholder approach (albeit hacky) to the device approach, because I often deal with multi-sites with more than just a handful of pages. And maintaining just an API to do the partial rendering is way more efficient than maintaining a new set of layout details per page, especially if you got tons of it.

Happy programming!

No Thoughts to Converting a Sitecore Website to a Single-Page App

Comments are closed.