Experience Sitecore ! | January 2025

Experience Sitecore !

More than 200 articles about the best DXP by Martin Miles

All you need to know about transforming Web.config on Sitecore XM Cloud

In Sitecore XM Cloud, one cannot modify web.config on the CM instance at runtime. By design, the CM webroot in XM Cloud containers is only writable by the deployment process, so "live" edits to web.config aren’t possible without redeploy. You can patch anything under App_Config/Include at runtime, by using Sitecore PowerShell Extensions, for example, but the main web.config file sits outside that folder, namely exactly at the web root, and requires stricter permissions. Only the Deploy process can modify it.

In this blog post, I am going to share all the techniques you can undertake to get your changes reflected within web.config on your desired environment.

Why?

Firstly, why at all would one need to modify web.config on the XM Cloud CM?

Transforming the CM instance’s web.config is essential because it’s the only way to inject critical, environment-specific settings, like Content Security Policy headers, custom session timeouts, IIS rewrite rules, or extra connection strings right into a locked-down XM Cloud deployment. Since the cloud platform prohibits direct edits to web.config at runtime, using XDT transforms ensures that everything from security hardening (CSP, HSTS) to feature flags or environment variables is baked into the build pipeline in a controlled, auditable way. This same transform can then be reapplied locally so your local CM containers mirror exactly what runs in production, reducing drift and making deployments predictable and secure. But..

How?

Since CM executes technically on an ASP.NET Framework runtime, an old good technique called XDT transformation, known from the old good days of ASP.NET, is still there with us. Iа you have never done it before, transformation may appear slightly complicated to produce it at first, but reading an XDT file is very intuitive. Here is an example:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.web>
    <customErrors mode="Off" xdt:Transform="SetAttributes"/>
	<!--<customErrors mode="Off" xdt:Transform="SetAttributes" xdt:Locator="Condition(@mode!='Off')"/>-->
  </system.web>
  <appSettings>
    <add key="Some_New_Key" value="value_to_insert" xdt:Transform="InsertIfMissing" xdt:Locator="Match(key)" />
  </appSettings>
  <location path="sitecore">
	<system.webServer>
		<httpProtocol>
			<customHeaders>
				<add name="Content-Security-Policy" value="default-src 'self' 'unsafe-inline' 'unsafe-eval' [https://apps.sitecore.net](https://apps.sitecore.net/); img-src 'self' data: https://demo.sitecoresandbox.cloud/ [https://s.gravatar.com](https://s.gravatar.com/) https://*.wp.com/cdn.auth0.com/avatars; style-src 'self' 'unsafe-inline' [https://fonts.googleapis.com](https://fonts.googleapis.com/); font-src 'self' 'unsafe-inline' [https://fonts.gstatic.com](https://fonts.gstatic.com/); block-all-mixed-content; child-src 'self' https://demo.sitecoresandbox.cloud/; connect-src 'self' https://demo.sitecoresandbox.cloud/; media-src https://demo.sitecoresandbox.cloud/" xdt:Transform="Replace" xdt:Locator="Match(name)"/>
			</customHeaders>
		</httpProtocol>
	</system.webServer>
  </location>
</configuration>

Cloud build-time XDT transform: officially recommended approach

For the cloud deployments, you can leverage the transforms section of xmcloud.build.json to apply an XDT patch at build time:

"transforms": [
  {
    "xdtPath": "/xdts/web.config.xdt",
    "targetPath": "/web.config"
  }
]

The deploy process will do the rest, and it has everything required to apply the specified transform against the provided web.config. Please note that xdtPath is relevant to the CM customization .NET Framework project, also a cloud redeployment is mandatory for changes to take effect

This build-time transform is cloud-compatible with no custom images needed, and centralizes the change in a single file. It’s also the officially documented method for altering web.config in XM Cloud.

Pros: Officially supported, one versioned transform file, applied automatically by the pipeline.

Cons: Changes take effect only on redeploy, which is typically normal in XM Cloud.

So far, so good. But the above officially recommended approach only works with cloud deployments. What should we do to transform configs on a local development docker-run containers?

Local XDT Transformation

The first thing that probably came to your mind would be to create a custom CM image derived from the official XM Cloud image provided by Sitecore. However, you are not allowed to deploy any custom images for CM, due to the safety railguards. Anyway, even if it were possible, this idea would generally be overkill. Instead, for local XM Cloud Docker development, we generally want to somehow mirror the cloud approach but without custom images.

There are two main options:

1. Dockerfile build-time transform

Luckily, Sitecore supplies us with a helpful Docker tools image for XM Cloud, officially named as scr.sitecore.com/tools/sitecore-xmcloud-docker-tools-assets, that contains an Invoke-XdtTransform.ps1 PowerShell script to perform exactly what we need.

In the docker/build/cm directory, along with Dockerfile, create a new folder xdts and copy the desired XDT into it. Next, let's add copying and the execution instructions to the docker/build/cm/Dockerfile itself:

COPY ./xdts C:\inetpub\wwwroot\xdts

RUN (Get-ChildItem -Path 'C:\\inetpub\\wwwroot\\xdts\\web*.xdt' -Recurse ) | `
    ForEach-Object { & 'C:\\tools\\scripts\\Invoke-XdtTransform.ps1' -Path 'C:\\inetpub\\wwwroot\\web.config' -XdtPath $_.FullName `
    -XdtDllPath 'C:\\tools\\bin\\Microsoft.Web.XmlTransform.dll'; };

This will process all the transforms and bake the result into the CM image. After rebuilding the container with docker-compose build, the web.config receives our changes.

Pros: Uses the same XDT logic as XM Cloud; no extra runtime steps.

Cons: Requires rebuilding the Docker image for every change, which results in slower iterative development, and is not cloud-compatible. As I said above, you can’t push a custom CM image to XM Cloud, and you generally don't need that because xmcloud.build.json takes care of that anyway. The only real negative here is that you violate the DRY principle because you have the XDT file duplicated.

2. Development-only runtime patches

As we know, one cannot create custom CM images, but nothing stops us from creating our own custom tools image! What for? Your CM image copies the tools folder from the Sitecore XM Cloud Docker Tools Assets image into the tools folder, and this folder contains out-of-the-box development-only XDT configuration transforms in a folder called dev-patches and also the entrypoint for the CM image. This folder contains some default config patches provided by Sitecore out-of-the-box:

Therefore, our goal is to reuse this image by creating our own, where we will add our own XDT transform folder with actual files inside if these folders. Because we expect to reuse the execution script as well, it is important to maintain the same folder/file structure as on the original image. In this case, our changes will get picked up and processed automatically.

Steps to achieve:

1. First of all, we need to create a folder for a custom XDT transformation. Let's call it YouCustomXdtFolder and create Web.config.xdt file inside it. Naming convention is important here: transform is always called Web.config.xdt and the folder name will be later used on stage 5 to reference this transformation.

2. Create a custom tools image. Create a Dockerfile file under docker/build/tools, which you also created:

# escape=`

ARG BASE_IMAGE

FROM ${BASE_IMAGE}

COPY dev-patches\ \tools\dev-patches\

3. Build out tools image. In the docker-compose.override.yml let's add a new record under services:

  tools:
    image: ${REGISTRY}${COMPOSE_PROJECT_NAME}-sitecore-xmcloud-docker-tools-assets:${VERSION:-latest}
    build:
      context: ./docker/build/tools
      args:
        BASE_IMAGE: ${SITECORE_TOOLS_REGISTRY}sitecore-xmcloud-docker-tools-assets:${TOOLS_VERSION}
    scale: 0

4. Instruct CM to use the custom tools image rather than the default one:

services:
  cm:
    build:
      args:
        TOOLS_IMAGE: ${REGISTRY}${COMPOSE_PROJECT_NAME}-sitecore-xmcloud-docker-tools-assets:${VERSION:-latest}
    depends_on:
      - tools
    environment:
      SITECORE_DEVELOPMENT_PATCHES: ${SITECORE_DEVELOPMENT_PATCHES}

5. Append the name(s) of the custom transform folder to the environmental variable, for example:

SITECORE_DEVELOPMENT_PATCHES: DevEnvOn,CustomErrorsOff,DebugOn,DiagnosticsOff,InitMessagesOff,YouCustomXdtFolder

These five steps will do the entire magic on your local CM!

Pros: No need to rebuild the image – just restart the container when the XDT changes. Uses the official Docker entrypoint logic.

Cons: It only affects your local dev environment (you must still use xmcloud.build.json for cloud). It also requires maintaining the environment variable, but that can be version-controlled.

Volume Overwrites

Avoid this approach!

It is based on mounting or copying entire config folders like App_Config or web.config directly via /docker/deploy mounting point folder, and is absolutely not recommended!

That approach is error-prone and hard to maintain - you risk overwriting updates, missing subtle changes, aтв potentially receive false positives which may later hurt you badly. Instead, try to use either Dockerfile build-time transform or Development-only runtime patches approaches wherever possible.

You can only use the Volume Overwrites approach for experimental and time-critical cases for a one-off proving a concept, without any intent of keeping these changes. If your concept appears to be successful, consider using one of the above methods for local, along with reflecting the changes in xmcloud.build.json for the cloud deployment.

Summary Comparison of Approaches

Approach Cloud-Compatible Local Support Build/Rebuild Needed Maintenance Effort Notes
XM Cloud xmcloud.build.json XDT ✅ (only way) (cloud-only) N/A (cloud build) Low – one XDT file Official method for XM Cloud builds.
Dockerfile XDT (build-time) Yes (rebuild image) Medium – Dockerfile edits Works exactly like cloud transform (same XDT logic). Not usable in the cloud.
Dev-only patches (runtime) ❌ (dev only) No (just restart) Low – simple patch & env

Uses SITECORE_DEVELOPMENT_PATCHES​.

Quick turnaround; no custom image.

Volume/config override No (instant) High-fragile/sync issues Not recommended – mass copies of folders are “ugly” and error-prone.


  • Build Speed: The dev-only approach avoids image rebuilds, which brings fast feedback, whereas the Dockerfile method requires rebuilding the CM image after changes and is slower, especially upon each change. XM Cloud transforms only run on deployment builds.

  • Maintenance: Keeping one XDT file in source control for both cloud and local is easiest. The Dockerfile method scatters transform logic into build scripts (higher maintenance). The dev-only patch centralizes it with environment configuration.

  • Error-Proneness: Transform files are declarative and less error-prone than manual file swaps. Volume mounts risk configuration drift. The built-in dev-patches and XM Cloud pipeline both use the official transform engine, which is robust.


Conclusion and recommendations

First and obvious: use XDT transforms wherever possible! Even in those rare occasions when you can modify web.config manually, it does not mean that you should!

For cloud deployments, always use xmcloud.build.json transforms to modify web.config. In local Docker, mirror the same transform logic. The preferred local method is to leverage the SITECORE_DEVELOPMENT_PATCHES mechanism: place the same Web.config.xdt under docker/build/tools/YourPatchName/ and add YourPatchName to the environment variable. This requires no Dockerfile hacking and no custom CM image, yet applies the transform at runtime using the same Microsoft.Web.XmlTransform script.

As a fallback or if needed, you can also inject a RUN Invoke-XdtTransform.ps1 step into the CM Dockerfile​, but this is more effort and not supported on XM Cloud. In all cases, avoid manual folder copies or replacing the entire config.

The transform-based approaches (build-time for cloud, and/or the dev-patch for local) strikes the best balance of simplicity, performance, and future maintainability​ and represents the current best practices.

Rendering Parameters vs. Rendering Variants - when should use one or another

Do you know how to identify when you should create a rendering variant for a component, and when you can simplify effort by setting rendering parameters? Below is the answer and it’s pretty straightforward.

To address let's first take a look at both options and options and identify their key differences.

Rendering Parameters allow you to have additional control over a component/rendering by passing additional parameters into it. Key-value-pair is the most simplistic form, but of course, you can use any advanced form of input by leveraging rendering parameters templates, but regardless of the chosen way the result will be the same - you pass some additional parameters into a component. Based on those params a component can do certain things, for example, show/hide specific blocks or use more advanced styling tricks. Important to keep in mind - that all the parameters are stored within a holding page. Remember that you should inherit Base Rendering Parameters template to have full support in Pages Builder.

parameters


Rendering Variants (aka. Headless Variants) feel more advanced compared to params. The principle difference is that a variant allows you to return principally different HTML output and do way more complicated manipulations over the HTML structure. You should use common sense when choosing variants and leverage them in cases where the same component may present various look and feel options: for example, a promo block with two images having a headless variant of these same images positionally swapped. Achieving the same with rendering parameters would require bringing ugly presentation logic into the components code along with code duplications. Using variants allows us to achieve the same result way more elegantly. Note that, Variants originate from SXA, therefore when you bring a legacy JSS site to XM Cloud without converting it to SXA - this option isn't available.

variants



Both Rendering Variants and Rendering Parameters assume you use the same component that receives the same datasource items (or none datasource at all). You should never leverage datasource items to control the presentation or behavior of components - they are purposed exclusively for storing the content, as it comes from their name.

Hope that clarifies the use cases and removes ambiguity.

Experience Edge: Know Your Limitations

Experience Edge brought us that much-desired Content-Delivery-as-a-Service approach and happened to be revolutionary in its vision. However, that flexibility of service comes at some expense, and the limitations each of us must be aware of. Understanding these is critical when building cloud-hosted Sitecore solutions. The key technical limits include API rate throttling, data payload/query size caps, content/media size limits, caching rules, and XM Cloud platform constraints. In this post, I will cover them all, so that it can help you plan better.

API Rate Limits

  • 80 requests/sec. The Experience Edge GraphQL endpoint is rate-limited. Each tenant’s delivery API allows at most 80 requests per second (visible as X-Rate-Limit-Limit: 80). Exceeding this returns HTTP 429 (Too Many Requests) until the 1-second window resets. In practice, Sitecore notes this is a "fair use" cap on uncached requests, so designing with CDN caching via SSG/ISR is essential to stay below the limit.

  • Rate-limit headers. Every Edge response includes headers like X-Rate-Limit-Remaining calls this second, and X-Rate-Limit-Reset essentially - the time until reset, to help clients throttle their calls. For example, if 5 requests are made in one second, the next response will show 75 remaining.

GraphQL Query & Payload Constraints

  • Max query results: A single GraphQL query returns at most 1,000 items/entities. To fetch more items, you must use cursor-based pagination. For example, any search or multi-item query is capped at 1000 results per call.

  • Query complexity limit: Edge enforces a complexity budget on GraphQL queries. Very large or deeply nested queries can fail if they exceed the complexity threshold (around 250 in older Sitecore docs). Developers should test complex queries and consider splitting them or trimming fields.

  • No persisted or mixed queries: Experience Edge does not support persisted queries. Also, due to a known schema issue, you cannot mix literal values and GraphQL variables in one query; you must use all variables if any are used. Not knowing this rule cost me once a decent amount of time for troubleshooting.

  • Payload request size: Very large GraphQL request payloads can be problematic. By default, Next.js APIs have a 2 MB body size limit, which can cause 413 Payload Too Large errors when submitting huge queries. Sitecore suggests raising this (say, to ~5 MB) if necessary. In practice, keep queries reasonably small to avoid frontend limits.

  • Include/Exclude paths: When querying site routes (siteInfo.routes), the combined number of paths in includedPaths + excludedPaths is limited to 100. This caps how many different route filters you can specify in one request.

Content & Delivery Constraints

  • Static snapshot only: Experience Edge provides a static snapshot of published content. It does not apply personalization, AB testing, or any dynamic/contextual logic at request time. Any logic based on user, session, or query string must be handled client-side. If you change a layout service extension or rendering configuration, you must republish the affected items for Edge to pick up the changes.

  • Security model: Edge does not enforce Sitecore item-level security. All published content on Edge is effectively public, so use publishing restrictions in the CMS to prevent sensitive items from being published.

  • Single content scope: An Edge tenant covers the entire XM Cloud tenant with a single content scope. You cannot scope queries, cache clears, or webhooks to a specific site. For example, when a cache clear or webhook trigger runs, it applies to the whole tenant’s content, not per site.

  • Sites per tenant: Edge supports up to 1,000 sites per tenant. A "site" in this context is a logical group defined by includedPaths/excludedPaths in siteInfo. You cannot define more than 1000 sites in one Edge environment. In practice, the maximum site I met was 300 per tenant and all those were served by a multisite add-on on a Next.Js front-end.

  • Multi-site rules: You cannot have two different site definitions pointing to the same start item on Edge. Also, virtual folders and item aliases are not supported on Edge. Content must be published in standard items, and all routes are resolved case-sensitively.

  • Locales and device layers: Culture locale codes in queries are case-sensitive (e.g. it-ITit-it). In the layout data delivered by Edge, only the Default device layer is supported in Presentation data, so multi-device renderings beyond “Default” aren’t included.

Media Limits

  • Max media item size: Each media item file size published to Edge is limited to 50MB. Larger media will not be published to Edge; such large assets should be handled via other services like Sitecore Content Hub, or you can self-host them at any preferred blob storage of choice.

  • Media URL parameters: The built-in Media CDN on Edge supports only the parameters w, h, mw, and mh for image resizing. No other image transformations, like quality or format changes, are yet available out-of-the-box.

  • Case-sensitive URLs: Media item URLs on Edge are case-sensitive. For example, if the item path is Images/Banners/promo-banner.jpg, using lowercase images/banners/promo-banner.jpg will end up with 404. This quirk has caused issues in practice, so be careful with link manager settings that change casing.

  • Delivery: Media is delivered via the same CDN cache as content. There is no per-request payload aggregation for media; each media URL is fetched independently (subject to the CDN and TTL rules below).

Caching Rules & TTL

  • Default TTL: By default Edge caches content and media for 4 hours each (see contentCacheTtl: "04:00:00" and mediaCacheTtl: "04:00:00"). This means cached responses may be served up to 4 hours old unless cleared.

  • Auto-clear: Content and media caches are auto-cleared by default (the contentCacheAutoClear and mediaCacheAutoClear settings are true). In practice, this means a publish or explicit clear will purge the CDN cache so users see new content.

  • Custom TTL: You can adjust the cache TTLs via the Edge Admin API. TTL values are strings in D.HH:MM:SS format. For example, setting contentCacheTtl to "720.00:00:00" yields a 720-day TTL, or "00:15:00" for 15 minutes. The default 4h can thus be increased or decreased per project needs.

  • Cache clearing: In addition to auto-clear on publish, Edge offers Admin API endpoints to clear the cache or delete content. For instance, you can clear all content or specific items via the API. To use these features, administrators must obtain appropriate Edge API credentials in XM Cloud Deploy.

XM Cloud Platform Limits (Impacting Edge)

  • Environment mapping: In XM Cloud, the best practice is a 1:1 mapping of XM environments to Edge tenants. In other words, each XM Cloud environment typically has its own Experience Edge deployment. This means content and API keys are not shared across environments by default.

  • Search index: XM Cloud uses Solr, and there is no option to plug in different search technologies for Edge indexing. The connector will only work with Solr indices configured in XM Cloud.

  • Admin credentials: XM Cloud Deploy limits the number of Experience Edge Admin API credentials per project to 10. Attempts to create more will fail with an error. Project administrators should plan credential usage accordingly, for example, one per dev/CD pipeline.

  • Snapshot publishing: To enable incremental updates, XM Cloud provides snapshot publishing. This ensures that as soon as an item is published, Edge content is updated without a full site rebuild. If snapshot publishing is not enabled, any content changes on Edge require full republishing of affected sites. Developers must enable the Snapshot Publishing feature in XM Cloud to avoid hitting the rate limit on builds.

Baseв on all the above, let's also think about some deployment & publishing considerations that may affect your project:

  • Static build (SSG) preferred: Since every uncached request to Edge counts toward the rate limit, Microsoft recommends using Static Site Generation (SSG) and Incremental Static Regeneration (ISR) on the frontend. With SSG, pages are built at deploy-time and served from the host cache, minimizing live queries to Edge.

  • Build-time pagination: Very large sites can take a long time to generate. The default sitemap plugin fetches all pages across all sites; projects should use included/excluded paths to limit build-time queries. Otherwise, large volumes of pages hitting Edge during a build can approach the rate limit.

  • Publish-time republishing: Because Edge content is static, certain backend changes require republishing. In particular, changes to clones, standard values, or rendering/template configurations won’t reflect on Edge until the dependent items are republished. Plan your release process to include republishes after such changes.

Hope knowing the above helps you plan better!