Experience Sitecore ! | All posts tagged 'XM Cloud'

Experience Sitecore !

More than 300 articles about the best DXP by Martin Miles

Compliance and Geographic Hosting Considerations for Sitecore XM/XP vs XM Cloud

Every organization must consider regulatory requirements when choosing how to deploy Sitecore. The choice between a self-managed Sitecore XM/XP and Sitecore XM Cloud can be influenced by how well each model meets various compliance and data hosting needs. Choosing between XP and XM Cloud is a trade-off: XP gives you full control over data location and security, while XM Cloud delivers built-in compliance where it’s available. Below is a concise comparison of how major regulations affect each model, followed by deeper details.

This post provides an in-depth look at key regulations and data sovereignty concerns, comparing their impacts on traditional self-hosted Sitecore XM/XP versus SaaS XM Cloud deployments. I will cover major data privacy laws: GDPR, UK GDPR, CCPA, industry-specific regulations: HIPAA for health data, PCI DSS for payment data, DORA for financial services, localization laws: Russia, China, Brazil, etc., emerging AI regulations, and data encryption/sovereignty considerations. A comparison table is included at the end to summarize how each issue affects XM/XP versus XM Cloud.

GDPR and UK GDPR

GDPR – the General Data Protection Regulation in the EU – and its UK counterpart (the UK GDPR, retained from EU law post-Brexit) impose strict rules on handling personal data. Both laws are similar in core principles, but they apply in different jurisdictions: EU versus UK. Organizations using Sitecore need to ensure compliance in terms of data processing roles, user consent, data subject rights (access, deletion, etc.), and data residency.

Data controller vs. data processor

  • A data controller decides the purposes and means of processing personal data and bears primary liability for compliance.

  • A data processor acts only on the controller’s instructions, must secure data appropriately, and assist the controller in meeting its obligations.

Why it matters
Controllers face direct fines and enforcement actions if they fail to collect valid consent, uphold data-subject rights or secure data properly. Processors are liable only for processor-specific duties (security, breach notification, sub-processor management) but must still be tightly contracted and audited.

  • XP

    • You act as both controller and processor. You choose EU or UK data centers (or on-prem), enforce TLS, encrypt databases at rest, maintain records of processing activities, conduct impact assessments, and sign DPAs with any cloud or analytics vendors.

    • You are obtaining valid consent for tracking cookies since Sitecore XP, by default, can track visitors via analytics databases and identify repeat visits. You also deploy consent banners
    • You build workflows for subject-access, rectification, and delete a contact’s data from the Experience Database upon request. Sitecore provides tools like the xConnect API to help delete or anonymize contact data, but it’s your job to use them appropriately.

    • If using a cloud provider (Azure, AWS) to host, you must ensure EU->US data transfers, if any, comply with GDPR transfer rules. Hosting entirely within the EU avoids needing special measures. For UK personal data, hosting in the UK or in countries with UK adequacy decisions facilitates compliance.
  • XM Cloud

    • You remain the controller; Sitecore is the processor. Sitecore encrypts data by default, segments customer data in EU or UK regions, and provides a processor agreement covering lawful transfers (adequacy decisions or SCCs), breach notifications, and sub-processor transparency.

    • Your team focuses on capturing valid consent, configuring Sitecore’s privacy settings, and using its APIs to fulfill data-subject requests.

    • With XM Cloud, you cannot self-select the exact data center location in the same way as self-hosting, but Sitecore offers regional hosting options. For example, a European client’s instance can be hosted in an EU data region, ensuring data stays in Europe to comply with GDPR’s data locality expectations.
    • If you require UK-only data residency, confirm if Sitecore can host specifically in the UK or an EU region that is acceptable under UK GDPR.

CCPA and CPRA

For organizations handling personal information of California residents, the California Consumer Privacy Act is a key regulation. CCPA focuses on data disclosures, the sale of personal information, and consumer rights to access or delete data. While CCPA doesn’t mandate data residency, it imposes obligations on how data is collected and shared.

  • XP

    • You implement "Do Not Sell or Share" links, provide the required privacy notices on your website, detect opt-outs in your code, map personal data across xDB, and delete or export records via Sitecore APIs or SQL queries.

    • CPRA adds rights to correct inaccurate data, limit the use of sensitive personal information, and receive detailed disclosures.

    • CCPA’s concept of "service provider" would apply to any vendors you use. If you host Sitecore on a cloud platform, that cloud vendor is your service provider (they process data only on your instructions). You should have a data processing addendum in place with them that meets CCPA’s service provider criteria (major cloud providers do offer these). Sitecore, as the company, typically wouldn’t be directly involved unless you send data to Sitecore support.

  • XM Cloud

    • Sitecore is your contracted service provider and certifies that it never sells or shares data. You still build front-end opt-out logic.

    • For consumer requests, you invoke Sitecore’s deletion/export endpoints or submit a support ticket for complete removal, including backups.

    • Opt-Out of Sale/Share: Since Sitecore Cloud itself is a service provider, data stored there isn’t a sale, but any other integrations of third-party ad networks via the site that could be considered a "sale" of data. That’s outside Sitecore’s scope, but you might use Sitecore to manage tags or content that could involve personal data transfers. Ensure you have a consent management solution for your website.
    • XM Cloud being headless means you might implement a separate consent banner in your front-end application.
    • CCPA does not require local storage in California, so having data in a Sitecore data center, even if in another state or in the cloud abroad, is fine as long as protections are in place. Typically, a U.S. company using XM Cloud would choose a North America region for data residency.

HIPAA (Past & 2025 Updates)

If you are in the healthcare sector or otherwise deal with Protected Health Information (PHI) in the U.S., compliance with HIPAA (Health Insurance Portability and Accountability Act) is required. HIPAA safeguards health information in the U.S. under three rules—Privacy, Security, and Breach Notification. Recent guidance expanded "PHI" to include behavioral tracking on health sites, and enforcement ramps up in 2025.

  • XP

    • Host on a HIPAA-compliant platform with a Business Associate Agreement (BAA).

    • Encrypt all PHI in xDB and analytics stores, enforce strict role-based access, maintain detailed audit logs, train staff on HIPAA policies, and have a breach-response plan.

  • XM Cloud

    • Sitecore has third-party attestation and signs a BAA, applying required safeguards for encryption, patching, access controls, and breach processes.

    • You focus on front-end consent capture, limit tracking to necessary PHI, and use Sitecore’s secure controls for record management.

    • Customer Responsibilities Remain: Even with Sitecore’s platform being HIPAA-ready, you, as the healthcare organization, must still use it properly.
    • You can still use personalization, but do so under the umbrella of HIPAA compliance. For instance, if using Sitecore CDP/Personalize for a patient portal experience, it will treat tracking data as PHI and store it accordingly.

PCI DSS

The Payment Card Industry Data Security Standard (PCI DSS) applies to any environment that processes, stores, or transmits credit card data. If your Sitecore implementation involves e-commerce or donations where card payments are handled, PCI compliance becomes a factor. PCI DSS sets twelve requirements for any system handling credit-card data, covering network security, data protection, vulnerability management, access control, monitoring, and policy.

  • XP

    • If you process or store raw card data, your entire Sitecore environment is in PCI scope. You need segmentation, encrypted vaults for keys, scans, penetration tests, strict logging, and a formal QSA audit.

    • Most avoid this by using tokenized forms or redirects so card data never touches Sitecore.

  • XM Cloud

    • XM Cloud is not PCI-certified for raw card handling.

    • Best practice is to use a PCI-compliant gateway, for example, Stripe via client-side scripts, and store only non-sensitive tokens or order IDs in Sitecore, keeping the CMS outside PCI scope.

DORA

For organizations in the financial sector, especially in the EU, the Digital Operational Resilience Act (DORA) is a new regulation, EU 2022/2554, that takes effect starting in 2025. DORA is all about ensuring that financial entities (banks, insurance companies, investment firms, etc.) and their critical IT service providers maintain robust operational resilience, including cybersecurity, incident reporting, and business continuity. It essentially mandates strict ICT risk management and includes rules for contracts between financial institutions and IT providers.

  • XP

    • Deployed on-prem or in your cloud, XP is an internal ICT asset. You include it in your ICT risk framework, resilience planning, and incident reporting—no special contract with Sitecore is needed.

    • If you operate Sitecore yourself and experience an incident, you handle reporting per DORA’s rules.
  • XM Cloud

    • Sitecore becomes an outsourced ICT provider. You must sign Sitecore’s DORA addendum, embedding clauses on SLAs, data access, subcontractor control, and incident notifications.

    • You rely on Sitecore’s resilience measures (backups, failover) and formal incident reports to meet DORA obligations.

    • DORA expects robust continuity plans. Sitecore, running XM Cloud, will have its own resilience measures (redundant infrastructure, backups, disaster recovery) to ensure continuity​
    • As a customer, you should inquire about RPO/RTO (Recovery Point/Objectives) for XM Cloud in disaster scenarios, and ensure those align with your needs.
    • You should also have an exit strategy (how to retrieve data from Sitecore if you needed to switch systems – DORA mandates having plans for termination of a provider contract).

Localization Laws

Beyond global regulations, many countries have specific data localization laws or restrictions requiring personal data of their citizens to be stored within national borders or meeting certain conditions.

If your Sitecore implementation serves users in such countries, you must account for these:

  • Russia (Law 242-FZ) mandates that all Russian-citizen data be stored in-country. XP lets you deploy to Russian data centers; XM Cloud cannot comply without a local region unless Sitecore establishes a dedicated local region there, which is unlikely given geopolitical complexities.

  • China (PIPL) demands in-country storage unless a security assessment is passed and public sites hold an ICP license. XP supports local clouds: Alibaba, Tencent, China-hosted Azure/AWS; XM Cloud is unavailable under these rules.

  • Brazil (LGPD) mirrors GDPR but allows cross-border transfers under adequacy, consent, or contractual safeguards. Many host in Brazil for performance - both XP and XM Cloud can comply.

  • Other Markets: India’s law permits whitelisted transfers; Canada’s PIPEDA has no strict localization; some Middle-East governments require local hosting. XP adapts universally; XM Cloud is limited to Sitecore’s region footprint.

AI Regulations

As Sitecore adds more AI-driven features, such as Sitecore Stream – the new AI orchestration and “copilot” capabilities across the platform – and AI-based personalization or content generation tools, organizations need to anticipate compliance with emerging AI regulations.

Emerging rules govern AI by risk level:

  • EU AI Act classifies AI from unacceptable to minimal risk. Marketing personalization and content generation are “limited risk,” requiring transparency and logging; high-risk uses, such as credit scoring, need full conformity assessments.

  • U.S. Guidelines (FTC, NIST) urge against deceptive AI use, mandate bias mitigation and data privacy; states address biometric profiling.

  • XP

    • You bear full responsibility for AI integrations (recommendation engines, chatbots). You must document model purposes, disclose AI use, ensure human oversight, and conduct impact assessments if required.

  • XM Cloud

    • Stream AI offers built-in guardrails, never trains on your data, and runs on Azure OpenAI with compliance certifications. You label AI outputs and disclose usage, while Sitecore manages secure model operations and regulatory alignment.

Summary Comparison Table

Regulation XM/XP XM Cloud
GDPR & UK GDPR Controller+processor – you build consent, encryption, erasure workflows. Controller+processor – Sitecore provides encryption, regional hosting, processor agreement.
CCPA / CPRA You build opt-out, mapping, deletion, and access workflows. Sitecore is a service provider – you invoke its deletion/access APIs.
HIPAA Host under BAA, encrypt PHI, audit logs, and breach response. Sitecore is HIPAA-ready under BAA with third-party attestation.
PCI DSS Full PCI scope if cards touch CMS, or avoid storing card data. Use an external gateway; only tokens in Sitecore to stay out of scope.
DORA Internal ICT asset – include in resilience and incident plans. Outsourced ICT – sign DORA addendum and rely on Sitecore’s SLAs/reporting.
Localization Laws Deploy in any national region to meet in-country mandates. Limited to Sitecore’s regions – cannot satisfy strict localization rules.
AI Regulations You ensure transparency, fairness, risk assessments, and audits. Stream AI has guardrails, no training on your data; you disclose AI use.
Encryption & Keys You enable TDE, disk encryption, TLS, and manage keys. Sitecore handles encryption at rest/in transit, but manages keys.

(Table Key: PHI = Protected Health Information, BAA = Business Associate Agreement, SCC = Standard Contractual Clauses, ICT = Information and Communication Technology.)

Conclusion

Sitecore XM/XP (self-hosted) and XM Cloud each have distinct strengths when it comes to compliance and geographic hosting:

  • Sitecore XM/XP (On-Premises or Customer-Managed) offers ultimate control over data – you decide where it lives, how it’s secured, and when to upgrade or patch. This makes it well-suited for organizations with strict data sovereignty demands or heavy regulatory obligations that standard cloud setups can’t meet. For instance, if you absolutely must keep data within a certain country’s borders (Russia, China) or within an isolated network, XP gives you that flexibility. It’s also advantageous if you require deep customization at the infrastructure level (custom encryption key management, specialized audit logging, etc.). In short, when localization and direct control are non-negotiable, the traditional XP deployment can fulfill those needs. However, with this power comes responsibility: your team must invest in security and compliance efforts (hardening servers, obtaining certifications, managing disaster recovery). As one Sitecore expert put it, XP’s flexibility can address strict compliance scenarios, though the effort is on you to secure and certify such an installation.
  • Sitecore XM Cloud (SaaS) provides a compliance-aligned platform for the most common needs. Sitecore has done the heavy lifting for GDPR, HIPAA, and cloud security standards, relieving customers from worrying about infrastructure-level compliance. This model shines for organizations that want to offload operations and focus on content and digital strategy, while trusting Sitecore to maintain a secure, compliant service. It’s especially beneficial if your regulatory requirements can be met by the service’s existing framework (e.g., hosting in broadly acceptable regions, encryption, standard certifications). For EU personal data, XM Cloud can keep you compliant by hosting in-region and acting as a proper data processor with a robust DPA. For healthcare, XM Cloud now meets HIPAA requirements, allowing use of cutting-edge features in a regulated context that previously might have required on-prem. For financial services, XM Cloud can be used with the right contractual protections (DORA addendum), letting banks leverage SaaS agility. The trade-off is reduced flexibility: if your needs fall outside the service’s design, there’s little you can change. You must also be comfortable ceding control; for some, that trust in a third party is a hurdle.

Ultimately, the choice may come down to the specifics of your compliance landscape. Many organizations will do a risk assessment: if using XM Cloud, can we accept its regional hosting options and trust model for our data? If yes, the benefit is quicker deployment and less maintenance burden, with Sitecore’s expertise backing compliance. If no – perhaps due to a unique law or an internal policy – then running Sitecore XP in your own controlled environment remains a valid path.

It’s worth noting that some organizations pursue a hybrid approach: using XM Cloud for most content management needs, but keeping certain data or functions on-prem. For example, a global company might run XM Cloud for its main website but have a separate XP instance for the Chinese market due to localization rules. Or use XM Cloud for content, but integrate with an on-prem analytics store for sensitive customer data. These approaches can mitigate compliance concerns while still reaping some SaaS benefits, though they add complexity.

In conclusion, both XP and XM Cloud can support a compliant solution – the difference lies in who manages the compliance controls and how granular your control is. If your organization has strong compliance and IT teams and needs fine-grained control or non-standard hosting, Sitecore XM/XP gives you the needed freedom (with effort). If your organization prefers to leverage vendor compliance investment and standardize on best practices, XM Cloud offers a convenient, secure choice that is continually updated by Sitecore.

References:

Redirects in Sitecore XM Cloud - know your options!

Sitecore XM Cloud supports multiple redirect strategies, from content-authored redirects via Sitecore items to headless app configuration and middleware logic.

In XM Cloud implementations, there are three typical patterns for performing redirects:

  • they can be defined at the CMS as Redirect Items or Redirect Maps, or
  • handled by Next.js middleware at runtime, or
  • baked into the front-end hosting, for example, in next.config.js or platform rules.

Each approach has tradeoffs in flexibility, performance, and author control. In addition, hosting platforms like Vercel, Netlify also allow static redirects via their config files or APIs, but that is outside of scope for XM Cloud, which already provides built-in mechanisms so marketers can manage redirects without code deployments​


Content-Authored Redirects: Items vs. Maps

XM Cloud’s built-in Redirect Item and Redirect Map features let content authors define redirects in the Content Editor.

A Redirect Item is created under a page node (right-click, Insert → Redirect) and simply points to a target URL. When a request hits that item’s path, the configured redirect is issued.

Redirect Maps live in the site’s Redirects settings and can contain many rules; each map item can define multiple source-to-target path mappings, with support for 301/302 and even server transfer redirects. Maps allow regex patterns and grouping, so a handful of regex rules can replace dozens of single-item redirects. For example, a mapping rule like ^/products/(.*)/(.*)$ -> /groceries/$1/$2 applies to many URLs, whereas a Redirect Item only covers one exact path.

Redirect Items are simplest for one-off cases, but managing many items can be cumbersome. Redirect Maps centralize rules (groups/folders, regex) for better visibility and maintainability. In practice, use Redirect Items for a few simple vanity URLs or moved pages, and Redirect Maps when you have multiple or patterned redirects. Always plan the scope: XM Cloud documentation even recommends limiting a map to ~200 entries for performance and manageability.

In either case, after creating or changing redirects, you must publish the site item (root) to push the updates to Experience Edge. XM Cloud caches redirect data at the edge with a typical ~4-hour TTL, so republishing the site clears that cache and makes new rules active immediately.

 

Developer-Controlled Redirects (via Head App)

For larger redirect sets or developer-controlled routing, it’s common to handle redirects on the front-end app instead of via XM Cloud. Next.js lets you define static redirects in next.config.js, which Vercel and other hosts apply at build time. For example:

module.exports = {
  async redirects() {
    return [
      { source: '/old-page', destination: '/new-page', permanent: true },
      { source: '/old-blog/:slug*', destination: '/blog/:slug*', permanent: true },
      // ... up to 1024 entries
    ]
  }
};

This redirects() array can include regex and wildcard matching. Because these rules are generated at build time and handled on the CDN edge, they execute before any JS or middleware, which means faster, low-overhead redirects. In fact, Next.js processes next.config.js redirects at the edge like at. Vercel’s network, so users are redirected without even involving server-side code. The trade-off is that such redirects only update when you rebuild the site – they cannot be changed dynamically by content authors.

Most static hosting providers also offer redirect files. For example, Netlify will process a plain-text _redirects file in CSV or _redirects format, and Vercel imposes a 1024-entry limit on static redirects. The Sitecore Accelerate docs note that beyond ~1024 redirects you should migrate to an Edge Function or JSON-driven middleware approach. Indeed, if you hit the limit, one can place a JSON of rules (.sitecore/redirects.json) and a custom middleware plugin to read it, avoiding XM Cloud entirely.

 

Middleware-Based Redirects (Dynamic)

In a pure headless scenario, XM Cloud content-managed redirects are usually handled by Next.js middleware at runtime. The standard XM Cloud starter kit includes a Redirects middleware plugin: on each request, it queries Experience Edge for any matching Redirect Item or Map and issues a redirect if found. This means redirects are always up-to-date without rebuilding the site. However, it also means every request incurs a check and often a GraphQL call against Edge.

User Request → Next.js Middleware: 
   - Load Redirects via GraphQL (from XM Cloud)
   - If a rule matches the path, return redirect response; otherwise continue

Because the middleware runs on every request unless specifically filtered outhaving many redirects can hurt performance. Sitecore’s docs warn that “the redirect middleware needs to process the list by hitting Experience Edge, which can cause performance issues” when redirects are numerous. The middleware can also be tuned: for instance, you can narrow its scope by adjusting the matcher so it only runs on certain paths, not on APIs, static assets, etc. If a site isn’t using content-managed redirects at all, you can disable or remove the redirect middleware plugin entirely.

In short, dynamic middleware redirects offer real-time flexibility where authors change in CMS and immediately get it effective after publishing, but at the cost of per-request overhead. In scenarios like user-specific or geolocation-based redirects, dynamic middleware is useful. If you anticipate a large number of static redirects with no runtime logic, it’s generally better to move them into a build-time config or use regex grouping to reduce count.

 

Hybrid Build-Time Redirects

An emerging approach combines the strengths of both worlds: content-managed redirects in XM Cloud, but applied via a build/redeploy, so that no middleware query is needed at request time. In this hybrid strategy, editors still create Redirect Items/Maps in XM Cloud, but a hook triggers a site rebuild. For example, you can use an Experience Edge webhook on the site to notify a custom service when redirects change. The service or another automation tool listens for that update, then calls the Vercel REST API to redeploy the site.

During the build, a custom script runs. This script invokes the Edge GraphQL API to retrieve all Redirect Maps and Items, transforms them into Next.js redirect objects, and writes them into a JSON or directly into next.config.js format. The build output then includes a static list of redirects (for example, in .sitecore/redirects.json) that Next.js will apply at the CDN edge. As a result, end users see the updated redirects immediately after the deploy, and the runtime Next.js middleware is bypassed entirely, improving performance.

This hybrid pattern requires some initial setup, but it keeps redirect management user-friendly: editors still work in XM Cloud as usual, but developers configure the rebuild pipeline. Key steps include: configuring the Edge webhook via the XM Cloud Admin API, setting up a middleware or serverless listener to call the Vercel "Redeploy" endpoint, and adding a build-time function like generateRedirects or similar in the Next.js app that populates the redirect list. The result is a form of hybrid static redirects that are always in sync with the CMS without incurring request-time lookup costs

 

Performance Considerations & Best Practices

  • Publish the Site: Remember to publish the headless site item whenever redirect items or maps change. XM Cloud’s Edge caches redirect data (about 4 hours by default), so failing to republish can cause old redirect rules to linger. In practice, always include the site node in your publish steps after editing redirects.

  • Use Regex Judiciously: Redirect Maps support full regex, which is powerful but costly to evaluate. Prefer direct path matches when possible, and group common patterns into a single regex rule. This reduces the total count and keeps matching fast.

  • Limit Redirect Count: If you have hundreds of redirects, especially distinct ones, consider moving them out of the CMS and into the front-end config. Next.js has a 1024-entry limit on static redirects, and thousands of CMS-managed redirects can strain middleware performance. The accelerate docs even suggest: “if you have a large number of redirects, you need to use the hosting provider features”, for example, Next.js config or Edge Functions.

  • Optimize Middleware: If using middleware, narrow its scope. In Next.js 13+, use the matcher option in middleware.ts to skip API routes, static assets (/_next/), or health checks. Also, XM Cloud starter kits allow disabling the redirect plugin if not needed. Excessive link prefetching or personalization features can inadvertently invoke the middleware multiple times, so configure prefetch settings appropriately.

  • Head-App Redirects First: As a rule of thumb, use static head-app redirects whenever feasible. These execute at the CDN edge and avoid server work. Reserve middleware redirects for cases where you truly need runtime logic - either user-based or geo-based, or maybe A/B testing.

  • Test Redirect Order: In Next.js, the order of plugins or configuration can matter. If a redirect isn’t firing, check that the Redirects plugin/middleware has higher priority than others, for example, than any catch-all pages.

  • Environment Nuances: Be aware of hosting specifics. For example, Netlify automatically sorts query parameters, which can affect regex matches. And ensure your targetHostname setting in XM Cloud site definitions includes your domains, so redirects use the correct host.

By combining these techniques - CMS-managed maps for author edits, static redirects in the head app for scale, and judicious middleware use for dynamic cases -you can build a robust redirect strategy in XM Cloud. The key is to balance flexibility (author editing, regex) with performance (pre-calculated redirects, minimizing per-request work). With careful planning and the new hybrid approach, XM Cloud sites can seamlessly redirect users and preserve SEO even as content or site structure changes.

References:

 

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.

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!

Sitemaps in Sitecore XM Cloud: Automation, Customization, and SEO Best Practices

In Sitecore XM Cloud, sitemaps are generated and served via Experience Edge to inform search engines about all discoverable URLs. XM Cloud uses SXA’s built‑in sitemap features by default, storing the generated XML as media items in the CMS so they can be published to Experience Edge. Sitemap behavior is controlled by the Sitemap configuration item under /sitecore/content/<SiteCollection>/<Site>/Settings/Sitemap. There are few important fields - Refresh threshold which defines minimum time between regenerations, Cache expiration, Maximum number of pages per sitemap for splitting into a sitemap index, and Generate sitemap media items which must be enabled to publish via Edge. The Sitemap media items field of the Site item will list the generated sitemap(s) under /sitecore/media library/Project/<Site>/<Site>/Sitemaps/<Site>​, and the default link provider is used unless overridden. Tip: you can configure a custom provider via <linkManager> and choose its name in the Sitemap settings.

Automated Sitemap Generation Workflow

When content authors publish pages, XM Cloud schedules sitemap regeneration automatically based on the refresh threshold. Behind the scenes, an OnPublishEnd pipeline (often the SitemapCacheClearer.OnPublishEnd handler in SXA) checks each site’s sitemap settings. If enough time has elapsed since the last build, a Sitemap Refresh job runs. In this job, the old sitemap media item is deleted and a new one is generated and saved in the Media Library​. Once created, the new sitemap item is linked in the Sitemap media items field of the site and then published. This typically triggers two publish actions: one to publish the new media item (/sitecore/media library/Project/.../Sitemaps/<Site>/sitemap) and one to re-publish the Site item so Experience Edge sees the updated link.

For high-volume publishing, it’s best to set a reasonable refresh threshold to batch sitemap generation. For example, if you publish many pages daily, you might set the refresh threshold to 0 forcing a rebuild every time, or schedule a daily publish so the sitemap is updated once per day. Generating sitemaps can be resource-intensive especially for large sites, so avoid rebuilding on every small change unless necessary.

Sitemap Filtering: SXA provides pipeline processors to include or exclude pages. By default, items inheriting SXA’s base page templates have a Change frequency field. Setting it to "do not include" will exclude that page from the sitemap​. The SXA sitemap pipelines (sitemap.filterItem) include built‑in processors for base template filtering and change-frequency logic. To exclude a page, simply open it in Content Editor (or Experience Editor SEO dialog) and set Change frequency to "do not include"​.

GraphQL Sitemap Query: Once published, the XM Cloud GraphQL API provides access to the sitemap media URL. For example, the following query returns the sitemap XML URL for a given site name:

query SitemapQuery($site: String!) {
      site {
        siteInfo(site: $site) {
          sitemap
        }
      }
    }

This returns the Experience Edge URL of the generated sitemap media item. You can use this in headless code or debugging to verify the sitemap’s existence and freshness.

Sitemaps in Local Docker Containers

In a local XM Cloud Docker setup, the /sitemap.xml route often returns an empty file by default because the Experience Edge publish never occurs. There is no web database or Edge target, so the OnPublishEnd process never actually runs, leaving the empty sitemap item. Attempting to publish locally throws an exception (Invalid Authority connection string for Edge). To debug or test sitemap issues locally, you can manually trigger the SXA sitemap pipeline.

I really like the Sitemap Developer Utility approach suggested by Jeff L'Heureux: in your XM Cloud solution’s Docker files, create a page (e.g. generateSitemap.aspx) inside docker\deploy\platform with code that simulates a publish event. For example, one can invoke the SitemapCacheClearer.OnPublishEnd() method manually in C#.

// Simulate a publish event for the "Edge" target
    Database master = Factory.GetDatabase("master");
    List<string> targets = new List<string> {"Edge"};
    PublishOptions options = new PublishOptions(master, master, PublishMode.SingleItem, 
        Language.English, DateTime.Now, targets);
    Publisher publisher = new Publisher(options);
    SitecoreEventArgs args = new SitecoreEventArgs("OnPublishEnd", new object[] { publisher }, new EventResult());
    new SitemapCacheClearer().OnPublishEnd(null, args);
    

This code triggers the same sitemap build logic as a real publish​. Jeff's utility page provides buttons to run various steps (OnPublishEnd, the sitemap.generateSitemapJob pipeline, etc.) and shows output.

Once you run the utility and the cache job completes, the media item is regenerated. Then restart or refresh your Next.js site locally to see the updated sitemap at http://front-end-site.localhost/sitemap.xml. The browser will display the raw XML with <loc>, <lastmod>, <changefreq>, and <priority> entries as it normally should.

Sitemap Customization for Multi-Domain Sites

A common scenario is one XM Cloud instance serving multiple language or regional domains (say, www.siteA.com and www.siteA.fr) with one shared content tree. In SXA this is often handled by a Site Grouping with multiple hostnames. By default, SXA will generate a single sitemap based on the primary hostname. This leads to two issues: the same XML file is returned on both domains, and each page appears several times (once per language) under the same <loc>. For example, a bilingual site without customization might show both English and French URLs under the English domain, duplicating <url> entries.

To fix this, customize the Next.js API route (e.g. pages/api/sitemap.ts) that serves /sitemap.xml. The approach is: detect which host/domain the request is for, fetch the raw sitemap XML via GraphQL, and then filter and rewrite the entries accordingly. For instance, if the host header contains the French domain, only include the French URLs and update the <loc> and hreflang="fr" links to use the French hostname. Pseudocode for the filtering might look like:

if (lang === 'en') {
      // Filter out French URLs and fix alternate links
      urls = urls.filter(u => !u.loc[0].includes(FRENCH_PREFIX))
                 .map(updateFrenchAlternateLinks);
    } else if (lang === 'fr') {
      // Filter out English URLs and swap French loc to French domain
      urls = urls.filter(u => u.loc[0].includes(FRENCH_PREFIX))
                 .map(updateLocToFrenchDomain)
                 .map(updateFrenchAlternateLinks);
    }
    

Here, FRENCH_PREFIX is something like en.mysite.com/fr, and we replace it with the French hostname. In practice, the XML is parsed (e.g. via xml2js), then the result.urlset.url array is filtered and modified, and rebuilt to XML. There is a great solution suggested by Mike Payne which uses two helper functions filterUrlsEN and filterUrlsFR to drop unwanted entries and updateLoc/updateFrenchXhtmlURLs to replace URL prefixes​. Finally, the modified XML is sent in the HTTP response. This ensures that when a sitemap is requested from www.site.ca, all <loc> URLs and alternate links point to site.ca, and when requested from www.othersite.com, they point to www.othersite.com.

SEO Considerations and Best Practices

  • Include Alternate Languages (hreflang): XM Cloud (via SXA) automatically adds <xhtml:link rel="alternate" hreflang="..."> entries in the sitemap for multi-lingual pages. Ensure these are correct for your domains. After customizing for multiple hostnames, the <xhtml:link> URLs should also be updated to the appropriate domain​. This helps Google index the right language version for each region.

  • Set Change Frequency and Priority: Use SXA’s SEO dialog or Content Editor on the page item to set Change frequency and Priority for each page. For example, if a page is static, set a low change frequency. These values are written into <changefreq> and <priority> in the sitemap. Note: Pages can be excluded by setting frequency to "do not include".

  • Maximize Crawling via Sitemap Index: If your site has many pages, configure Maximum number of pages per sitemap so XM Cloud generates a sitemap index with multiple files. This avoids any single sitemap exceeding search engine limits and keeps crawlers from giving up on a very large file.

  • Robots.txt: SXA will append the sitemap link /sitemap.xml to the site’s robots.txt automatically​. Verify that your robots.txt in production references the correct sitemap and hostname.

  • Media Items and Edge: Always keep Generate sitemap media items enabled: without having this, XM Cloud cannot deliver the XML to the front-end. After a successful build, the sitemap XML is stored in a media item and served by Experience Edge. You can confirm the published sitemap exists by checking /sitecore/media library/Project/<Site>/<Site>/Sitemaps/<Site> or by running the GraphQL query mentioned above.

  • Link Provider Configuration: If your site uses custom URL routing (e.g. language segments or rewritten paths), you can override the link provider used for sitemap URLs. In a patch config, add something like:

    <linkManager defaultProvider="switchableLinkProvider">
          <providers>
            <add name="customSitemapLinkProvider" 
                 type="Sitecore.XA.Foundation.Multisite.LinkManagers.LocalizableLinkProvider, Sitecore.XA.Foundation.Multisite"
                 lowercaseUrls="true" .../>
          </providers>
        </linkManager>

    Don't forget to set the "Link provider name" field in the Sitemap settings to customeSitemapLinkProvider​ afterwards. This ensures the sitemap uses the correct domain and culture prefixes as needed.

Diagnostics and Troubleshooting

If the sitemap isn’t updating or the XML is wrong, check these:

  • Site Item Settings: On the site’s Settings/Sitemap item, confirm the refresh threshold and expiration are as expected. During debugging you can set threshold to 0 to force immediate rebuilds.

  • Was it published to Edge? Ensure the sitemap media item was published to Edge. You might need to publish the Site item or Media Library manually if it wasn’t picked up.

  • Cache Type: In the SXA Sitemap settings, the Cache Type can be set to "Inactive," "Stored in cache", or "Stored in file". For XM Cloud, the default "Stored in file" is typically used so the XML is persisted. If set to "Inactive", the sitemap generator will not run.

  • Inspect Job History: In the CM admin (/sitecore/admin/Jobs.aspx), look for the "Sitemap refresh" jobs to see if these succeeded or threw errors.

  • Next.js Route Errors: If your Next.js site’s /sitemap.xml endpoint returns an error, inspect its handler. The custom API route uses GraphQLSitemapXmlService.getSitemap(). Ensure the hostnames in your logic match your ENV variables, namely PUBLIC_EN_HOSTNAME. Add logging around the xml2js parsing if the output seems empty or malformed.

By following the above patterns - configuring SXA sitemap settings, automating generation on publish, and customizing for your site topology -you can ensure that XM Cloud serves up accurate, SEO‑friendly sitemaps. This helps search engines index your content fully and respects multi-lingual domain structures and refresh logic specific to a headless architecture.

References: one, two, three and four.

XM Cloud content migration: connecting external database

Historically when performing content migration with Sitecore we used to deal with database backups. In a modern SaaS world, we do not have the luxury of neither managing cloud database backups, nor the corresponding UI for doing this. Therefore, we must find an alternative approach.

Technical Challenge

Let’s assume we have a legacy Sitecore website, in my case that was XP 9.3 and we’ve been provided with only a master database backup having all the content. The objective is to perform content migration from this master database into a new and shiny XM Cloud environment(s).

Without having direct access to the cloud, we can only operate locally. In theory, there could be a few potential ways of doing this:

  1. Set up a legacy XP of the desired version with the legacy content database already attached/restored to it. Then try to attach (or restore) a vanilla XM Cloud database to a local SQL Server as a recipient database in order to perform content migration into it. Unfortunately, the given approach would not work since SQL Server version incompatibility between XM Cloud and XP 9.3. Even if that was possible, running XP 9.3 with the XM Cloud database won’t work as Xз 9.3 neither knows about XM Cloud schema nor is capable of handling Items as Resource required feature which was invented later in XP 10.1. Therefore – this option is not possible.

  2. Can we go the other way around by using the old database along with XM Cloud? This is not documented, but let’s assess it:

    1. Definitely won’t work in the cloud since we’re not given any control of DBs and their maintenance or backups.

    2. In a local environment, XM Cloud only works in Docker containers and it is not possible to use it with an external SQL Server where we have a legacy database. But what if we try to plug that legacy database inside of the local SQL Container? Sadly, there are no documented ways of achieving that.

  3. Keep two independent instances side by side (legacy XP and XM Cloud in containers) and use an external tool to connect both of them in order to migrate the content. In theory that is possible but carries on few drawbacks.
    1. The tool of choice is Razl, but this tool is not free, requires a paid license, and does not have a free trial to ever test this out.
    2. Connecting to a containerized environment may not be easy and require some additional preps
    3. You may need to have a high-spec computer (or at least two mid-level machines connected to the same network) to have both instances running side by side.

After some consideration, the second approach seems to be reasonable to try so let’s give it a chance and conduct a PoC.

Proof of Concept: local XM Cloud with external content database

Utilize the second approach we’re going to try attaching the given external legacy database to XM Cloud running in a local containerized setup. That will allow using a built-in UI for mass-migrating the content between the databases (as pictured below) along with the Sitecore PowerShell script for finalizing and fine-tuning the migrated content.

Control Panel

Step 1: Ensurу SQL Server port is externally exposed

We are connecting the external SQL Server Management studio through a port of the SQL Server container that is exposed externally in order to make it possible. Luckily, that has been done for us already, just make sure docker-compose has:

ports:
          - "14330:1433"

Step 2: Spin up an XM Cloud containers and confirm XM Cloud works fine for you

Nothing extraordinary here, as easy as running .\init.ps1 followed by .\up.ps1.

Step 3: Connect SQL Management Studio to SQL Server running in a container.

After you sound up containers, run SQL Management Studio and connect to SQL Server running in SQL container through an exposed port 14330, as we did at step 1:

Connection parameters

Step 4: Restore the legacy database

If you have a Data-Tier “backpack” file you may want to do an extra step and convert it into a binary backup for that particular version used by XMCloud before restoring. This step is optional, but in case you want to restore the backup more than once (which is likely to happen), it would make sense to take a binary backup as soon as you restore the data-tier “backpack” first time ever. Data-tier backups process much slower than binaries, so that will definitely save time in the future.

Once connected, let’s enable contained database authentication. This step is mandatory, otherwise, that would not be possible to restore a database:

EXEC sys.sp_configure N'contained database authentication', N'1'
    go
    exec ('RECONFIGURE WITH OVERRIDE')
    go

One more challenge ahead: when performing backup and restore operations, SQL Server shows up a path local to the server engine, and not the host machine. That means, our backup should exist “inside” of SQL container. Luckily, w have this also covered. Make sure docker-compose.override.yml contains:

mssql:
    volumes:
          - type: bind
    source: .\docker\data\sql
    target:c:\data

That means, one can locate legacy database backups into .\docker\data\sql folder of a host machine and it will magically appear within C:\datafolder when using SQL Management Studio database backup restore tool which you can perform now.

Important! Restore legacy database using the “magic name” in a format Sitecore.<DB_NAME_SUFFIX>, further down below I will be using the value RR as DB_NAME_SUFFIX.

Once got restored database in SQL Server Management Studio under the name Sitecore.RR we need to plug this database to the system. There is a naming convention hidden from our eyes within CM containers.

Step 5: Configure connection strings

Unlike in XM/XP – there is no documented way to plug an external database. The way connection strings are mapped to the actual system is cumbersome, it uses some “magic” hidden within the container itself and obfuscated from our eyes. It only tool to reach it experimental way. Here are the steps to reproduce:

  • Add environmental variable to docker-compose record for CM:

    • Sitecore_ConnectionStrings_RR: Data Source=${SQL_SERVER};Initial Catalog=${SQL_DATABASE_PREFIX}.RR;User ID=${SQL_SA_LOGIN};Password=${SQL_SA_PASSWORD}
  • Add a new connection string record. To do so you’ll need to create a connection strings file within your customization project as .\src\platform\<SITENAME>\App_Config\ConnectionStrings.config with the content of the connection strings file from the CM container with the addition of a new string:

Please note the difference in the suffix format of both above records, that is totally fine. CM container still processes that correctly.

Step 6: Reinstantiating CM container

Simply restarting a CM container is not sufficient. You must remove it and re-create it, just killing/stopping is not sufficient.

For example, the below command will work for that purpose:

docker-compose restart cm

… not will this one:

docker-compose kill cm

The reason is that CM will not update environmental variables from docker-compose file upon restart. Do this instead:

docker-compose kill cm
    docker-compose rm cm --force
    docker-compose up cm -d

Step 7: Validating

  1. Inspecting CM container for environmental variables will show you this new connection string, as added:

    1. "Env": [
                      "Sitecore_ConnectionStrings_RR=Data Source=mssql;Initial Catalog=Sitecore.RR;User ID=sa;Password=6I7X5b0r2fbO2MQfwKH"
  2. Inspecting connection string config (located at C:\inetpub\wwwroot\App_Config\ConnectionStrings.config on CM container) contains the newly added connection string.

Step 8: Register new database with XM Cloud

It can be done the below config patch that does this job. Save it as docker\deploy\platfo.rm\App_Config\Include\ZZZ\z.rr.config for test and later do not forget to include it in a platform customization project, so that it gets shipped with each deployment
<?xml version="1.0" encoding="UTF-8"?>
    <configuration
        xmlns:patch="www.sitecore.net/.../">
        <sitecore>
            <eventingdefaultProvider="sitecore">
                <eventQueueProvider>
                    <eventQueuename="rr"patch:after="evertQueue[@name='web']"type="Sitecore.Data.Eventing.$(database)EventQueue, Sitecore.Kernel">
                        <paramref="dataApis/dataApi[@name='$(database)']"param1="$(name)"/>
                        <paramref="PropertyStoreProvider/store[@name='$(name)']"/>
                    </eventQueue>
                </eventQueueProvider>
            </eventing>
            <PropertyStoreProvider>
                <storename="rr"patch:after="store[@name='master']"prefix="rr"getValueWithoutPrefix="true"singleInstance="true"type="Sitecore.Data.Properties.$(database)PropertyStore, Sitecore.Kernel">
                    <paramref="dataApis/dataApi[@name='$(database)']"param1="$(name)"/>
                    <paramresolve="true"type="Sitecore.Abstractions.BaseEventManager, Sitecore.Kernel"/>
                    <paramresolve="true"type="Sitecore.Abstractions.BaseCacheManager, Sitecore.Kernel"/>
                </store>
            </PropertyStoreProvider>
            <databases>
                <databaseid="rr"patch:after="database[@id='master']"singleInstance="true"type="Sitecore.Data.DefaultDatabase, Sitecore.Kernel">
                    <paramdesc="name">$(id)
                    </param>
                    <icon>Images/database_master.png</icon>
                    <securityEnabled>true</securityEnabled>
                    <dataProvidershint="list:AddDataProvider">
                        <dataProviderref="dataProviders/main"param1="$(id)">
                            <disableGroup>publishing</disableGroup>
                            <prefetchhint="raw:AddPrefetch">
                                <sc.includefile="/App_Config/Prefetch/Common.config"/>
                                <sc.includefile="/App_Config/Prefetch/Webdb.config"/>
                            </prefetch>
                        </dataProvider>
                    </dataProviders>
                    <!-- <proxiesEnabled>false</proxiesEnabled> -->
                    <archiveshint="raw:AddArchive">
                        <archivename="archive"/>
                        <archivename="recyclebin"/>
                    </archives>
                    <cacheSizeshint="setting">
                        <data>100MB</data>
                        <items>50MB</items>
                        <paths>2500KB</paths>
                        <itempaths>50MB</itempaths>
                        <standardValues>2500KB</standardValues>
                    </cacheSizes>
                </database>
            </databases>
        </sitecore>
    </configuration>

Step 9: Enabling Sitecore PowerShell Extension

Next, we’d want to enable PowerShell, if that is not yet done. You won’t be able to migrate the content using SPE without performing this step.

<?xml version="1.0" encoding="utf-8"?>
    <configuration
        xmlns:patch="http://www.sitecore.net/xmlconfig/"
        xmlns:role="http://www.sitecore.net/xmlconfig/role/"
        xmlns:set="http://www.sitecore.net/xmlconfig/set/">
        <sitecorerole:require="XMCloud">
            <powershell>
                <userAccountControl>
                    <tokens>
                        <tokenname="Default"elevationAction="Block"/>
                        <tokenname="Console"expiration="00:55:00"elevationAction="Allow"patch:instead="*[@name='Console']"/>
                        <tokenname="ISE"expiration="00:55:00"elevationAction="Allow"patch:instead="*[@name='ISE']"/>
                        <tokenname="ItemSave"expiration="00:55:00"elevationAction="Allow"patch:instead="*[@name='ItemSave']"/>
                    </tokens>
                </userAccountControl>
            </powershell>
        </sitecore>
    </configuration>

Include the above code into a platform customization project as .\docker\deploy\platform\App_Config\Include\ZZZ\z.SPE.config. If everything is done correctly, you can run SPE commands, as below:

SPE results

The Result

After all the above steps are done correctly, you will be able to utilize the legacy content database along with your new shiny local XM Cloud instance:
Result in Sitecore Content Editor
Now you can copy items between databases just by using built-in Sitecore UI preserving their IDs and version history. You can also copy items with SPE from one database to another which are both visible to the SPE engine.

.NET Core Renderings for XM Cloud finally gets some love

That is not a secret – Sitecore always used to prioritize Next.Js framework as the first-class citizen for XM Cloud. All the best and finest features tend to find their way to a given framework in the first place. However, recently, there has been much activity around the .NET Core Rendering Framework which makes a lot of sense given most of us, Sitecore tech professionals, originate from the Microsoft and .NET background. More excitement – that is done on .NET 8, which is the latest LST runtime!

Starter Kit

ASP.NET Core framework was with us for a while, periodically receiving some minor updates and fixes. But let’s be honest: having an SDK on its own is one thing, but receiving a decent starter kit on top of that framework is what makes us developers actually create at scale. And that moment has just occurred – without any loud fanfare, XMC ASP.NET Core Starter Kit went public. Please be aware that this is only a PRE-RELEASE version and has its own temporal shortcomings, I gave it a try and want to share my findings with you.

What are these shortcomings? Just a few:

  • FEaaS and BYOC components are not yet supported, therefore you also cannot use Form since it leverages those
  • System.Text.Json serializer is more strict than Newtonsoft which was removed in favor of a built-in solution, thus some components may fail
  • SITECORE_EDGE_CONTEXT_ID variable is not supported

Everything else seems to work the same. There are also some expectations of XM Cloud supporting .NET Rendering at a built-in editing host at some time later in the same manner that works today along with JSS applications, but I do not work for Sitecore and can only make assumptions and guesses without any certainty to it.

First Impression

I forked the repo and cloned the forked code into my computer. Let’s take a look at what we have got there.

VS Code

  • the code varies from what we used to see from XM Cloud Foundation Head starter kit, and that’s understood
  • at the root folder we still have xmcloud.build.json, sitecore.json and folders – .config and .sitecore
  • xmcloud.build.json is required for cloud deploy, but does not have renderingHosts root section required for editing host(s), as I explained above
  • there is headapps folder to keep the solution file along with .NET projects subfolder(s), currently just a single one – aspnet-core-starter
  • there is also local-containers folder that contains docker-compose files, .env, docker files, scripts, Traefik, and the rest of the container assets we got used to
  • another difference – authoring folder contains serialization settings and items as well as .NET framework project for CM customizations
  • however, there are no init.ps1 and up.ps1 files, but that is easy to create yourself by stealing and modifying those from XM Cloud Foundation Head

With that in mind, we can start investigating. There is a ReadMe document explaining how to deploy this codebase, but before going ahead with it I of course decided to:

Run Local Containers

There are no instructions on container setup, only for cloud deployment, but after spending a few years with Foundation Head, the very first thing that naturally comes into my mind is running this starter kit in local Docker containers. Why not?

There are a couple of things one should do first before spinning up containers.

1. Modify settings in .ENV file – at least these two:

# Enter the value for SQL Server admin password:
SQL_SA_PASSWORD=SA_PASSWORD
# Provide a folder storing a Sitecore license file:
HOST_LICENSE_FOLDER=C:\Projects
2. We need to generate Traefik SSL Certificates. To do so let’s create .\local-containers\init.ps1 script with the below content:
    [CmdletBinding(DefaultParameterSetName = "no-arguments")]
    Param()
    $ErrorActionPreference = "Stop";
    
    # duplicates in Up.ps1 scrips
    $envContent = Get-Content .env -Encoding UTF8
    $xmCloudHost = $envContent | Where-Object {$_ -imatch "^CM_HOST=.+"}
    $renderingHost = $envContent | Where-Object {$_ -imatch "^RENDERING_HOST=.+"}
    $xmCloudHost = $xmCloudHost.Split("=")[1]
    $renderingHost = $renderingHost.Split("=")[1]
    
    Push-Location docker\traefik\certs
    try{
        $mkcert = ".\mkcert.exe"
        if($null -ne(Get-Command mkcert.exe -ErrorAction SilentlyContinue)){
            # mkcert installed in PATH
            $mkcert = "mkcert"
        }elseif(-not(Test-Path$mkcert)){
            Write-Host "Downloading and installing mkcert certificate tool..." -ForegroundColor Green
            Invoke-WebRequest "https://github.com/FiloSottile/mkcert/releases/download/v1.4.1/mkcert-v1.4.1-windows-amd64.exe" -UseBasicParsing -OutFile mkcert.exe
            if((Get-FileHash mkcert.exe).Hash -ne "1BE92F598145F61CA67DD9F5C687DFEC17953548D013715FF54067B34D7C3246"){
                Remove-Item mkcert.exe -Force
                throw "Invalid mkcert.exe file"
            }
        }
        Write-Host "Generating Traefik TLS certificate..." -ForegroundColor Green
        & $mkcert -install
        & $mkcert "$xmCloudHost"
        & $mkcert "$renderingHost"
    }
    catch{
        Write-Error "An error occurred while attempting to generate TLS certificate: $_"
    }
    finally{
        Pop-Location
    }

    Write-Host "Adding Windows host"
    Add-HostsEntry "$renderingHost"
    Add-HostsEntry "$xmCloudHost"

    Write-Host "Done!" -ForegroundColor Green

And then execute this script:

Certs

There is no up.ps1 script, so instead let’s run docker-compose directly: docker compose up -d

You may notice some new images show up, and you also see a new container: aspnet-core-starter

Docker

If everything is configured correctly, the script will execute successfully. Run Sitecore from its default hostname, as configured in .env file: https://xmcloudcm.localhost/sitecore

From there you will see no significant changes. Containers just work well! Sitecore has no content to interact with the head application. I will add the content from the template but let’s make the could deployment first.

Deploy to the Cloud

ReadMe document suggests an inconvenient way of cloud deployment:

1. Create a repository from this template.

2. Log into the Sitecore Deploy Portal.

3. Create a new project using the ‘bring your code’ option, and select the repository you created in step 1.

For the majority of us, who are on the Sitecore Partner side, there are only six environments available grouped into two projects. These allocations are priceless and are carefully shared between all XM Cloud enthusiasts and aspirants who are learning a new platform. We cannot simply “create a new project” because we don’t have that spare project, so in order to create one we have to delete the existing one. Deleting a project requires deleting all (three) of its environments in the first place, which is half of the sandbox capacity, carrying valuable work in progress for many individuals.

That is why I decided to use CLI instead. Luckily it works exactly the same as it does with Next.Js starter kits, and from .\.config\dotnet-tools.json you may see that it uses that same version. You deploy the root folder holding xmcloud.build.json file as a working directory, so there are no changes in execution.

Eventually, once deployed we navigate to XM cloud. I decided to follow the ReadMe and create a Basic site from Skate Park template. Basically, I am following steps 4-18 from the ReadMe file.

As a side exercise, you will need to remove a Navigation component from a Header partial item, located at /sitecore/content/Basic/Basic/Presentation/Partial Designs/Header. Basic site will break in the debugger if you do not delete currently incompatible rendering that has a serialization issue.

Building Dev Tunnel in Visual Studio

Next, let’s open and build the solution in the Visual Studio IDE, which refers to .\headapps\aspnet-core-starter.sln file. You may see it related to three Sitecore dependencies from Sitecore.AspNetCore.SDK.LayoutService.Client:

  • Transient: Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces.ISitecoreLayoutClient
  • Singleton: Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.ISitecoreLayoutSerialize
  • Singleton: Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Converter.IFieldParser

Modify .\headapps\aspnet-core-starter\appsettings.json with the setting values collected from the previous steps. You will end up with something looking as:

Appsettings.json

Now let’s create a Dev Tunnel in VisualStudio:

Dev Tunnel

There will be at least two security prompts:

Dev Tunnel Authorize Github Dev Tunnel Authorize Notice

If everything goes well, a confirmation message pops up:

Dev Tunnel Created

Now you will be able to run and debug your code in Visual Studio:

Debugger Works

Make a note of the dev tunnel URL, so that we can use it to configure Rendering Host, as described at step 27 of ReadMe. You will end up with something as below:

Rendering Hosts

So far so good. You can now run the website by URL and in Experience Editor. Running in Page will however not work yet due to the below error:

No Pages Without Publish

To explain that, Experience Editor runs as a part of CM and pulls content from a GraphQL endpoint on that same CM. Pages instead is a standalone separate application, so it does not have access neither to the endpoint nor to the Rendering Hosts settings item. It only has access to Experience Edge so we must publish first. Make sure you publish the entire Site Collection. Once complete, Page works perfectly well and displays the site:

Pages Work 1 Pages Work 2

To explain what happens above: Pages app (which is a SaaS-run editor) pulls Experience Edge for the settings of the rendering editing host (which runs in a debuggable dev tunnel from Visual Studio) and renders HTML right there with the layout data and content pulled from Experience Edge.

Deploy Rendering Host to Cloud

Without much thinking, I decided to deploy the rendering host as Azure Web App, with the assumption that the .NET 8 application would be best supported in its native cloud.

Web App Configure

After the Web App is created, add the required environmental variables. The modern SITECORE_EDGE_CONTEXT_ID variable is not yet supported with .NET Core SDK, so we should go the older way:

Azure App Settings

A pleasant bonus of GitHub integration is that Azure creates GitHub Actions workflow with the default functional build and deployment. There is almost nothing to change, I only made a single fix replacing this run: dotnet publish -c Release -o ${{env.DOTNET_ROOT}}/myapp with a hardcoded path since this variable contains space from “Program Files” part and is incorrectly tokenized breaking the build. After this fix, GitHub actions got built the right way and I started receiving green status:

Github Actions

… and the published site shows up from the Azure Web App powered rendering host:

Published Site

Finally, we can get rid of Dev Tunnel, replacing it with the actual “published site” hostnames:

Getting Rid Of Dev Tunnel

After republishing the Rendering Host item to Edge, we can stop the debugger and close Visual Studio. Both Experience Editor and Pages app are now working with an editing host served by the Azure Web App.

Verdict

Of course, that would be much anticipated for XM Cloud to have built-in .NET editing host capabilities same way as JSS does. But even without it, I applaud to Sitecore development team for making and keeping working on this starter kit as that is a big milestone for all of us within the .NET Community!

With this kit, we can now start building XM Cloud powered .NET app at a faster pace. I believe all the missing features will find their way to the product, and maybe later there will be some (semi)official SSG support for .NET, something like Statiq. That will allow deployments to a wider set of hosting, such as Azure Static Web Apps, Netlify, and even Vercel which does not support .NET as of today.

Full guide to enabling Headless Multisite Add-On for your XM Cloud solution

Sitecore Headless Next.Js SDK recently brought the feature that makes it possible to have multiple websites from Sitecore content tree being served by the same rendering hoist JSS application. It uses Next.js Middleware to serve the correct Sitecore site based on the incoming hostname.

Why and When to Use the Multisite Add-on

The Multisite Add-on is particularly useful in scenarios where multiple sites share common components or when there is a need to centralize the rendering logic. It simplifies deployment pipelines and reduces infrastructure complexity, making it ideal for enterprises managing a portfolio of websites under a unified architecture. This approach saves resources and ensures a consistent user experience across all sites.

How it works

The application fetches site information at build-time, not at runtime (for performance reasons). Every request invokes all Next.js middleware. Because new site additions are NOT frequent, fetching site information at runtime (while technically possible) is not the best solution due to the potential impact on visitor performance. You can automate this process using a webhook to trigger automatic redeployments of your Next.js app on publish.

Sitecore provides a relatively complicated diagram of how it works (pictured below), but if you do not get it from the first look, do not worry as you’ll get the understanding after reading this article.

Uuid 8601bf89 50ab 5913 42ed 4773b05ab2c3[1]

Technical Implementation

There are a few steps one must encounter to make it work. Let’s start with the local environment.

Since multisite refers to different sites, we need to configure hostnames. The Main site operates on main.localhost hostname, and it is already configured as below:

.\.env
    RENDERING_HOST=main.localhost
.\docker-compose.override.yml
    PUBLIC_URL: "https://${RENDERING_HOST}"

For the sake of the experiment, we plan to create a second website served by second.localhost locally. To do so, let’s add a new environmental variable to the root .env file (SECOND_HOST=second.localhost) and introduce some changes to init.ps1 script:

$renderingHost = $envContent | Where-Object{$_ -imatch "^RENDERING_HOST=.+"}
$secondHost = $envContent | Where-Object{$_ -imatch "^SECOND_HOST=.+"}
$renderingHost = $renderingHost.Split("=")[1]
$secondHost = $secondHost .Split("=")[1]

down below the file we also want to create SSL certificate for this domain by adding a line at the bottom:

& $mkcert -install
# & $mkcert "*.localhost"
& $mkcert"$xmCloudHost"
& $mkcert"$renderingHost"
& $mkcert"$secondHost"

For Traefic to pick up the generated certificate and route traffic as needed, we need to add two more lines at the end of .\docker\traefik\config\dynamic\certs_config.yaml file:

tls:
certificates:
    - certFile:C:\etc\traefik\certs\xmcloudcm.localhost.pem
keyFile:C:\etc\traefik\certs\xmcloudcm.localhost-key.pem
    - certFile:C:\etc\traefik\certs\main.localhost.pem
keyFile:C:\etc\traefik\certs\main.localhost-key.pem
    - certFile:C:\etc\traefik\certs\second.localhost.pem
keyFile:C:\etc\traefik\certs\second.localhost-key.pem

If you try initializing and running – it may seem to work at first glance. But navigating to second.localhost in the browser will lead to an infinite redirect loop. Inspecting the cause I realized that occurs due to CORS issue, namely that second.localhost does not have CORS configured. Typically, when configuring the rendering host from docker-compose.override.yml we provide PUBLIC_URL environmental variable into the rendering host container, however, that is a single value and we cannot pass multiple.

Here’s the more descriptive StackOverflow post I created to address a given issue

To resolve it, we must provide the second host in the below syntax as well as define CORS rules as labels into the rendering host, as below:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.rendering-secure.entrypoints=websecure"
  - "traefik.http.routers.rendering-secure.rule=Host(`${RENDERING_HOST}`,`${SECOND_HOST}`)"
  - "traefik.http.routers.rendering-secure.tls=true"
# Add CORS headers to fix CORS issues in Experience Editor with Next.js 12.2 and newer
  - "traefik.http.middlewares.rendering-headers.headers.accesscontrolallowmethods=GET,POST,OPTIONS"
  - "traefik.http.middlewares.rendering-headers.headers.accesscontrolalloworiginlist=https://${CM_HOST}, https://${SECOND_HOST}"
  - "traefik.http.routers.rendering-secure.middlewares=rendering-headers"

Once the above is done, you can run the PowerShell prompt, initialize, and spin up Sitecore containers, as normal, by executing init.ps1 and up.ps1 scripts.

Configuring Sitecore

Wait until Sitecore spins up, navigate to a site collection, right-click it, and add another website calling it Second running on a hostname second.localhost.

Make sure second site uses the same exact application name as main, you can configure that from /sitecore/content/Site Collection/Second/Settings item, App Name field. That ensures the purpose of this exercise is for both sites to reuse the same rendering host application.

You should also make sure to match the values of the Predefined application rendering host fields of /sitecore/content/Site Collection/Second/Settings/Site Grouping/Second and /sitecore/content/Site Collection/Main/Settings/Site Grouping/Main items.

Another important field here is Hostname, make sure to set these fields for both websites:

Hostname

Now you are good to edit Home item of Second site. Multisite middleware does not affect the editing mode of Sitecore, so from there, you’ll see no difference.

Troubleshooting

If you’ve done everything right, but second.localhost resolves to the Main website, let’s troubleshoot. The very first location to check is .\src\temp\config.js file. This file contains sites variable with the array of sites and related hostnames to be used for the site resolution. The important fact is that a given file is generated at the build time, not runtime.

So, you open it up and see an empty array (config.sites = process.env.SITES || '[]') that means you just need to initialize the build, for example by simply removing and recreating the rendering host container:

docker-compose kill rendering
docker-compose rm rendering -f
docker-compose up rendering -d

Also, before running the above, it helps to check SXA Site Manager, which is available under the PowerShell Toolbox in Sitecore Desktop. You must see both sites and relevant hostnames there and in the correct order – make sure to move them as high as possible, as this site chain works by “first come – first served” principle.

Multisite

After rendering host gets recreated (it may take a while for both build and spinning up steps), check the site’s definition at .\src\temp\config.js again. It should look as below:

config.sites = process.env.SITES || '[{"name":"second","hostName":"second.localhost","language":""},{"name":"main","hostName":"main.localhost","language":""}]'

The amount of SXA client site records must match the records from SXA Site Manager. Now, running second.localhost in the browser should show you rendered home page of the second site.

Another technique of troubleshooting is to inspect middleware logs. To do so, create .env.local file at the rendering host app root (if does not exist yet) and add the debugging parameter:

DEBUG=sitecore-jss:multisite

Now, rendering host container logs will expose you the insights into how multisite middleware processes your requests and resolves site contexts, and sets site cookies. Below is a sample (and correct) output of the log:

sitecore-jss:multisite multisite middleware start: { pathname: '/', language: 'en', hostname: 'second.localhost' } +8h
sitecore-jss:multisite multisite middleware end in 7ms: {
  rewritePath: '/_site_second/',
  siteName: 'second',
  headers: {
  set-cookie: 'sc_site=second; Path=/',
  x-middleware-rewrite: 'https://localhost:3000/_site_second',
  x-sc-rewrite: '/_site_second/'
},
  cookies: ResponseCookies {"sc_site":{"name":"sc_site","value":"second","path":"/"}}
} +7ms

The above log is crucial to understanding how multisite middleware works internally. The internal request comes rewrites as <https://localhost:3000/_site_second where ‘second‘ is a tokenized site name parameter. If .\src\main\src\temp\config.js file contains the corresponding site record, site gets resolved and sc_site cookie is set.

If you still have the issues of showing up the Second website that resolves to Main website despite the multisite middleware log outputs correct site resolution, that may be likely caused by conflicting with other middleware processors. This would be your number one thing to check especially if you have multiple custom middleware. Miltisie middleware is especially sensitive to the execution order, as it sets cookies. In my case, that problem was because Sitecore Personalize Engage SDK was registered through middleware and also programmed to set its own cookie, and somehow configured with multisite middleware.

In that case, you have to play with order constant within each conflicting middleware (they are all located under .\src\lib\middleware\plugins folder) with the following restart of a rendering host.

Resources Sharing

Since the multisite add-on leverages the same rendering host app for the multiple sites that use it, all the components and layouts TSX markups, middleware, and the rest of the resources become automatically available for the second site. However, that is not true by default for the Sitecore resources. We must assign at least one website that will share its assets, such as renderings, partials, layouts, etc across the entire Site Collection. Luckily, that is pretty easy to do at the site collection level:

Site Collection

For the sake of an experiment, let’s make Second website to use Page Designs from the Main site. After the above sharing permission, they are automatically available at /sitecore/content/Site Collection/Second/Presentation/Page Designs item.

Configuring the cloud

Once we make the multisite add-on function well, let’s make the same work in the cloud.

  1. First of all, check all the changes to the code repository, and trigger the deployment, either from Deploy App, CLI, or your preferred CI/CD
  2. After your changes arrive at XM Cloud environment – let’s bring the required changes to Vercel, starting with defining the hostnames for the sites:Vercel
  3. After you define the hostnames in Vercel, change the hostname under Site Grouping item for this particular cloud environment to match the above hostname configured earlier in Vercel.
  4. Save changes and smart publish Site Collection. Publishing takes some time, so please stay patient.
  5. Finally, you must also do Vercel full re-deployment, to force regenerating \src\temp\config.js file with the hostnames from Experience Edge published at the previous step

Testing

If everything is done right in the previous steps, you can access the published Second website from Vercel by its hostname, in our case that would be https://dev-second.vercel.app. Make sure all the shared partial layouts are seen as expected.

When testing 404 and 500 you will see that these are shared between both sites automatically, but you cannot configure them individually per site. This occurs because both are located at .\src\pages\404.tsx and .\src\pages\500.tsx of the same rendering host app and use different internal mechanics rather than generic content served from Sitecore throughout .\src\pages\[[...path]].tsx route. These error pages however could be multi-lingual, if needed.

Hope you find this article useful!

Using conditions with XM Cloud Form Builder

If you follow the release news from Sitecore, you’ve already noted the release of highly awaited XM Cloud Forms, which however did not have a feature of adding conditional logic. Until now.

The good news is that now we can do it, and here’s how.

See it in action

Imagine you have a registration form and want to ask if your clients want to receive the email. To do so you add two additional fields at the bottom:

  • a checkbox for users to define if they want to receive these emails
  • a dedicated email input field for leaving the email address

Obviously, you want to validate the email address input as normal and make it required. At the same time you want this field to only play on with a checkbook being checked, otherwise being ignored and ideally hidden.

Once we set both Required and Hidden properties, there is a hint appears saying that we cannot have them both together as it simply creates a deadlock – something I mentioned in my earlier review of XM Cloud Forms builder

01

So, how we achieve that?

From now on, there is an additional Logic tab that you can leverage to add some additional logic to your forms.

02

Let’s see what can do with it:

  • you can add new pieces of logic
  • apply multiple conditions within the piece of logic and define if all must comply or just any of them (logical “AND” and “OR”)
  • you can create groups of conditions to combine multiple OR logic clauses to work against the same single AND condition
  • for each condition, select a particular field of application from a dropdown
  • define what requirements you want to meet with it from another dropdown, which is content-specific:
    • strict match
    • begins or ends with
    • contains
    • checked / unchecked
    • etc.
  • not just having multiple conditional logic, you can also have multiple fields logic within a single condition with the same “and / “or”

Once all conditions are met, you execute the desired action against a specified field:

04

Coming back to a form with an optional email subscription defined by a checkbox, I created the following rule:

05

The conditional logic rules engine is very much intuitive and human-readable. I do not even need to explain the above screenshot as it is naturally self-explanatory. So, how does it perform? Let’s run Preview and see it in action.

When running the form as normal, we only see a checkbox disabled by default and can submit it straight away:

06

But checking Email me updates and promotions tick enables Email field, which is Required. The form won’t submit with an invalid email address until the checkbox is checked.

07

My expectation for the conditional logic was applying conditions for multipage forms at the page level. Say, I have a first page having some sort of branching condition, so that if the user meets it – it gets to pages 2 and 3, but if not – only page 4, with both branching ending up at page 5. Unfortunately, I did not find a way of doing that. Hopefully, the development teams will add this in the future, given how progressively and persistently they introduce new features. In any case, what we’ve been given today is already a very powerful tool that allows us to create really complicated steams of input user data.

XM Cloud Forms Builder

XM Cloud Forms Builder released

Composable Forms Builder is now available with Sitecore XM Cloud. Let’s take a look at this one of the most anticipated modules for Sitecore’s flagship hybrid headless cloud platform.

Historically, we had an external module called Web Forms for Marketers that one could install on top of their Sitecore instance in order to gain the desired functionality of collecting user input. This module was later well reconsidered and reworked, later finding its reincarnation as Sitecore Forms, an integral part of Sitecore platforms since version 9. Customers enjoyed this built-in solution provided along with their primary DXP, however with the headless architecture of XM Cloud there were no CD servers any longer, therefore no suitable place for saving the collected user input. There was clearly a need for a SaaS forms solution, and this gap if finally filled out!

An interesting fact: until the release of Forms with XM Cloud the relevant composable solution for interacting with the visitors was Sitecore Send, and because of that Sitecore logically decided to derive XM Cloud Form module from Sitecore Send codebase (as it already had plenty of the desired features), rather than from legacy Sitecore Forms.

Sitecore XM Cloud Forms

So, what we’ve got?

The main goal was to release a new Forms product as SaaS solution that integrates with any custom web front-end. The actual challenge was to combine the ultimate simplicity of creating and publishing forms for the majority of marketing professionals along with tailoring this offering optimized for typical headless projects. In my opinion, despite the complexities, it was well achieved!

Let’s first take a look at its desired/expected capabilities:

  • Template Library
  • Work with Components Builder
  • Use external datasources for pre-populating form
  • Reporting and analytics
  • Ability to create multi-step and multi-page forms
  • Conditional logic (not available yet)

One would ask, if there’s no CD server or any managed backend at all, where does the submission go into? There might be some SaaS-provided storage along with the required interface to manage the collected input. Incorrect! There’s none. It was actually a smart move by Sitecore developers who decided to kill two birds with one stone: save effort for not building a universal UI/UX with the tool that will hardly satisfy variable needs from such a diverse range of customers/industries, that would be hardly doable. But the second reason is more legit: avoid storing any Personally Identifiable Information data, so that it won’t be processed within XM Cloud, leaving particular implementation decisions to leave to customers’ discretion.

That is done in a very composable SaaS manner, offering you to configure a webhook, passing a payload of collected data to the desired system of your choice.

Webhooks

Upon the form submission, the webhook is triggered to submit the gathered data to the configured system, it could be a database, CRM, CDP, or whichever backend is for some form. Even more, you can have shared webhooks so that multiple forms can use the same configured webhook. Similarly to the legacy forms that submitted their data into xDB, the most logical choice would be using powerful Sitecore CDP for processing this data. However with webhooks, the use case of XM Cloud forms becomes truly universal, and if you combine it with Sitecore Connect – that could span to whichever integration gets provided by it.
Webhooks come with multiple authentication options, covering any potential backend requirement.

Let’s see it in action!

The editor looks and feels like XM Cloud Pages – similar styling, layout, and UI elements:
02 03

First, let’s pick up the layout by simply dragging and dropping it on a canvas. For simplicity, I selected Full Width Layout. Once there, you can start dropping fields to a chosen layout:

04
Available Fields:
  • Action Button
  • Email
  • Phone
  • Short Text
  • Long Text
  • Select (single dropdown)
  • Multi Select (where you can define the number of options, say selected 3 of 9)
  • Date
  • Number
  • Radio
  • Checkbox (single)
  • Checkbox Group
  • Terms & Conditions
  • reCAPTCHA
Besides input-capturing elements, you can also define “passive” UI elements from underneath Basic expander:
  • Text
  • Spacer
  • Social Media – a set of clickable buttons to socials you define
  • Image, which has a pretty strong set of source options:
Media Upload options
Look at the variety of configuration options on the right panel. You can define:
  • Background Color within that field – transparent is the default one. You can even put a background image instead!
  • Settings for the field box shadows which also define Horizontal and Vertical Lengths, Blur and Spread Radiuses, and of course – the shadow color
  • Help text that is shown below and prompts some unobvious guidance you’d like a user to follow
  • For text boxes, you can set placeholder and prefill values
  • The field could be made mandatory and/or hidden by the correspondent checkboxes
  • Validation is controlled by a regex pattern and character length limit
  • Additionally, you can style pretty everything: field itself, label, placeholder, and help text, as well as set the overall padding to it

Please note, that at the current stage, the edited form is in a Draft state. Clicking Save button suggests you run your form in a Preview before saving, and that was very helpful – in my case, I left the Full Name field as a hidden field by mistake, and preview mode immediately showed me that. After fixing the visibility, I am good to go with saving.

The Forms home screen shows all the available forms. To Activate, I need to create a Webhook first, then assign it to the form. In addition, you define the action you want to do upon webhook submission – redirect to URL, display success message, or maybe – do nothing, as well as configure failure submission message.

This time Activate button works well and my form is listed as Active. From now on you cannot edit fields anymore and you cannot change the status back from Active. Therefore always verify your form in Preview before publishing.

Weirdly, you cannot even delete a form in Active status. What you can do however is a duplicate active form into a draft one, and you could go on with fields editing from there.

Forms listing

Testing Form

The most obvious desire at this stage is to real-test your form before usage. And luckily developers took care of that as well.

Testing webhook
I also created a webhook catcher with Pipedream RequestBin. On my first go, I faced a deadlock being unable to submit the form for the test. The reason for this was that I mistakenly checked both the Hidden and Required checkboxes on a field and could bo progress from there. Even the validation message did not show on a hidden field. Another mistake was that I overlooked that on Preview and published form into the active state. Hopefully, developers will find a solution to it sooner rather than later.

I give it another try to test how validation works:

Validation in action

Once validation passes, Test Form Submission dialog shows you the JSON payload as it goes out along with HTTP headers supplied with this webhook request. Let’s hit Submit button and see the confirmation – I chose to see a confirmation message that shows up.

Webhook catcher shows all my submitted data along with HTTP headers, everything was sent and received successfully!

webhook catcher

Multipage Forms

Multipage Forms are supported and that is impressive. Pay attention to the Add Page link button in between page canvases. Once activated, it also enforces Next button on non-final page canvases that trigger switching to the next form page:
20

What’s Next? Pages Editor!

Let’s use this newly created form from XM Cloud pages. Please note a new section called Forms under the Components tab. That is where all of your active forms reside. You can simply drag-drop this form to a desired placeholder, as you normally do in the Pages editor.

Consume Forms from Pages Editor

Please note: you must have your site deployed to an editing host running on Headless (JSS) SDK version 21.6 or newer to make it work – that is when XM Cloud Forms support was added. In other case, you face this error:

BYOC error before SDK 21.6

Experience Editor and Components Builder

Surprisingly, created forms are available from Components Builder:
Forms in Components Builder
However, Experience Editor does not have a direct way of consuming XM Cloud Forms. I tried the following chain of steps in order to make it work:
  1. Create and Activate a new form from Forms editor
  2. Consume it from the Components builder into a new component using BYOC, then publish this component
  3. Open Pages app, find the component with an embedded form you’ve built at step (2) and drop it to a page, then publish
  4. Open that same page in Experience Editor

Live Demo in Action

As you know, often a video is worth a thousand words, so here it is below. I’ve recorded the whole walkthrough from explaining to showcasing it all in action up to еhe most extreme example – when you create and publish a form, then consume it from the XM Cloud Components builder, making the part of a composite component, which in turn is used Pages editor to put down on a page which also opens up successfully in the Experience Editor. Unbelievable, and it all functions well. Just take a look yourself:

Developers Experience

As developers, how would we integrate forms into our “head” applications? That should work with a Forms BYOC component for your Next.js App coming out of the box with SDK. I spotted some traces of XM Cloud forms a while ago as a part of Headless JSS SDK 21.6.0 a while ago when it was in a state of “Canary” build. Now it got released and among the features, one can see an import of SitecoreForm component into the sample next.js app, as part of pull request merged into this release.

The documentation is available here, but … everything is so absolutely intuitive, that you hardly need one, don’t you?

Template Library

It is worth mentioning that XM Cloud Forms contains a Template Library handful of pre-configured forms you can use straight away or slightly modify as per your needs. There is an expectation it will grow with time covering any potential scenario one could ever have.
Template Library

Licensing

Since Forms are bundled into XM Cloud they’re included with every XM Cloud subscription.

What is missing?

  • file upload feature is not supported – webhooks alone are not sufficient to handle it
  • ability for customization and extension – hopefully, it comes as there’s an empty section for custom fields

Hopefully, the product developers will implement these and more features in the upcoming releases. But even with what was released today, I really enjoyed XM Cloud Forms builder!