Experience Sitecore ! | More than 300 articles about the best DXP by Martin Miles

Experience Sitecore !

More than 300 articles about the best DXP by Martin Miles

SUGCON Europe 2025 Takeaways

SUGCON Europe 2025 in Antwerp, Belgium first week of April was packed with energy insightful sessions, inspiring community content, and the always-welcome hallway chats and late-night bar banter that make these events unforgettable. There were plenty of important announcements from Sitecore’s keynotes and sessions. These updates carry serious weight for future work with Sitecore platforms - and they shouldn’t be overlooked.

Let's take a look at them one be one.

As per Dave O’Flanagan, the CEO of Sitecore, Sitecore Stream is a capability layer rather than a specific product. It orchestrates the life behind every AI-powered product in Sitecore and facilitates content creation, optimization, and personalization across both traditional and cloud-native setups. Its purpose is to streamline workflows through intelligent automation and suggestions, all while remaining flexible for various implementation needs. Sitecore Stream has become an important collaboration tool and even beyond. For example, they are currently experimenting with MCP (Model Context Protocol) integration that standardizes two-way communication between AI assistants and external apps, tools, and data sources. As artificial intelligence became central for Sitecore, the company was called an AI-first organization for the first time ever. Looking at the velocity of new features and changes, we believe that statement.

Besides the AI investments, Sitecore announces strategiс partnerships further ahead:

  • both Vercel and Netlify provide premium hosting services for the headless application, the best in their breed
  • CI Hub is industry-standard in-app connectivity software, that brings further more composable integrations and connections
  • Microsoft, as an existing partner, expands with innovations and collaborations over AI and Azure services
  • TransPerfect is an ultimate translation service that supports all the languages globally and has the support for Sitecore XM Cloud

Sitecore XM/XP version 10.5 is around the corner, expected to be released somewhere around Symposium 2025, and brings lots of features, among them:

  • even more AI integrations (as we know Stream already supports self-hosted platforms starting from version 10.2) with deeper content interactions, extended branding support via chat and tools, etc.
  • wider integration with other compostable products
  • continued stability and support

Sitecore DXPs remain crucial products for Sitecore and will gain new features. Thanks to Stream, the AI adoption is within reach without the need for a cloud migration, so that existing projects can begin leveraging AI features and gaining value immediately.

In order to have Stream operable on self-hosted/PaaS platforms you must have access to the Sitecore Cloud Portal, plus a Stream subscription, that's in addition to the XM/XP license you already have. There is Sitecore.AiClient.config file that defines the connection string, and brand kit id, along with the reference path. Currently, the only capability offered for non-cloud DXPs is brand-aware content generation, where Stream suggests versions aligned with the brand’s tone and structure. Multiple variations can be created, allowing editors to choose the most appropriate one directly from the Content Editor. Here's the roadmap:

Sitecore Tream roadmap

Sitecore Marketplace was the first ever announced publically as the first milestone was reached. It takes the strongest power of the Sitecore ecosystem back to track - the community with its crowdsourcing effort at a scale. Marketplace will let developers create either internal apps to circulate exclusively within their organizations or release public apps on free, freemium, or paid models, subject to pre-publishing moderation by Sitecore.

Currently, Marketplace only supports XM Cloud and is available with the Early Access Program. Developers can rely on specified "touchpoints" in the portal and Pages Builder app for the extension, whether it is a Cloud Portal App, Navigation or context panel integration, or even a field-level extension, such as a custom field. There is also an SDK that helps build marketplace apps. With further milestones, it becomes publicly available and will expand to the rest of SaaS products, such as Content Hub, Personalize, and others.

Sitecore always used to be a content-centric vendor and this time they introduced XM Cloud Content which is based on the learnings from Content Hub One and represents probably the most significant shift. This new backend will ultimately serve as the foundation for storing and delivering all content within XM Cloud, purely SaaS. Once live, this architectural evolution will make it possible to phase out the traditional "XM" layer and its underlying Docker-based services entirely.

XM Cloud Content lets us get rid of:

  • the hierarchical tree-like data structure into a more flexible relation-based data organization
  • all the publishing issues, with immediate publishing by changing the "published" flag
  • inheritance-based model, in favor of composition, offering better flexibility for content modeling from "fragments of schema"
  • legacy versioning embedded into a content item. The new Content system allows "external" versioning since once it gets published content becomes immutable

We've been shown the new system in action, from content structure modeling to consumption, and what was impressive - the immediate system responsiveness. Content consumption is rapid and works through with GraphQL, more to say the system suggests the query structure, making it even faster.

There also was announced new Content SDK for XM Cloud, but it has nothing to do with the above XM Cloud Content, so please do not be confused.

Content SDK is effectively a slim subset of JSS SDK exclusively for modern XM Cloud development, without having legacy code for rudimental features, that do not exist in XM Cloud (XP Forms, REST API content consumption, chrome editing mode, etc). In addition, the overall size and complexity was drastically reduced, namely:
- less included packages: 6 now against 14 as in JSS SDK
- less files: 65% reduction!
- less code: 54% of unused code has been removed
- faster page load: counted as 200ms
- overall size reduction by a shocking 89%

JSS to Content SDK

The best part is that XM Cloud Content SDK is already available along with its source code. As for the original JSS, it won't go away and will stay developed further for supporting Sitecore XM and XP implementations.

Worth mentioning large improvements to the XM Cloud Starter Kits with exciting expectations of the App Router introduction along with optimization of middleware.

Last but not least, there were significant documentation improvements. Given the overwhelming number of headless SDKs (JSS, Content, .NET Core, etc), the documentation required some order and consistency, which is now achieved (proof).

Separately worth mentioning Content Hub changes. If you occasionally monitor Sitecore's changelog (that's where the company highlights all the new features and fixes), you may have noticed that Content Hub announcements disproportionately dominate that page - there were plenty of recent additions and improvements into CLI/SDKs/APIs of Content Hub. Along with media delivery and Experience Edge enhancements, these changes are massive!

Due to its nature, Content Hub appears to be the best beneficiary of AI and Sitecore Stream introduction. It also essentially benefits from the introduction of Marketplace:

Sitecore’s Content Hub is evolving—moving from trusted integrations to flexible, composable extensibility, unlocking:

This SUGCON event has sent a clear message: Sitecore is evolving, and doing it rapidly. It hears back from the community, learns from the mistakes, both own and competitors', and leverages its own strength. The future seems to be bright and we're almost there.

XM Cloud: Beyond the Serialization

Distinguish Between Definition and Content Items

In XM Cloud, serialization is primarily a developer/ops concern, not a task for content authors. Developers and DevOps engineers use the Sitecore CLI to manage definition items (templates, layouts, SXA settings, etc.) in version control, whereas content authors continue to work in the Content Editor/Experience Editor and publish content via the usual publishing pipeline.  In contrast, content authors simply publish content; they shouldn’t be expected to run CLI commands: we reserve the CLI workflow for dev-defined items, and let content authors handle content via Sitecore’s authoring tools.

In XM Cloud, all templates and configurations are the definition items in the Sitecore tree - many of these are delivered as items-as-resources in protobuff resource files. By default, XM Cloud provides a base set of definition items in resource packages; custom definitions (such as your templates, renderings, layouts, SXA component definitions) should be added via serialization. The CLI can push/pull any item in the tree, so treat your custom definitions like any other serialized item. For example, you would include custom layouts (under /sitecore/Layout/Layouts/YourTenant), rendering definitions (/sitecore/Layout/Renderings/YourTenant), SXA rendering variants, themes, and placeholder settings in your modules. In fact, the Sitecore docs note that SCS modules allow you to "organize and separate serialized items according to their purpose".

You generally do not manually transfer definitions by hand; instead, you define them in your project and let the CLI handle serialization. As a special tool, Sitecore provides an Items as Resources CLI plugin (dotnet sitecore itemres) to package definition items into *.DAT resource files, but in most cases, you simply let sitecore ser push pack the YAML and deploy it to XM Cloud.

Note, that some environment-level settings are not items. For example, custom Sitecore security domains live in a config file on the server, not in the content tree, so they cannot be serialized or deployed via CLI. In those cases, you must handle them outside of SCS.

In summary: treat definition items as code: include them in your serialization modules and commit them, and let the Sitecore CLI bundle them into the XM Cloud resource IAR package. Content items, by contrast, should be managed by content workflows.

Content Serialization

Generally, don’t serialize day-to-day content. XM Cloud follows the old wisdom of treating content differently from code: the CLI is not intended as a general content migration tool. Instead, content authors create or edit content items in the CM interface or Pages Builder and then publish them to delivery targets. Serializing content will lead to conflicts and source-control bloat.

That said, there are a few exceptions where you might include minimal content stubs or settings in serialization. For example, it’s common to serialize the homepage item itself so that a brand-new environment has the correct site entry point immediately upon first serialization. You'd want to explicitly include the tenant and site root with CreateAndUpdate operations - this ensures that new environments get a placeholder homepage item (and updates propagate), without overwriting content. You might also serialize certain settings or dictionary items that are truly "configuration" (say, a language items folder or a set of shared data templates). But actual content (articles, product entries, news items) should be kept out of serialization.

In practice, restrict content serialization to very small, static subsets (often at most the single site-root node) and mark them as create-only or create-and-update. For example:

{
  "name" : "TenantA_SiteRoot",
  "path" : "/sitecore/content/TenantA/SiteA,"
  "scope": "singleItem",
  "allowedPushOperations" : "CreateUpdateAndDelete"
}

This would ensure the /SiteA item exists in all environments, but leave its children alone. The rest of /sitecore/content/TenantA/SiteA descendants (actual page content) would not be included.

Remember that after you push any content items, you still must publish them to make them live. The ser push command only writes into the CM database; it does not publish to Experience Edge. To serve content, you must run a publish command (dotnet sitecore publish --target Edge).

Summarizing: Limit content serialization to definition items with some exceptions like an empty homepage or settings, and let normal publishing handle real content.

Also, "do not include vanilla XM Cloud items – include only custom-created items". Content changes by authors should flow through Sitecore’s normal publishing workflows, not through SCS commits.

Serialization Scopes

Sitecore CLI modules use scopes to control how deeply an include or rule applies. The scope property can be one of:

  • SingleItem – only the specified item itself (no children).

  • ItemAndChildren – the item plus its immediate (one-level) children.

  • ItemAndDescendants – the item and all levels of descendants - the full subtree. That is a default value.

  • DescendantsOnly – all descendants of the item, but not the item itself.

  • Ignored – skips this branch entirely, used in rules to exclude subtrees.

For example, suppose you include a path /sitecore/content/TenantA/SiteA in a module. If not specified, that is ItemAndDescendants by default, so it would pull the entire site tree. If you want only the root item and none of its children, you would add a rule with "scope": "SingleItem". Conversely, to skip an unwanted subfolder, you could use "scope": "Ignored" on that path.

A concrete example of a rules section might look like:

"items": {
  "includes": [
    {
      "name": "SiteA",
      "path": "/sitecore/content/TenantA/SiteA",
      "allowedPushOperations": "CreateUpdateAndDelete",
      "rules": [
        { "path": "/sitecore/content/TenantA/SiteA/Data", "scope": "Ignored" },
        { "path": "/sitecore/content/TenantA/SiteA/Home", "scope": "Ignored" },
      ]
    }
  ]

This says "include the SiteA item (with all descendants by default) but ignore the entire Data and home Page subfolders." In practice, you’ll define scopes to precisely include what you need: broad (ItemAndDescendants) for full branches, and narrow (SingleItem or Ignored) to exclude or limit depth.

Serialization Order and Modules

Since you organize your serialized items into modules within each .module.json file in the Sitecore CLI, you must somehow define which modules depend on stuff from others. This each module can have an optional references list to other modules. The CLI uses these references to enforce a load order. For example, if you have a Foundation module with templates (Foundation.CoreTemplates) and a Feature module that depends on them (Feature.Shop), you would write:

{
  "namespace": "Feature.Shop",
  "references": [ "Foundation.CoreTemplates" ],
  "items": {
    "includes": [
      { "name": "ProductTemplates", "path": "/sitecore/templates/Feature/Shop", "allowedPushOperations": "CreateUpdateAndDelete" }
      // ...
    ]
  }
}

By specifying "references": [ "Foundation.CoreTemplates" ], you ensure the CLI pushes/pulls Foundation templates first, then the shop items. Wildcards are allowed ( "Foundation.*") to reference all Foundation modules. If you omit references, the CLI may process modules in alphabetical or file order, which can lead to missing-dependency errors, such as pushing a rendering before its template exists.

In practice, follow Helix conventions: have a base module for core templates/branch templates, then project-level modules that reference it and use the references array in each module to declare dependencies.

Alternatively, you can also tag modules in CLI commands (with -i) to run only subsets, but the fundamental sequencing comes from references. If dependencies are missing or misordered, you will see errors during push (like “item not found” or unresolved references). Always review dotnet sitecore ser info to verify your module graph.

SXA Serialization Considerations

When using SXA in XM Cloud, treat SXA definition items as code. As usual, you should serialize your SXA component definitions and site structure, but not your actual content. Key SXA assets to include are:

  • Rendering Variants and Templates: Anything under /sitecore/layout/Rendering Variants/... and the SXA Page/Partial designs under /sitecore/layout/Layouts/... or /sitecore/content/TenantName/SiteA/Presentation/....

  • Placeholder Settings and Layouts: SXA uses placeholders and page layouts, which live under /sitecore/Layout or under the site; include those as part of layouts and placeholders sections.

  • Themes and Styles: If your SXA site uses a custom theme (under /sitecore/media library/Themes or the SXA Theme item), serialize that.

  • SXA Site Settings: Under each site (e.g. /sitecore/content/Tenant/SiteA/Settings), include any site-level settings items that define colors, etc., if they are required for deployment.

  • Site Templates: SXA allows site templates in the dashboard; if you have custom site templates, serialize those under /sitecore/templates/Project/....

Do not serialize authored page content (articles, products, etc.) from SXA sites. Also consider shared vs. site-specific: if multiple sites under one tenant share variants or partial designs, put them in a shared module (under the tenant node). If a design is only for SiteA, put it under the SiteA module. In general, follow the same include rules as above, putting SXA assets under your project/feature paths. For example, include renderings and placeholder settings under /sitecore/Layout/Renderings/Project/... and /sitecore/Layout/Placeholder Settings/Project/... - these would cover your custom SXA renderings and placeholder definitions as well. By version-controlling SXA assets in the CLI, you ensure that your site designs, variants and styles are consistent across environments, while leaving content (pages and datasources) to the content pipeline.

Module Organization

For the multisite and especially multi-tenant data architecture, it becomes crucial to organize your modules and serialization in a totally isolated way. Thus, everything becomes self-contained so that removing one site/ tenant folder does not affect the rest of the serialized assets. To achieve that, I would recommend doing this:

  • Organize all the assets under a specific folder, for example: authoring/items/client
  • All the relevant modules fall into this folder. I usually split them into three groups: client.global.module.json; client.components.module.json and client.site.module.json
  • Each of these module files must include a path directive, which effectively defines a subfolders structure: "path": "components" or "path": "site". This is the most crucial bit.
  • Nothing else exists immediately under authoring\items folder other than clients and areas; typically there would be a global folder followed by client1, client2, etc.

I would also recommend reading this article, which I found to be helpful. Hope you find these tips helpful!

 

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:

 

SQL-level access to database in XM Cloud

Could you ever guess that you can still access databases in XM Cloud directly at SQL level? Yes, you can, and below I am showing how exactly.

Disclaimer
The step‑by‑step SQL‑level techniques described in this post are provided strictly for educational purposes. Directly querying or modifying the underlying database of Sitecore XM Cloud is strongly discouraged, as it bypasses critical application‑level safeguards, voids support agreements, and may lead to data corruption, security vulnerabilities, or service outages. Always interact with your XM Cloud instance through the officially supported APIs, Sitecore CLI, or designated administration tools. Proceeding with direct database access is done entirely at your own risk, and the author and any affiliated parties disclaim all liability for any loss, damage, or disruption resulting from such actions.

The key trick here goes from using Sitecore PowerShell Extensions, if you have them enabled at your instance, you're good to go! This applies to both cloud and locally containerized databases. PowerShell has a useful commandlet Invoke-SqlCommand that allows making SQL-level connections and accepts two parameters: $connection and $query:

Invoke-SqlCommand -Connection $connection -Query $query 

But how exactly do we get a connection? Luckily, since we operate from within ASP.NET application, we can use its own API to get it, no even needing to look up the connection strings config:

$connection = [Sitecore.Configuration.Settings]::GetConnectionString("master")

The other thing is that we must know the physical name of the database to operate against. Once again, we can calculate that without even a lookup into configs:

$builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder $connection
$dbName = $builder.InitialCatalog

Please note that on a local, your database name is always Sitecore.Master, while on a cloud instance, it is very long, combined from the org, project, and environment names, thus always varies from environment to environment, so you have to calculate it like that.

Next, let's build a query. To start with we can do a basic select for the items availabe. Having a physical database name, we can pass it into a query as below:

$sql = @"
USE [{0}]
SELECT ID, [Name], [TemplateID], Created from  [dbo].[Items]
"@

With that in mind, let's combine everything together into a single script:

$sql = @"
USE [{0}]
SELECT ID, [Name], [TemplateID], Created from  [dbo].[Items]
"@

Import-Function Invoke-SqlCommand

Write-Verbose "Cleaning up the History, EventQueue, and PublishQueue tables in the $($db.Name) database."
$connection = [Sitecore.Configuration.Settings]::GetConnectionString("master")
$builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder $connection
$dbName = $builder.InitialCatalog
$query = [string]::Format($sql, $dbName)

Invoke-SqlCommand -Connection $connection -Query $query 

Running it brings the desired result:


But what is especially amazing is that you also have write access to master database! With that in mind, I create a query that creates a new item under a specific location and also populates some of its fields. It looks slightly more complicated, but again, nothing extraordinary:

$sql = @"
USE [Sitecore.Master];

-- Variables
DECLARE @NewItemId UNIQUEIDENTIFIER = NEWID();
DECLARE @ParentId UNIQUEIDENTIFIER = '{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}';
DECLARE @TemplateId UNIQUEIDENTIFIER = '{76036F5E-CBCE-46D1-AF0A-4143F9B557AA}'; -- Sample Item template
DECLARE @TextFieldId UNIQUEIDENTIFIER = '{A60ACD61-A6DB-4182-8329-C957982CEC74}'; -- Text field ID
DECLARE @Now DATETIME = GETUTCDATE();
DECLARE @ItemName NVARCHAR(255) = 'SQL-inserted item';
DECLARE @Language NVARCHAR(10) = 'en';
DECLARE @Version INT = 1;
DECLARE @TextValue NVARCHAR(MAX) = 'This item was created entirely with SQL INSERT script';

-- 1. Insert into Items
INSERT INTO [dbo].[Items] 
    ([ID], [Name], [TemplateID], [ParentID], [MasterID], [Created], [Updated])
VALUES 
    (@NewItemId, @ItemName, @TemplateId, @ParentId, '00000000-0000-0000-0000-000000000000', @Now, @Now);

-- 2. Insert into VersionedFields (CORRECT field ID now)
INSERT INTO [dbo].[VersionedFields]
    ([ItemId], [FieldId], [Language], [Version], [Value], [Created], [Updated])
VALUES
    (@NewItemId, @TextFieldId, @Language, @Version, @TextValue, @Now, @Now);
"@

Import-Function Invoke-SqlCommand

Write-Verbose "Cleaning up the History, EventQueue, and PublishQueue tables in the $($db.Name) database."
$connection = [Sitecore.Configuration.Settings]::GetConnectionString("master")
$builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder $connection
$dbName = $builder.InitialCatalog
#$query = [string]::Format($sql, $dbName)
$query = $sql

Invoke-SqlCommand -Connection $connection -Query $query 

Upon the execution, I saw no changes in Content Tree, which likely happens due to some CM caches in place. For the sake of clarity, I restarted CM and got this newly created item with all the fields, as expected:

Note, that $name token from standard vales does not expand here - this gets done by Sitecore API upon item creation logic, while we directly inserted into database bypassing it.

That approach is really awesome, despite feeling so hacky! Imagine combining it with the SPE Remoting and/or combining that with AI - it brings unlimited potential. 

But once again, it all goes for educational purposes exclusively.

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

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

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

Why?

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

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

How?

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

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

Cloud build-time XDT transform: officially recommended approach

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

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

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

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

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

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

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

Local XDT Transformation

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

There are two main options:

1. Dockerfile build-time transform

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

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

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

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

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

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

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

2. Development-only runtime patches

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

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

Steps to achieve:

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

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

# escape=`

ARG BASE_IMAGE

FROM ${BASE_IMAGE}

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

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

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

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

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

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

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

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

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

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

Volume Overwrites

Avoid this approach!

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

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

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

Summary Comparison of Approaches

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

Uses SITECORE_DEVELOPMENT_PATCHES​.

Quick turnaround; no custom image.

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


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

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

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


Conclusion and recommendations

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

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

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

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

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

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

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

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

parameters


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

variants



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

Hope that clarifies the use cases and removes ambiguity.

Experience Edge: Know Your Limitations

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

API Rate Limits

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

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

GraphQL Query & Payload Constraints

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

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

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

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

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

Content & Delivery Constraints

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

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

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

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

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

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

Media Limits

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

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

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

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

Caching Rules & TTL

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

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

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

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

XM Cloud Platform Limits (Impacting Edge)

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

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

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

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

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

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

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

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

Hope knowing the above helps you plan better!

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.

Merry Christmas and happy New Year!

Every year I create a special Christmas postcard to congratulate my readers on a new oncoming year, full of changes and opportunities. Wish you all the best in 2025!

My artwork for the past years (click the label to expand)
2024


2023


2022


2021


2020


2019


2018


2017


2016