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 https://sitecore.myget.org/F/sc-packages/api/v3/index.json
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.
and also that both Experience Editor and Rendering Host on their own work well
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:
rendering: image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-rendering:${VERSION:-latest} build: context: ./docker/build/rendering target: ${BUILD_CONFIGURATION} args: DEBUG_PARENT_IMAGE: ${REGISTRY}${COMPOSE_PROJECT_NAME}-dotnetsdk:${VERSION:-latest} SOLUTION_IMAGE: ${REGISTRY}${COMPOSE_PROJECT_NAME}-solution:${VERSION:-latest} volumes: - .\:C:\solution environment: ASPNETCORE_ENVIRONMENT: "Development" 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" JSS_EDITING_SECRET: ${JSS_EDITING_SECRET} depends_on: - solution - cm labels: - "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:
dotnetsdk: image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-dotnetsdk:${VERSION:-latest} build: context: ./docker/build/dotnetsdk args: DOTNET_VERSION: ${DOTNET_VERSION} 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. solution: image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-solution:${VERSION:-latest} build: context: ./ args: BUILD_CONFIGURATION: ${BUILD_CONFIGURATION} BUILD_IMAGE: ${REGISTRY}${COMPOSE_PROJECT_NAME}-dotnetsdk:${VERSION:-latest} depends_on: - 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:
- 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="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Net6x>7.0-*</Net6x> <PlatformVersion>1.*</PlatformVersion> <SitecoreAspNetVersion>21.0.*</SitecoreAspNetVersion> </PropertyGroup> <ItemGroup> <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)" /> </ItemGroup> </Project>
Next, copy Dockerfile
which contains build instructions for the solution having both projects – .NET Framework with deployable configuration and .NET (Core) Rendering application.
Serialization
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": [ "src/*.module.json" ],
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:
tls: certificates: - 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:
cm: image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-xmcloud-cm:${VERSION:-latest} build: context: ./docker/build/cm args: PARENT_IMAGE: ${SITECORE_DOCKER_REGISTRY}sitecore-xmcloud-cm:${SITECORE_VERSION} TOOLS_IMAGE: ${TOOLS_IMAGE} volumes: - ${LOCAL_DEPLOY_PATH}\platform:C:\deploy - ${LOCAL_DATA_PATH}\cm:C:\inetpub\wwwroot\App_Data\logs - ${HOST_LICENSE_FOLDER}:c:\license environment: SITECORE_LICENSE_LOCATION: c:\license\license.xml RENDERING_HOST_INTERNAL_URI: "http://rendering:3000" JSS_DEPLOYMENT_SECRET_xmcloudpreview: ${JSS_DEPLOYMENT_SECRET_xmcloudpreview} SITECORE_JSS_EDITING_SECRET: ${JSS_EDITING_SECRET} SITECORE_EDITING_HOST_PUBLIC_HOST: "${RENDERING_HOST}" 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 RENDERING_HOST_PUBLIC_URI: "https://${RENDERING_HOST}" 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:
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.
After XM Cloud is up and running, let’s try it in 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
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.
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
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.
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
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.
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?
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.
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.
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.