Try our conversational search powered by Generative AI!

Nicklas Israelsson
Oct 30, 2018
(10 votes)

Routing in a SPA with a working On-Page Editing experience

Since 11.24.0 the features described in this blog post are no longer considered beta features and are available to every user. The code examples have been updated to reflect that.

To demonstrate some concepts useful when creating a SPA with working OPE we are releasing a new SPA template site on Github, called MusicFestival, together with a series of blog posts. Don’t be discouraged that the SPA is written in Vue.js if you’re using React, Angular or something else, as these concepts will work in any client side framework.

Introducing a new SPA template site: MusicFestival

When creating a SPA and navigating inside the CMS UI, you might notice that the context isn’t what you expect, or that the page navigation tree isn’t updated.

Example of failing context switching

In this gif we demonstrate that the page tree does not update when clicking links inside the editing iframe. Thanks to ‘Andreas J’ that commented on our first OPE and client-side rendering blog post with this scenario.

The answer to the scenario presented above is:

  • Use regular anchor tags in edit mode with the expected URL format.
  • Present page and block models as client side components.

This blog will go in depth on how to create the expected URL formats as there are several things you need to consider and watch out for. We will also show you how to use the new features included in the CMS UI 11.24.0 release to solve the more advanced scenarios. Those features are:

  • New epiReady message.
  • Updated the global epi object with properties isEditable, inEditMode and ready.

We will talk a lot about routing, so if you’re not very familiar with it see our learning path article for an introduction.

Routing table on the client

When using client side routing, you need to consider URLs on your site. If you want to use a client side routing framework, you are free to write your own routing table and take responsibility for creating URLs and matching them with your content. There are, however, a number of advantages to making sure that you use the same routes in your client side routing as Episerver does when it comes to URLs, so we strongly recommend it. We also recommend that you keep the notion that one URL represents one page model in the CMS. This is the concept that the UI is built upon and following that concept will make the editing experience nicer and easier to get right.

The biggest upside to keep the default routing is probably that you can use the URL to get the content JSON that matches the path. You can use a solution similar to the one that Johan Björnfot created called ContentDeliveryExtendedRouting to get content from any Episerver URL by just setting the accept-header to JSON. (You don’t need to use the ContentDeliveryAPI to use his code as long as your API accepts content links and you use the same URLs that Episerver does).

Another upside with using the default routing that is easily overlooked, is how to deal with languages and different versions of content. This affects editing and can be complicated to get right with a custom routing implementation.

Here is an example of how simple the routing table on the client becomes if the SPA uses the same URLs as Episerver. Simply route all URLs to one component that has the responsibility to load the correct SPA page-component.

// example in Vue.js
routes: [
        path: '*',
        component: PageComponentSelector

The whole file can be viewed in our new template site on Github.

Getting the page navigation tree to match the current viewed page

As you noticed in the above GIF, the links you have in your template will look like they work when clicked in OPE, but the page tree and other widgets will not update if you use a framework like Vue.js router  or React router. This is because no page request is going to the server, so the CMS UI doesn’t change context

We considered adding a communication path for templates to request a page tree update, but then the client would need to know how to map between the Episerver URLs and the sites client side URLs and it’s less work for the template to not duplicate the routing table (see previous section "Routing table on the client").

There are two ways of solving this:

  • Disable all links, or make them editable, so the user can only navigate the site with the page tree.
  • Keep them working, which we recommend as some users have very large websites.

To make the second option work, you can disable client side routing and instead use regular anchor tags whenever the site is rendered in edit mode. In order for anchor tags to work, you need to make sure that the URL is the correct URL for the mode that the content is rendered in.


Consider the different view modes: View, Edit, Preview

In Episerver, there are three modes available: Edit-, preview- and regular view-mode. A page has different URL in each mode when Episerver resolves them.

  • View mode: When the content is delivered to the site visitor.
url: "/en/artists/"
  • Edit mode: When the content is loaded in the CMS UI, and the content is editable in OPE.
url: "/EPiServer/CMS/Content/en/artists,,6_7/?epieditmode=True"
  • Preview mode: When the content is loaded in the CMS UI, but rendered as it would in View mode.
url: "/EPiServer/CMS/Content/en/artists,,6_7/?epieditmode=False"

In a standard site using regular MVC controllers, you can, for example, use something like this to get the current mode for a request:

EPiServer.Web.IContextModeResolver contextModeResolver;

var currentMode = contextModeResolver.CurrentMode;

This option is unfortunately not available in a site using AJAX requests to an API to get the data to render since traditional Episerver routing is not used. This makes it hard to know what kinds of URLs to resolve when serializing the models.

URLs resolved by Episerver use a query string parameter called epieditmode to keep track of this. The value will be True if the content is in edit mode. The value will be False if the content is in preview mode. If the value does not exist, we assume that we’re in regular view mode.

There are two parts that need this query string: the request from the SPA’s client code and the response from the server’s API.

Point of views

Responsibilities of the client

In the SPA, you can figure out what mode it’s loaded in by looking at the epieditmode query string on the window.location. The client should include this information in the requests to the server API.

Without regular MVC Razor views, the SPA becomes responsible for rendering the correct mode. It should not render OPE attributes (e.g. data-epi-property-name) in View mode for example. To set the right editing context in CMS UI (to update the page tree, etc), the SPA site needs to have disabled the router links and use regular <a>-tags, but it also needs to set the correct URL in the src attribute. To save you some work on parsing URL queries, we’re introducing some new properties, including a isEditMode, and a new topic message on the injected epi object. We’ll introduce the epi object in the section called "New features in CMS UI".

For the API to respond with URLs for the correct mode, the client needs to send its current mode in the request, such as including it as a query parameter. In our new template site, you can see it looks like this:

GET /EPiServer/CMS/Content/en/artists,,6/?epieditmode=True&expand=* HTTP/1.1
Accept: application/json

Responsibilities of the API

For the client to be able to render correct links, the JSON returned by your API needs to have URLs with the content link (including work id) and the right context query on it whenever serializing a page’s URL. Using the IUrlResolver interface, this will be easy.

_urlResolver.GetUrl(contentLink, language, new UrlResolverArguments
    ContextMode = contextMode

For example, a response to the request shown above (with query parameter epieditmode=True) would look like this in our new template site:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
    "name": "Artists",
    "url": "/EPiServer/CMS/Content/en/artists,,6_7/?epieditmode=True"

You can see how this can be implemented when using the ContentDeliveryAPI and the ContentDeliveryExtendedRouting in our new template site.

Compare mode shows two views at the same time

Compare mode is not really a mode of its own since it’s an extension of edit mode that shows two different versions at the same time, but it is still good to consider it since you might need to make some adjustments for that in your client side rendering in order for it to work properly. For instance, you will want to turn off any event handler for the contentSaved topic message in the right hand side iframe so that you don’t update the version shown there when the left iframe is being edited. In version 11.24.0, we’re introducing some new properties, including an isEditable on the injected epi object that can be used to know when to listen to the contentSaved topic message. This we’ll introduce in the section called "New features in CMS UI".

Note that compare mode URLs looks the same as edit, which can be confusing when the loaded content version (such as “Published”) is usually rendered as in preview mode.

  • Compare mode: When the content is loaded in the CMS UI alongside another version of the content that is also being loaded in the CMS UI. The left side view can be editable in OPE if it’s a draft version.
left side (draft) url: "/EPiServer/CMS/Content/en/artists,,6_139/?epieditmode=True"

right side (published) url: "/EPiServer/CMS/Content/en/artists,,6_7/?epieditmode=True"

New features in CMS UI 11.24.0

We’re introducing a new event that we call epiReady. This event will be raised as soon as the iframe inside the UI is loaded. The event will contain information regarding if the current content is editable. In addition to that event, we will also set a few properties on the global epi object that is available in edit mode. It’s the same epi object that you use to subscribe to the different events such as contentSaved and the aforementioned epiReady. The epi object will look like this:

epi: {
    isEditable: true,
    inEditMode: true,
    ready: true

The object is constructed using the info from the epiReady event and will not get the correct isEditable value until the event has been raised. By looking at the ready property you will know if the beta object has been initialized or not. Here is a sample of how this event and object could be used:

const context = {
    inEditMode: false,
    isEditable: false

// Listen to the `epiReady` event to update the `context` property.
window.addEventListener('load', () => {
    // Expect `epi` to be there after the `load` event. If it's not then we're
    // not in any editing context.
    if (!window.epi) {

    function setContext() {
        // The event only has `isEditable`, but the epi object has both.
        context.inEditMode = window.epi.inEditMode;
        context.isEditable = window.epi.isEditable;

    // Check for beta and that ready is an actual true value (not just truthy).
    if (window.epi.ready === true) {
        // `epiReady` already fired.

    // The subscribe method won't be available in View mode.
    } else if (window.epi.subscribe) {
        // Subscribe if the `epiReady` event hasn't happened yet.
        window.epi.subscribe('epiReady', () => setContext());

Related links

Template site using the techniques discussed in this blog:

Documentation on client side rendering in Episerver:

Learning path documentation on page tree and routing:

Documentation on routing:

Oct 30, 2018


Please login to comment.
Latest blogs
Create your first demo site with Optimizely SaaS/Visual Builder

Hello everyone, We are very excited about the launch of our SaaS CMS and the new Visual Builder that comes with it. Since it is the first time you'...

Patrick Lam | Jul 11, 2024

Integrate a CMP workflow step with CMS

As you might know Optimizely has an integration where you can create and edit pages in the CMS directly from the CMP. One of the benefits of this i...

Marcus Hoffmann | Jul 10, 2024

GetNextSegment with empty Remaining causing fuzzes

Optimizely CMS offers you to create partial routers. This concept allows you display content differently depending on the routed content in the URL...

David Drouin-Prince | Jul 8, 2024 | Syndicated blog

Product Listing Page - using Graph

Optimizely Graph makes it possible to query your data in an advanced way, by using GraphQL. Querying data, using facets and search phrases, is very...

Jonas Bergqvist | Jul 5, 2024