Skip to main content


Not just Next.js! Making XM Cloud work with .NET Core Headless Renderings

Rendering Host with Xm Cloud

Next.js is a wonderful framework and paired with its vendor platform (Vercel) it indeed provides exceptional capabilities for building natively headless applications. That unfortunately shadows out another great SDK for headless implementations – .NET Core Renderings, which in my opinion is undervalued. So, I decided to give it some more care by making it work with XM Cloud. Let’s see how it went.

Since you can run XM Coud locally only in docker containers, I decided to make this PoC in the containers entirely. This also gives you the opportunity to clone my ready-to-use solution source code from GitHub and it will just magically work for you.

Running .NET Core SDK

So, in order to start we need first to define our donors. After playing with various repositories of XM Cloud I ended up with xmcloud-foundation-head repo as a decent starterkit. For .NET Core Renderings Headless SDK I used an official scaffolding:

dotnet new -i Sitecore.DevEx.Templates --nuget-source

which installs headless templated including the one I need: sitecore.aspnet.gettingstarted

dotnet new sitecore.aspnet.gettingstarted -n MyProject

MyProject here is a scaffolding name of a .NET rendering site, which I decided to leave untouched for the sake of this PoC.

Before init I adjusted hostnames within Init.ps1 script, replacing all occurrences of myproject.localhost with xmcloudcm.localhost so that will have to do less adjustments further ahead. Thaat is ok as soon as you keep COMPOSE_PROJECT_NAME parameter of these two unique as it will be used for prefixing containers (myproject and starterkit in my case)

Next, run this script:

.\init.ps1 -InitEnv -LicenseXmlPath c:\Projects\license.xml -AdminPassword b -Topology xm1

After initialization, I made minor adjustments to the environmental .env file, most of them as per this blog post. Namely, I changed images to ltsc2022 for work with process isolation, the latest version of Traefic which is 2.9.8, and set MANAGEMENT_SERVICES_IMAGE to 5.1.25 and HEADLESS_SERVICES_IMAGE to 21.0.5, but you obviously don’t have to do any of these, it’s just nice to have.

I also set NET SDK to the latest version: DOTNET_VERSION=7.0 and verified hostnames for xm1 topology are exactly the same as I referenced in Init.ps1 file prior to running it.

I already made a big mess on my dev machine prior to this experiment, so as an optional measure, I did dome sanity exercises:

  • archived the existing certs from the local CA under C:\Users\Martin\AppData\Local\mkcert folder.
  • rebuilt the images by deleting them by prefix (docker rmi $(docker images --format "{{.Repository}}:{{.Tag}}"|findstr "myproject-") command).
  • Adjust hostnames inside of up.ps1 where it does authentication and at the end of the script where it opens URLs in a browser (this script is incapable of taking these values from the environment).
  • also adjusted Traefic config to reference certificates for adjusted hostname docker\traefik\config\dynamic\certs_config.yaml

Eventually, I run .\Up.ps1 script to build and run the containers and confirm the donor template works in principle. Once containers spun up, I made sure everything shows up in Sitecore as expected, including headless site.

Installed On Cm

and also that both Experience Editor and Rendering Host on their own work well

Rendering Host

So far so good. Now I need to “export” these features to XM Cloud containers.

Target XM Cloud system

XM Cloud ships with images for local development, so it was only a matter of which one to choose from. I quickly looked at Awesome Sitecore projects to see and evaluate the description of four XM Cloud repositories available. I choose XM Cloud Starter Kit as the more recent, relevant, and proven XM Cloud starter kit.

Unfortunately, ltsc2022 images are not supported here therefore I have to progress with hyperv isolation mode.

So now I need to copy .NET Rendering section of docker-compose to my XM Cloud container. Out of the box, Next.js host is coming out with the same name which is rendering. Since I don’t need it any longer, it is OK to delete the files and replace its section of docker-compose.yml files with .NET one. Since the structure of both solutions is different (headless starter kit has docker-compose isolated in their own folders one per topology), I need to adjust the paths, and here’s what I ended with:

  image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-rendering:${VERSION:-latest}
    context: ./docker/build/rendering
    - .\:C:\solution
    ASPNETCORE_URLS: "http://*:80"
    # These values add to/override ASP.NET Core configuration values.
    # See rendering host Startup for details.
    LayoutService__Handler__Uri: "http://rendering/sitecore/api/layout/render/jss"
    Analytics__SitecoreInstanceUri: "http://rendering"
    - solution
    - cm
    - "traefik.enable=true"
    - "traefik.http.routers.rendering-secure.entrypoints=websecure"
    - "traefik.http.routers.rendering-secure.rule=Host(`${RENDERING_HOST}`)"
    - "traefik.http.routers.rendering-secure.tls=true"

Trying to run with the above will still error out, as there are unresolved dependencies. Let’s add them both – dotnetsdk and solution:

  image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-dotnetsdk:${VERSION:-latest}
    context: ./docker/build/dotnetsdk
  scale: 0

# The solution build image is added here so it can be referenced as a build dependency
# for the images which use its output. Setting "scale: 0" means docker-compose will not
# include it in the running environment. See Dockerfile for more details.
  image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-solution:${VERSION:-latest}
    context: ./
    - dotnetsdk
  scale: 0

Please pay attention to the interesting instruction scale: 0 – that prevents showing these containers but when you may want to troubleshoot one of these with Docker Desktop for example, just comment out this line. You can also use that for hiding Init containers so that your cluster looks nicer.

Root-level directory files copying

I know, it is not the best to structure files in that way, but for the sake of this PoC I decided to leave them as they were initially in order to have fewer changes in Dockerfiles. Let’s do some copying:

  1. Copy Packages.props into the root of a target solution, then need to adjust it for the .NET Rendering project:
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="">
    <PackageReference Update="Sitecore.XmCloud.Kernel" Version="$(PlatformVersion)" />
    <PackageReference Update="Sitecore.XmCloud.ContentSearch" Version="$(PlatformVersion)" />
    <PackageReference Update="Sitecore.XmCloud.ContentSearch.Linq" Version="$(PlatformVersion)" />
    <PackageReference Update="Sitecore.XmCloud.LayoutService" Version="$(PlatformVersion)" />
    <PackageReference Update="Sitecore.XmCloud.Assemblies" Version="$(PlatformVersion)" />
    <PackageReference Update="Sitecore.Assemblies.SitecoreHeadlessServicesServer" Version="19.*" />

    <PackageReference Update="Sitecore.AspNet.ExperienceEditor" Version="$(SitecoreAspNetVersion)" />
    <PackageReference Update="Sitecore.AspNet.Tracking" Version="$(SitecoreAspNetVersion)" />
    <PackageReference Update="Sitecore.AspNet.Tracking.VisitorIdentification" Version="$(SitecoreAspNetVersion)" />
    <PackageReference Update="Sitecore.LayoutService.Client" Version="$(SitecoreAspNetVersion)" />
    <PackageReference Update="Sitecore.AspNet.RenderingEngine" Version="$(SitecoreAspNetVersion)" />
    <PackageReference Update="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="$(Net6x)" />
    <PackageReference Update="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(Net6x)" />
    <PackageReference Update="Microsoft.Extensions.Http" Version="$(Net6x)" />
    <PackageReference Update="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="$(Net6x)" />

Next, copy Dockerfile which contains build instructions for the solution having both projects – .NET Framework with deployable configuration and .NET (Core) Rendering application.


This goes simply – copy src\items folder along with src\Items.module.json configuration, naming does not matter here as CLI Serialization plugin operates by a wildcard, so we can leave names as they are:

"modules": [

Rendering Application

Next, we need to add Rendering project to the solution. The easiest was doing that with Visual Studio, which also helps compile and test it against the target endpoint, once it runs. The key point here is to ensure the Rendering project gets referenced by the relative path, otherwise it docker won’t correctly build it. The correct referenced path should look as: "src\platform\Platform.csproj".

The main configuration file to modify is appsettings.json – it contains endpoints for both Layout Service and Experience Editor, including API key and the rest of the required information for the connection. Apart from adjusting the hostnames there was the incompatibility of the endpoint, as XM Cloud did not work with /api/layout/render/jss but worked well with /api/layout/render endpoint instead. Here’s what I got at the end:

  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
  "AllowedHosts": "*",
  "LayoutService": {
    "Handler": {
      "Name": "jss-endpoint",
      "Uri": "https://xmcloudcm.localhost/sitecore/api/layout/render",
      "RequestDefaults": {
        "sc_apikey": "{dba6453f-6a71-4de9-b9c3-36ea77fcc730}",
        "sc_site": "MyProject"
  "RenderingEngine": {
    "ExperienceEditor": {
      "Endpoint": "/jss-render"
  "JSS_EDITING_SECRET": "PlaceholderForEditingSecret",
  "Analytics": {
    "SitecoreInstanceUri": "https://xmcloudcm.localhost/"

Traefik and SSL certificates

If you don’t want seeing were exceptions like “The SSL connection could not be established, see inner exception“, don’t miss this step.

Traefik operates file-based pairs of certificates, those you generate with mkcert.exe tool typically from within .\Init.ps1 script. These files come into docker\traefik\certs folder which is a mapped volume into a Traefik container to be used there. Traefik knows which certificates to use from its configuration file located at  docker\traefik\config\dynamic\certs_config.yaml. Please note that the paths are local to the Traefic container as the folder gets mapped:

    - certFile: C:\etc\traefik\certs\_wildcard.xmcloudcm.localhost.pem
      keyFile: C:\etc\traefik\certs\_wildcard.xmcloudcm.localhost-key.pem
    - certFile: C:\etc\traefik\certs\xmcloudcm.localhost.pem
      keyFile: C:\etc\traefik\certs\xmcloudcm.localhost-key.pem

The above example sets two pairs of certificates: one for xmcloudcm.localhost domain and another is a wildcard pair of certs for its third-level subdomains *.xmcloudcm.localhost. I just copied the wildcard pair from a donor

Other important bits

Take a look at the deployable configuration from this file src\platform\App_Config\Include\MyProject.config. Apart from adjusting the hostnames here, you can look at this clause: serverSideRenderingEngineApplicationUrl="$(env:RENDERING_HOST_PUBLIC_URI)" which references a missing environmental variable, so let’s add the one to docker-compose.override.yml as well:

  image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-xmcloud-cm:${VERSION:-latest}
    context: ./docker/build/cm
    - ${LOCAL_DEPLOY_PATH}\platform:C:\deploy
    - ${LOCAL_DATA_PATH}\cm:C:\inetpub\wwwroot\App_Data\logs
    - ${HOST_LICENSE_FOLDER}:c:\license
    SITECORE_LICENSE_LOCATION: c:\license\license.xml
    RENDERING_HOST_INTERNAL_URI: "http://rendering:3000"
    JSS_DEPLOYMENT_SECRET_xmcloudpreview: ${JSS_DEPLOYMENT_SECRET_xmcloudpreview}
    SITECORE_Pages_Client_Host: ${SITECORE_Pages_Client_Host}
    SITECORE_Pages_CORS_Allowed_Origins: ${SITECORE_Pages_CORS_Allowed_Origins}
    ## Development Environment Optimizations
    SITECORE_DEVELOPMENT_PATCHES: DevEnvOn,CustomErrorsOff,DebugOn,DiagnosticsOff,InitMessagesOff
    Sitecore_AppSettings_exmEnabled:define: "no" # remove to turn on EXM
    # maps rendering host hostaname to a serverSideRenderingEngineApplicationUrl="" attribute jss app 
  entrypoint: powershell -Command "& C:/tools/entrypoints/iis/Development.ps1"

Deploy configuration

It must be run automatically, for in order to be double sure, run Visual Studio and use Publish command against Platform projects. It will suggest you publish configs into a mapped volume folder of your CM instance, similarly as in the screenshot below:

Deploy Configs
Run it!

Once we complete merging the code, let’s run it again and test what we’ve got, by running .\up.ps1 script. It will take some time for building new containers which

A little after you confirm XM Cloud device user code token, you’ll see lots of green lines for populating Solr schema and rebuilding the indexes, later showing the serialization worked well bringing items for a new MyProjects site, its templates, along with the rest of the serialized dependencies.

Serialization Done

After XM Cloud is up and running, let’s try it in Experience Editor:

Experience Editor

It looks and works well.

Troubleshooting tips

  • If you receive a certificate error – check which certificate is coming along with a page. If that says TRAEFIK DEFAULT CERT that likely means Traefik configuration file does not reference the correct certs, check at docker\traefik\config\dynamic\certs_config.yaml
  • If you receive any msbuild errors check the solution in Visual Studio and make sure everything is referenced and builds well. Also manually verify the relative paths for the projects within *.sln file, as I explained above.
  • A Sitecore.JavaScriptServices application was not found for the path /sitecore/content/MyProject/home” error means you just did not deploy the configuration. You can confirm that from ShowConfg.aspx not showing a site named “MyProject” which will appear after you deploy the configuration.
  • There might be many unforeseen errors due to the path’s misconfiguration, so look carefully at the error messages. If there are issues with come containers – get into them and watch logs. Use container console to curl local resources to sure it is up and tunning

An example of an unforeseen issue

Drawbacks of this approach

First and most, unlike it works with Next.js – there is no built-in rendering host for .NET Headless Rendering coming with XM Cloud – and you have to make it with your own effort. Not to say, lack of Vercel OOB support with tons of useful features.

The second issue for the moment is that .NET Headless Renderings do not have support for GraphQL, unlike Next.js SDK and that is a shame. Lots of current Sitecore professionals are coming from .NET generic background, so we’d like to see Sitecore giving us some more care by supporting .NET Renderings with their latest and greatest from the new composable world.

Thoughts on “Not just Next.js! Making XM Cloud work with .NET Core Headless Renderings”

  1. Martin Miles Post author

    Isn’t having a choice a good thing?
    Not everyone go to Vercel, and that is exactly what my message between the lines is about

  2. Unfortunely the vibes I get from Sitecore marketing material is that .NET is kind of stagnant, it won’t be removed, but they also don’t seem to care that much.

    It is also visible on the rest of the ecosystem, since most of the new acquisitions are using other tech stacks, with exception of Order Cloud.

    Even the way developer advocates telling us that with NextJS we can now use Mac OS or Linux, well we would also be able to do the same with .NET, if Sitecore would be bothered to actually migrate XP/XM from .NET Framework into .NET Core.

  3. Martin Miles Post author

    It is all about wording, and wording sometimes could be misleading. I would agree on .NET Renderings should get some updates into its SDK, but from the rest- it is fully operable thing with XM Cloud. Of course, you need to set up separate rendering/editing host as build-in only works wit JSS apps, but that is not a rocket science. There are know examples of .NET Renderings work with XM Cloud, take MVP site for example.
    So again, reading between the lines, especially written by some marketers, would definitely help out

  4. While doable, your article is after all a proof of it, I have been long enough on this industry to understand the signs of where things are going.

    The way Sitecore has been silent on .NET support roadmap, hardly moving anything into modern .NET, and that we without any kind of Sitecore support, have to do the manual integration, rocket science or not, shows where they are heading.

    So many times has a product been kept alive only because the community doesn’t give up doing what the company themselves won’t do, until the comunity diminishes to the point that they cannot any longer keep up, and that is it.

    So I guess the question is, does the .NET community, the one that helped Sitecore gain its market share among CMS developers, still matter to Sitecore, or we are now all composable and thanks for nothing.

  5. You’re right in what you’re saying, Paulo. And I thinks it is a matter of numbers for Sitecore – old Sitecore community counts ~30K of people ALLTOGETHER. All types of professionals. While Next.js community alone counts times more. You cannot do everything at once, so the idea came up was to switch to the wider audience. But what should we do with the existing community?

  6. Acknowledge that the existing community does exist, and nurture it, otherwise those ~30K might decide to go elsewhere instead of learning JavaScript/TypeScript and Nextjs.

  7. Agree with you again, and will refer to STEVE TZIKAKIS at SUGCON just 5 days ago: “…set out his ambitious target of going from 40,000 Sitecore developers to 100,000 in the market”. So, even there are 40K of existing professionals “replaced” with 100K of generic node/next developers, that gives up a positive balance of 60K developers. Sad, but true.

  8. Thanks for the blog post Martin.

    Re: discussion here… I see this as less of trying to grow the developer base and more of chasing trends. Still have to sell the software to non-Microsoft shops to get it into their hands. Existing companies using .Net developers will continue to stay in their lane and find budget challenges moving and rewriting current installations over to XMC. Take it for what it is; they push Vercel, knowing they get a kickback on each signup.

    I do agree that Sitecore is smart to grow the “potential” customer base in any way they can.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Martin Miles

Martin is a Sitecore Expert and .NET technical solution architect involved in producing enterprise web and mobile applications, with 20 years of overall commercial development experience. Since 2010 working exclusively with Sitecore as a digital platform. With excellent knowledge of XP, XC, and SaaS / Cloud offerings from Sitecore, he participated in more than 20 successful implementations, producing user-friendly and maintainable systems for clients. Martin is a prolific member of the Sitecore community. He is the author and creator of the Sitecore Link project and one of the best tools for automating Sitecore development and maintenance - Sifon. He is also the founder of the Sitecore Discussion Club and, co-organizer of the Los Angeles Sitecore user group, creator of the Sitecore Telegram channel that has brought the best insight from the Sitecore world since late 2017.

More from this Author

Follow Us