Redirects and the Layout Service

In a typical Sitecore ASP.NET server-side rendered solution, redirects are handled by returning a 301 or 302 HTTP status code to the client browser to signal that the client should redirect to a particular URL.

In a Sitecore Headless solution, things might not be quite so simple. For one thing, the Layout Service does not readily provide a way to return a 301/302 redirect status code in the HTTP response from the API. In addition, even if you convinced the Layout Service to return a 301, if you were using the JSS SDK in a JavaScript application running on the client browser, in-app routing might need to handle redirects returned from the API call and identify which route to use and redirect accordingly (in the app itself, not with a full page reload.) Turns out that isn’t as simple as it might appear either.

Both kinds of music – country AND western!

There are at least two distinct types of redirect that we might want to handle in our headless Sitecore application:

  • Redirects when no item or route exists in Sitecore that matches the requested item
  • Redirects when an item exists and its context resolves, but we want it to redirect elsewhere

Redirects where no item exists (these aren’t the items you are looking for)

This is the typical “vanity URL” scenario. For example we might have published a nice, friendly URL for potential customers such as which will redirect to It’s easier to remember and easier for the user to digest. It might also have SEO benefits to use the first URL rather than the second one.

Sure, we can handle this using a CDN or via web server modules like URL Rewrite, but that isn’t very helpful to our digital producers who want to manage redirects themselves in the CMS and publish the new vanity URL alongside a marketing campaign.

This is where modules like the 301 Redirect Module or SXA Redirects come in. Both of these empower your users to manage redirects in the Sitecore tree, but they were not designed to work with Sitecore Headless Services. That said, you could leverage either option, or roll your own, and patch it in. I’ve outlined how you might do that later in this post.

Redirects where an item exists (I resolve therefore I am)

In this scenario we might have a node in the Sitecore content tree that has no content but has descendants that do. Perhaps a Category page that holds all items in a particular category, but that has no actual content itself.

Now you might argue that we shouldn’t have nodes in our IA that have no useful content – and I’d be on your side in that argument! – however some clients would appear to disagree, and so here we are.

Digging into the Layout Service code

Let’s look at each scenario and see how they might be achieved in the Layout Service, as well as some pointers on what to avoid.

Redirects with no Sitecore items

As mentioned earlier, patching into the usual MVC pipelines is no use with the Layout Service because it just blows right past those suckers and does its own thing in a Controller API. The controller that runs the show is in:


It inherits from System.Web.Mvc.Controller and exposes the “placeholder” and “render” actions in the Layout Service API via MVC controller action methods. The first thing that I looked at was overriding the virtual methods, but after going down a rabbit hole it became apparent that, although overriding the methods would be simple enough, setting up the constructor was more problematic and needed quite a bit of hackery which would prove hard to maintain and upgrade in the future. I then looked into patching the pipeline.

Finding the right place to patch was the key to doing this. Long story short, the place to patch is in the <mvc.requestBegin> pipeline, just after Sitecore.LayoutService.Mvc.Pipelines.RequestBegin.ContextItemResolver like so:

<?xml version="1.0"?>
<configuration xmlns:patch="" xmlns:set="">
<processor type="Mysite.Feature.Redirects.Pipelines.RedirectHandler, Mysite.Feature.Redirects" resolve="true"
patch:after="*[@type='Sitecore.LayoutService.Mvc.Pipelines.RequestBegin.ContextItemResolver, Sitecore.LayoutService.Mvc']"/>

Then in your RedirectHandler you can add whatever approach floats your boat to identify and process redirects, returning the appropriate 301/302 status code and URL.


As an Australian politician and Prime Minister once said, “Life wasn’t meant to be easy”. In this case there’s a couple of gotchas.

In a JSS application, a client browser requests the initial app from the server and on subsequent requests calls back to the Layout Service API for the route data. This means that there are actually two scenarios to consider: the initial request to the server and the subsequent calls to the API. How you handle this is dependent on your topology and your choice of SDK.

For example, in an SSR setup with a Node.js proxy, your proxy app will need to be able to handle redirects both from the initial request, and on subsequent proxied API requests. In a CSR setup this will be handled by your JSS app. In either case, your JSS app will need to be able to handle HTTP responses with redirect status codes.

Working on this with my front end colleague in an SSR topology using the React JSS SDK, we encountered issues with Axios following 301s without any way of trapping it. It was fine for the initial request, but we were unable to handle Layout Service API calls that returned 301 in the React app. Your mileage may vary, and if you are using .NET rendering host then this should not be an issue.

Another gotcha is CORS. The LayoutServiceController has a bunch of ActionFilterAttributes, one of which is EnableApiKeyCors. This adds some CORS magic to the response and you might need to add this code to the response your redirect handler, depending on your scenario. For reference, you might add something like this to your response code:

HttpResponseBase response = filterContext.HttpContext.Response;
response.Headers["Access-Control-Allow-Origin"] = header;
if (string.IsNullOrWhiteSpace(response.Headers.Get("Access-Control-Allow-Headers")))
response.AddHeader("Access-Control-Allow-Headers", "*");
if (string.IsNullOrWhiteSpace(response.Headers.Get("Access-Control-Allow-Credentials")))
response.AddHeader("Access-Control-Allow-Credentials", "true");
if (string.IsNullOrWhiteSpace(response.Headers.Get("Access-Control-Allow-Credentials")))
response.AddHeader("Access-Control-Expose-Headers", "ETag");
if (!string.IsNullOrWhiteSpace(response.Headers.Get("Access-Control-Max-Age")))
response.AddHeader("Access-Control-Max-Age", "10");
view raw CorsHeaders.cs hosted with ❤ by GitHub

Phew. This post is longer than I had intended. Nearly there.

Redirects where an item exists

This one is a bit easier. Option one is to have a fight with the customer and convince them to just put some content on the “node with no content”.

If that fails, you could create a custom item type in Sitecore to handle the redirection. I created a NavigationRedirectNode template that had a Treelist field from which you could pick an item from the site tree. Then in the code I just checked for that template ID and returned the redirect. In my case, because of the Axios issues mentioned above, we opted to return the route to which we wanted to redirect via the ItemContext (similar to this post) and handled that in the app. I’m not saying this is the only or best way – it’s just where we ended up.

However! There was a gotcha. The item has to have layout. It doesn’t have to have anything on it, but if you don’t have a Sitecore layout applied to the item then Sitecore jumps in, hijacks the request, and you end up at “The layout for the requested document was not found” with a red screen of doom.


As you can see, there’s a bit more to redirects when using Sitecore Headless Services compared to a “traditional” Sitecore MVC solution. The above represents my own experiences with applying redirects in a JSS React SSR solution, and is by no means comprehensive, but I hope that it provides some pointers on your own headless travels 🙂