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:Projectslicense.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:UsersMartinAppDataLocalmkcert 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 dockertraefikconfigdynamiccerts_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 srcitems folder along with srcItems.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: “srcplatformPlatform.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 dockertraefikcerts 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 dockertraefikconfigdynamiccerts_config.yaml. Please note that the paths are local to the Traefic container as the folder gets mapped:
tls:
certificates:
– certFile: C:etctraefikcerts_wildcard.xmcloudcm.localhost.pem
keyFile: C:etctraefikcerts_wildcard.xmcloudcm.localhost-key.pem
– certFile: C:etctraefikcertsxmcloudcm.localhost.pem
keyFile: C:etctraefikcertsxmcloudcm.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 srcplatformApp_ConfigIncludeMyProject.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:inetpubwwwrootApp_Datalogs
– ${HOST_LICENSE_FOLDER}:c:license
environment:
SITECORE_LICENSE_LOCATION: c:licenselicense.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 dockertraefikconfigdynamiccerts_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.
Leave A Comment