Converting Sitecore MVC sites to Jamstack with Headless Services and JSS – Next.js

In this post I’ll be showing an approach to convert existing Sitecore MVC applications to the Jamstack architecture, it’s time to think about how to modernize our old-fashioned Sitecore apps to benefit from modern tech stacks capabilities like headless, SSG, ISR, multi-channel, etc.


Jamstack architecture for existing Sitecore MVC sites is possible because of the ability of the Sitecore Layout Service to render MVC components to HTML, and include them in its output.

Jamstack architecture with Next.js. Ref: here

The publishing and rendering process consists of the following steps:

  1. The Layout Service outputs MVC components as HTML, embedded in its usual service output.
  2. The Layout Service output is published to the Content Delievry with each page/route, allowing it to be queried by Sitecore headless SDKs such as Next.js.
  3. The Next.js application queries the Layout Service output for the route and passes it into one or more placeholder components.
  4. Based on the lack of a componentName property in the layout data, the Placeholder component in the Sitecore Next.js SDK renders the Sitecore component directly as HTML into the pre-rendered document.


  • Sitecore version 10.2+ – An upgrade of your MVC application would be needed.
  • Sitecore Headless Services module version 19+.


To make things easier for this demo, I’m using the “Basic Company – Unicorn” site from the Sitecore Helix examples, you can find the repo here.

The first step is to upgrade the solution to 10.2, you can also find my open PR with the upgrade here.

Then, we need to add the Headless Services to our CM and CD images. You can find the final code here, which also adds on top of it a Next.js RH app image.

At this point, we have our MVC application up and running on Sitecore 10.2 and Headless Services are also installed. We are now ready to start making some changes to the app so we can make it work with JSS.

Prepare the MVC site to be compatible with JSS App

First of all, we need to create our API key in order to allow the Layout Service to communicate through our Sitecore instance. For that, we simply create an item under /sitecore/system/Settings/Services/API Keys

Make sure to keep CORS and controllers to allow * for this demo

To enable editing and static generation support in the JSS app, we have to make the site root to inherit from /sitecore/templates/Foundation/JavaScript Services/App template:

To enable editing support, we need the layout to inherit the template /sitecore/templates/Foundation/JavaScript Services/JSS Layout .

Now, we need to configure the Layout Service Placeholders field. This field determines which placeholder information to include in the Layout Service response data.

Inspect the Layout Service reponse

We can have a look now and analyze the Json we are getting from the Layout Service by visiting the endpoint:


Layout Service response

We can see in the response, that we are getting the placeholders we included previously (main, header and footer).

Configure the Sitecore Layout Service to output HTML for MVC renderings

Let’s now go and configure the “Hero Banner” component to render HTML instead of Json:

Done, let’s publish this change and see what we get in the Layout Service response for this component:

So, here we go, we can find the HTML now in the contents. Let’s enable the HTML output on all the other MVC renderings and publish those changes, in the meantime, let’s create our JSS app.

Create the Nextjs JSS app

Let’s open a terminal and navigate to the src folder (..\examples\helix-basic-unicorn\src\Project\BasicCompany). We now run the JSS CLI command to create a new app, here we can choose if we want to fetch data with REST or GraphQL, also the prerendering on SSG or SSR:

jss create basic-company nextjs --empty --fetchWith GraphQL --prerender SSG

The JSS app is now created. Let’s set it up and connect it to our Sitecore instance. Run the following CLI command:

cd basic-company
jss setup

Provide the following values:

1- Is your Sitecore instance on this machine or accessible via network share? [y/n]: y
2- Path to the Sitecore folder (e.g. c:\inetpub\wwwroot\ ..\examples\helix-basic-unicorn\docker\deploy\website
3- Sitecore hostname (e.g.; see /sitecore/config; ensure added to hosts): https://www.basic-company-unicorn.localhost/
4- Sitecore import service URL [https://www.basic-company-unicorn.localhost/sitecore/api/jss/import]:
5- Sitecore API Key (ID of API key item): {B10DB745-2B8A-410E-BDEC-07791190B599}
6- Please enter your deployment secret (32+ random chars; or press enter to generate one):

Now we can deploy the config (check the files creates under sitecore/config). For this we run the following CLI command

jss deploy config

Prepare the NextJs App to render our content

Let’s update the Layout.tsx to add our placeholders (header, main, footer):

Also copy the “basic-company.css” from the website folder into the “src/assets” folder and update the _app.tsx with this :

All good, time to connect and test it! Run the following CLI command:

jss start:connected

Yay! visit http://localhost:3000 and you can see the Basic Company MVC site rendered as a JSS App, this is ready to be deployed and make it statically generated, but let’s move one step forward and start converting one of the components to React, as I see this approach to incrementally start your migration to JSS (React).

Experience Editor compatibility

Let’s double-check that our Experience Editor is still working as expected:

Start converting components from MVC (C#/Razor) to Next.js (JavaScript/React) incrementally

Let’s duplicate the “Hero Banner” rendering in Sitecore, change the template to make it a “Json Rendering”, rename it to “HeroBanner” to make it compliant with React naming conventions, and disable the “Render as HTML” checkbox. Also, make sure the “component name” field is set to “HeroBanner”. Then add this new component to the Homepage next to the MVC one.

Duplicated HeroBanner component

Publish the rendering and check again the Layout Service response, now, you should be able to see the two versions of the component, the one in HTML and the Json:

Good! We got the expected results on the Layout Service response, if we go now and refresh our JSS App, we will see that the component is added but still lacking its React implementation:

Create the React component through the component scaffolding

To create the React implementation of the component we created, just run the following in the terminal (always from the JSS App root):

jss scaffold BasicContent/HeroBanner 

Have a look at the files created, make some changes to the React implementation (BasicContent/HeroBanner.tsx)

import { Text, Field, ImageField } from '@sitecore-jss/sitecore-jss-nextjs';
export type HeroBannerProps = {
  fields: {
    Title: Field<string>;
    Subtitle: Field<string>;
    Image: ImageField;
const HeroBanner = ({ fields }: HeroBannerProps): JSX.Element => {
  const bannerStyle = {
    backgroundImage: `url(${fields.Image?.value?.src})`,
  return (
    <section className="hero is-medium is-black" style={bannerStyle}>
      <div className="hero-body">
        <div className="container">
          <Text field={fields.Title} tag="h1" className="title" />
          <Text field={fields.Subtitle} tag="h2" className="title" />
export default HeroBanner;

Now both MVC and React components are living on the same site, I kept both to make it more visual, but the proper way of migrating would be just replacing the MVC rendering.

I hope you find this interesting, you can find the complete solution here, it’s a fork of the Sitecore Helix Examples and added on top the headless services, Sitecore 10.2 upgrade, a NextJS rendering host, and app.

Thanks for reading!

Ref Official documentation

Sitecore 9.x upgrade and dynamic placeholders

In the previous Sitecore versions dynamic placeholders where not there out of the box, so in case you needed them a custom solution was required. Good news came with Sitecore 9.x as that was integrated and ready to be used OOTB.

That’s great yes, but, there is always a “but” when it comes to solutions upgrade, the way we were generating the dynamic placeholders key could potentially be different to what Sitecore is doing now with the builtin feature.

One of the most common approaches was to use the solution based on the DynamicPlaceholders.Mvc nuget package, you might be familiar with the approach that Habitat solution was following:

@using Sitecore.Feature.Teasers
@using Sitecore.Foundation.SitecoreExtensions.Extensions
@using Sitecore.Foundation.Theming.Extensions
@using Sitecore.Mvc.Presentation
@model Sitecore.Feature.Teasers.Models.DynamicTeaserModel

@using (Html.BeginEditFrame(Model.Item.ID.ToString(), ""))
  var options = RenderingContext.Current.Rendering.GetCarouselOptions();
  <div class="owl-carousel" data-options='{"items": @options.ItemsShown, "navigation": @(options.ShowNavigation ? "true" : "false"), "navigationText": ["<", ">"], "autoPlay": @(options.AutoPlay ? "true" : "false") }'>
    @for (var i = 0; i < Model.Items.Count(); i++)
        @Html.Sitecore().DynamicPlaceholder($"teaser-placeholder-{i + 1}")

All good, so now the problems! After we finished with the upgrade, we noted that the content from Dynamic Placeholders was gone. We did some research and noticed that now the generated key was different:

Before we had something like that:


And now Sitecore is generating the keys like that:


Powershell to the rescue!!

We decided to go with what we think is the better approach to work with this kind of bulk content update, powershell. You can find the script here in Github.

function ProcessRendering{
    [parameter(Mandatory=$true, Position=0)]
    [parameter(Mandatory=$true, Position=1)]
    [parameter(Mandatory=$false, Position=2)]
	$phMatches = [regex]::Matches($_.Placeholder,'(_[0-9a-fA-F]{8}[-][0-9a-fA-F]{4}[-][0-9a-fA-F]{4}[-][0-9a-fA-F]{4}[-][0-9a-fA-F]{12})')

	if ($phMatches.Success) {
		Write-Host "Match found in item - [$($item.Paths.FullPath)]"
		$replacePh = $rendering.Placeholder
		$phMatches | ForEach-Object {
			$renderingId = $_.Value
			$replacePh = $replacePh.Replace($renderingId, "{$($renderingId.ToUpper())}-0")

		$replacePh = $replacePh.Replace('{_', "-{")
		$rendering.Placeholder = $replacePh
		Write-Host "Replacing [$($rendering.Placeholder)] with [$($replacePh)]"
		Set-Rendering -Item $item -Instance $rendering -FinalLayout:$param

$allPaths = "/sitecore/templates", "/sitecore/content"

New-UsingBlock (New-Object Sitecore.Data.BulkUpdateContext) {
	$allPaths | ForEach-Object -Process {
		Get-ChildItem -Path $_ -Version * -Recurse | ForEach-Object {
			$item = $_;
			Write-Host "Processing shared layout for item $($_.ID)" 
			Get-Rendering -Item $item | ForEach-Object {
				ProcessRendering $item $_ $false
			Write-Host "Processing final layout for item $($_.ID)" 
			Get-Rendering -Item $item -FinalLayout | ForEach-Object {
				ProcessRendering $item $_ $true

After running it you will get all your dynamic placeholder keys updated on your templates and content items.

I hope it helps you if you face a similar issue.

Happy upgrade!

Sitecore upgrade from 8.2 to 9.3 and the “query” datasources issue

If you are migrating from Sitecore 8.x to 9.3, you are using SXA and “query” datasources you should have a quick look here!

After we finished with our Sitecore upgrade (among with all the other upgrades, like .NET framework, GlassMapper, SXA, etc, more about this soon) and the QA team finished with the fantastic job of regression we were ready for our KPI tests.

One of the most challenging and important KPI for our client is the server performance, for that we usually run load and stress tests on our pre-prod environments, (more about performance testing soon). We started to get the Azure logs flooded with this exception:

AzureSearch Query [sitecore_web_index]: search=This_Is_Equal_ConstNode_Return_Nothing

This was happening on each and every request several times, even with our custom indexes, so of course affecting the performance at server side.

In order to confirm this first assumption, I started by getting a profiling session, I’ve used Azure Diagnostic Tools to collect a profiler trace:

Here we go 🙂 as I expected the hotspot was pointing to the related error message in the logs:

I did a lot of research in the internet and couldn’t find anything that helped me pointing out what was going on. So.. hands to work!

As it was happening on all pages types, so that was my first clue, it had to be on a global component, let’s then why not start with the footer and header page designs?

As I mentioned before, our sites are using SXA and we have some queries and tokens (like $site) as datasources to support muli-site, that was my starting point then:

query datasource

Seems like after the upgrade, something changed and now Sitecore and SXA are conflicting with those tokens when using a query as datasource.

What got changed on Sitecore or SXA?

After a full config comparison between my previous instance and the new 9.3 one, I noticed this new processor has been introduced (GetDatasourceItemByQuery) in the Sitecore.ContenSearch.config file:

So, let’s have a look at this dll and see what is that doing!

Yay! found it! So, it is now making use of content search to resolve the datasource queries. Sounds like a good improvement, but, unfortunately not for my project, as seems like is not expecting to have a SXA token there.

Next steps: I opened a support ticket to Sitecore and SXA confirmed this issue and said they had fixed it in SXA 10.0. I’m still waiting for a hotfix for 9.3 as our client is not upgrading to 10 yet.

After I removed this processor, I ran my performance tests again and it got improved, another profiling session confirmed that the bottleneck was gone.

I hope you find this useful in case you face the same issue, happy upgrade!