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

Blog content

So, don't miss out! Subscribe to this blog/twitter from the buttons above!

The Future is Now: Sitecore Content SDK 1.2 brings App Router into XM Cloud

For years, I’ve been navigating the ever-evolving landscape of Sitecore, and I’ve seen my fair share of game-changing updates. But let me tell you, the recent release of Sitecore Content SDK v1.2 feels different. This isn’t just another incremental update; it’s a fundamental shift in how we build for XM Cloud, and it’s bringing the power of the Next.js App Router to the forefront of Sitecore development. I’ve had the chance to dive deep into this release, and I’m thrilled to share my findings with you.

The Writing on the Wall: JSS, Content SDK, and the XM Cloud Trajectory

Let’s be honest: the transition to a composable DXP has been a journey. We’ve all been wondering about the future of JSS and how it fits into the XM Cloud picture. Well, Sitecore has made its stance crystal clear: the Content SDK is the only way to go for new XM Cloud projects.
Sitecore’s official support lifecycle KB article outlines everything.

Here’s the key takeaway:

JSS SDK 22.X will reach its end-of-life (EOL) in June 2026.
This means if you’re on JSS with XM Cloud, it’s time to start planning your upgrade. JSS will continue to be the SDK for on-premise XM/XP deployments, but for the cloud, the Content SDK is the designated path forward. This isn’t a bad thing - in fact, it’s a massive leap in the right direction.

Why the Content SDK is a Game-Changer

I’ve spent some time with the Content SDK, and I can tell you firsthand that it’s more than just a rebrand of JSS. It’s a leaner, meaner, and more focused toolkit designed specifically for the needs of XM Cloud. The community has been buzzing about this, and for good reason. As my friend at Fishtank puts it, the Content SDK is a major upgrade. Here’s a breakdown of the key differences:

Feature
JSS SDK
Content SDK
Supported Products
Sitecore XM/XP and XM Cloud
Exclusively for XM Cloud
Visual Editing
Experience Editor & XM Cloud Pages
XM Cloud Pages Builder only
Complexity & Size
Larger, more complex starter apps
Significantly smaller and simpler
Component Mapping
Automatic
Manual registration by default
Configuration
Scattered across multiple files
Centralized in sitecore.config.ts and sitecore.cli.config.ts
Data Fetching
Various data fetching plugins
Unified SitecoreClient class
Middleware
Separate middleware plugin files
Simplified with defineMiddleware in middleware.ts

Farewell, Experience Editor

One of the most significant changes is the removal of Experience Editor support. This might sound alarming at first, but it’s a strategic move. By focusing solely on the XM Cloud Pages Builder, the Content SDK sheds a tremendous amount of complexity. This results in a leaner codebase that’s easier to understand, maintain, and, most importantly, faster.

Enter the App Router: A New Era for Next.js in XM Cloud

The headline feature of Content SDK 1.2 is, without a doubt, the introduction of beta support for the Next.js App Router. This is a monumental step forward, aligning XM Cloud development with the latest and greatest from the Next.js ecosystem.
For those unfamiliar, the App Router is a new paradigm in Next.js that simplifies routing, data fetching, and component architecture. It introduces concepts such as server components, nested layouts, and streamlined data fetching, making the building of complex applications more intuitive than ever.
I’ve been exploring the xmcloud-starter-js repository, specifically the release/CSDK-1.2.0 branch, to see how all of this comes together.
Let’s take a look at some of the code.

The New App Router Structure

The folder structure itself tells a story. Gone is the pages directory, replaced by a more intuitive app directory:
src/
├── app/
│   ├── [site]/
│   │   ├── [locale]/
│   │   │   └── [[...path]]/
│   │   │       └── page.tsx
│   │   └── layout.tsx
│   ├── api/
│   ├── layout.tsx
│   ├── not-found.tsx
│   └── global-error.tsx
├── components/
├── lib/
└── middleware.ts
This structure is not only cleaner but also more powerful, allowing for features such as route groups and nested layouts that were previously cumbersome to implement in the Pages Router.

Data Fetching in Server Components

With the App Router, Server Components are the default. This means you can fetch data directly within your components without getServerSideProps or getStaticProps. Here’s a look at the new page.tsx:

// src/app/[site]/[locale]/[[...path]]/page.tsx

import { notFound } from 'next/navigation';
import { draftMode } from 'next/headers';
import client from 'src/lib/sitecore-client';
import Layout from 'src/Layout';

export default async function Page({ params, searchParams }: PageProps) {
  const { site, locale, path } = await params;
  const draft = await draftMode();

  let page;
  if (draft.isEnabled) {
    const editingParams = await searchParams;
    // ... handle preview and design library data
  } else {
    page = await client.getPage(path ?? [], { site, locale });
  }

  if (!page) {
    notFound();
  }

  return <Layout page={page} />;
}
Notice how clean and concise this is. We’re using async/await directly in our component, and Next.js handles the rest. This is a huge win for developer experience.

A New Approach to Layouts

The App Router also introduces a more powerful way to handle layouts. You can now create nested layouts that are automatically applied to child routes. Here’s a simplified example from the starter kit:
// src/app/[site]/layout.tsx

import { draftMode } from 'next/headers';
import Bootstrap from 'src/Bootstrap';

export default async function SiteLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ site: string }>;
}) {
  const { site } = await params;
  const { isEnabled } = await draftMode();

  return (
    <>
      <Bootstrap siteName={site} isPreviewMode={isEnabled} />
      {children}
    </>
  );
}

This SiteLayout component wraps all pages within a given site, providing a consistent structure and allowing for site-specific logic.

Migrating from JSS to the Content SDK: A Checklist

For those of us with existing JSS projects, the migration to the Content SDK is now a top priority. While Sitecore provides a comprehensive migration guide, here’s a high-level checklist to get you started:
  1. Update Dependencies: Replace your jss-nextjscode> packages with the new @sitecore-content-sdk/nextjs packages.
  2. Centralize Configuration: Create sitecore.config.ts and sitecore.cli.config.ts to consolidate your settings.
  3. Refactor Data Fetching: Replace your data fetching logic with the new SitecoreClient class.
  4. Update Middleware: Consolidate your middleware into a single middleware.ts file using the defineMiddleware utility.
  5. Register Components: Manually register your components in the .sitecore/component-map.ts file.
  6. Update CLI Commands: Replace your jss CLI scripts with the new sitecore-tools commands.
  7. Remove Experience Editor Code: Clean up any code related to the Experience Editor, as it’s no longer supported.

This is a non-trivial effort, but the benefits in terms of performance, maintainability, and developer experience are well worth it.

The Rise of AI in Sitecore Development

Another fascinating aspect of the Content SDK is its deep integration with AI-assisted development. The repository now includes guidance files for Claude, GitHub Copilot, and other AI tools. This is more than just a novelty; it’s a serious productivity booster. These guidance files instruct your AI assistant on Sitecore-specific conventions, ensuring that the code it generates adheres to best practices.

I’ve been experimenting with this, and the results are impressive. When I ask my AI assistant to create a new component, it knows to:
  • Use the correct naming conventions.
  • Create the necessary props interface.
  • Use the Sitecore field components (<Text>, <RichText>, etc.).
  • Handle missing fields gracefully.
This is a huge time-saver, ensuring that even junior developers can write high-quality, consistent code.

The Road Ahead

The release of Content SDK 1.2 and the introduction of the App Router mark a pivotal moment for Sitecore XM Cloud. It’s a clear signal that Sitecore is fully committed to modern, composable architecture and is dedicated to delivering a world-class developer experience.
For those of us who have been in the Sitecore trenches for years, this is an exciting time. We’re seeing the platform evolve in ways that will allow us to build faster, more robust, and more engaging digital experiences than ever before. The road ahead is bright, and I, for one, can’t wait to see what we build with these new tools.

References

The new and shiny Sitecore Marketplace

For years, the Sitecore community has been the platform's secret weapon. We've built countless modules, shared our knowledge at SUGCONs, and pushed the boundaries of what's possible with this DXP. Today, I'm thrilled to share my experience with the Sitecore Marketplace, which officially launched in 2025 and represents a fundamental shift in how we extend and customize Sitecore in the composable era.

I've had the privilege of participating in the Early Access Program and recently submitted a proposal for the Sitecore AI Challenge that made it to the finals. Today, I'm completing the final submission, and I can tell you firsthand: this is not just another feature release. The Marketplace is a strategic bet on the community, a multiplication of our collective effort, and a recognition that Sitecore's greatest strength has always been its people.

In this comprehensive guide, I'll walk you through everything you need to know to build production-ready Marketplace apps, from arch.


Why the Marketplace Matters: The Strategic Context

Let's be honest: the shift to SaaS has been challenging for many of us who built our careers on the extensibility of Sitecore XP. We could customize anything, extend everything, and build solutions that were uniquely tailored to our clients' needs. When XM Cloud launched, there was a legitimate concern: how do we maintain that level of customization in a managed SaaS environment?

The Marketplace is Sitecore's answer to that question. It's a way to preserve the extensibility we love while embracing the benefits of SaaS. More importantly, it's a recognition that the community is the engine of innovation. By providing us with the tools to build and share apps, Sitecore is effectively multiplying their own development capacity by the size of the community. That's brilliant.

The Developer Studio: Your Starting Point

Your journey begins in the Developer Studio, a new section in the Sitecore Cloud Portal. This is where you'll create and configure your apps. The interface is clean and intuitive, but there are some important decisions to make:

Creating Your App

When you create a new app, you'll need to provide:
  • App Name: This appears in the Marketplace and to end users.
  • Description: Make it clear and compelling.
  • Logo URL: This is mandatory. Your logo needs to be hosted somewhere accessible.
  • Extension Points: Where your app will appear in the Sitecore UI.
  • API Access: Which Sitecore APIs your app needs to call.
  • Deployment URL: Where your app is hosted.
It's important to note that you host your own apps. Sitecore provides the SDK and the integration points, but the hosting is entirely up to you. This gives you complete control over your technology stack, deployment pipeline, and scaling strategy. I'm hosting my AI Challenge app on Vercel, which has been seamless.

App Types: Custom vs. Public

Currently, you can build Custom Apps, which are available to your organization and any other organizations you explicitly authorize. Public Apps, which will be available to all Sitecore customers through the Marketplace, are coming in a later release. Public apps will undergo a quality review process, which is a good thing for the ecosystem.

Architecture: Making the Right Choice

The architecture you choose for your Marketplace app has significant implications for what you can build and how complex your development will be. There are two main patterns:

1. Client-Side Architecture

In a client-side architecture, your app runs entirely in the user's browser. All API requests to Sitecore APIs are proxied through the browser using the SDK. This is the simplest option and is ideal for:
  • UI extensions and custom fields
  • Dashboard widgets
  • Simple data visualization
  • Apps that don't require server-side processing
The key advantage is simplicity. You can build a client-side app with just React or Vue, deploy it as a static site, and you're done. The SDK handles all the authentication and communication with Sitecore automatically.

2. Full-Stack Architecture

A full-stack architecture includes both client-side and server-side components. This is necessary when you need to:
  • Make direct server-to-server API calls to Sitecore
  • Integrate with AI services (like OpenAI, Anthropic, etc.)
  • Perform heavy processing or data transformations
  • Implement agentic workflows
  • Store and manage your own data
For full-stack apps, you'll need to use custom authorization with Auth0. This is more complex to set up, but it gives you the flexibility to build sophisticated applications. My AI Challenge submission is a full-stack app because it integrates with multiple AI services and requires server-side orchestration.

3. Authorization: Built-in vs. Custom

The Marketplace SDK supports two authorization patterns:
  • Built-in Authorization: The SDK automatically manages authentication. This is the default and recommended option for client-side apps.
  • Custom Authorization (Auth0): Required for server-side API calls. You'll need to configure Auth0 credentials in the Developer Studio and handle session invalidation when your app is available in multiple organizations.
For your first app, I strongly recommend starting with client-side architecture and built-in authorization. Get something working, understand the SDK, and then move to full-stack if you need to.

Extension Points: Where Your App Lives

Extension points are the designated areas in the Sitecore UI where your app can be displayed. Understanding them is crucial to designing a great user experience.

1. Standalone (Cloud Portal)

Your app appears on the Cloud Portal home page in the "Marketplace apps" section. It opens in a new tab under the navigation header. This is ideal for apps that are used independently of XM Cloud, like reporting dashboards or administrative tools.
Image source: cloud portal app configuration screen

2. XM Cloud Full Screen

Your app is listed in the "Apps" dropdown in the XM Cloud navigation header and appears full screen. This is perfect for apps that need a lot of screen real estate, like content migration tools or complex configuration interfaces.
Image source: cloud portal app configuration screen

3. XM Cloud Page Builder Context Panel

This is one of my favorite extension points. Your app appears in the context panel to the left of the Page Builder canvas. This is ideal for page-level extensions, like:
  • Real-time analytics insights
  • SEO analysis tools
  • Content quality checkers
  • HTML validators
The context panel is always visible while editing a page, so your app is right there when users need it.

4. XM Cloud Page Builder Custom Field

Your app appears in a modal when a user clicks "Open app" on a custom field. This is perfect for:
  • Icon pickers
  • Color pickers
  • External data selectors
  • Media libraries
I've seen some brilliant implementations of this extension point, including a Material UI icon picker with over 10,000 icons.
Image source: cloud portal app configuration screen

5. XM Cloud Dashboard Widget

Your app can be added as a draggable widget to the XM Cloud site dashboard. This is ideal for displaying site-wide metrics, like bounce rates, popular pages, or performance data from third-party analytics tools.

The Marketplace SDK: A Deep Dive

The Sitecore Marketplace SDK is the foundation of all Marketplace development. It's open-source, well-documented, and a pleasure to work with. Let's dive into the details.

1. Package Structure

The SDK is divided into distinct packages:
  • @sitecore-marketplace-sdk/client: Required for all apps. Handles secure communication between your app and Sitecore.
  • @sitecore-marketplace-sdk/xmc: Optional. Provides type-safe interfaces for XM Cloud APIs, including queries, mutations, and subscriptions.

2. Getting Started: The 5-Minute Quickstart

Here's how you can get a basic Marketplace app running in about 5 minutes:

1. Create a new Next.js or Vite app:
npx create-next-app@latest my-marketplace-app
cd my-marketplace-app
2. Install the SDK packages:
npm install @sitecore-marketplace-sdk/client @sitecore-marketplace-sdk/xmc
3. Create a custom hook for the Marketplace client:
// hooks/useMarketplaceClient.ts
import { useEffect, useState } from 'react';
import { ClientSDK } from '@sitecore-marketplace-sdk/client';

export const useMarketplaceClient = () => {
    const [client, setClient] = useState<ClientSDK | null>(null);
    const [isInitialized, setIsInitialized] = useState(false);
    const [error, setError] = useState<Error | null>(null);

    useEffect(() => {
        const initClient = async () => {
            try {
                const sdkClient = new ClientSDK({
                    onError: (err) => {
                        console.error('SDK Error:', err);
                        setError(err);
                    }
                });

                await sdkClient.initialize();
                setClient(sdkClient);
                setIsInitialized(true);
            } catch (err) {
                setError(err as Error);
            }
        };

        initClient();
    }, []);

    return { client, isInitialized, error };
};
4. Use the client in your component:
// app/page.tsx
"use client"
import { useMarketplaceClient } from '@/hooks/useMarketplaceClient';
import { ApplicationContext } from '@sitecore-marketplace-sdk/client';
import { useState, useEffect } from 'react';

export default function Home() {
    const { client, error, isInitialized } = useMarketplaceClient();
    const [appContext, setAppContext] = useState<ApplicationContext | null>(null);

    useEffect(() => {
        if (isInitialized && client) {
            client.query('application.context')
                .then((res) => {
                    if (res.data) {
                        console.log('Application context:', res.data);
                        setAppContext(res.data);
                    }
                })
                .catch((err) => console.error('Query failed:', err));
        }
    }, [isInitialized, client]);

    if (error) {
        return <div>Error initializing SDK: {error.message}</div>;
    }

    if (!isInitialized) {
        return <div>Initializing Marketplace SDK...</div>;
    }

    return (
        <div>
            <h1>My Marketplace App</h1>
            {appContext && (
                <div>
                    <p>Organization: {appContext.organization.name}</p>
                    <p>User: {appContext.user.email}</p>
                </div>
            )}
        </div>
    );
}
That's it. You now have a working Marketplace app that can communicate with Sitecore.

3. Queries, Mutations, and Subscriptions

The SDK provides three main ways to interact with Sitecore APIs

 

1. Queries: Reading Data

Queries are used to retrieve data from Sitecore. Here are some common examples:
// Get application context
const contextResult = await client.query('application.context');

// Get list of sites
const sitesResult = await client.query('sites.list', {
    contextId: appContext.tenant.id
});

// Get a specific page
const pageResult = await client.query('pages.get', {
    contextId: appContext.tenant.id,
    pageId: 'your-page-id'
});


2. Mutations: Writing Data

Mutations are used to create, update, or delete data in Sitecore:
// Create a new template
const templateResult = await client.mutate('templates.create', {
    contextId: appContext.tenant.id,
    name: 'My Custom Template',
    baseTemplates: ['{1930BBEB-7805-471A-A3BE-4858AC7CF696}']
});

// Create a new item
const itemResult = await client.mutate('items.create', {
    contextId: appContext.tenant.id,
    parentId: 'parent-item-id',
    templateId: 'template-id',
    name: 'My New Item',
    fields: {
        Title: 'My Item Title',
        Text: 'My item content'
    }
});

// Delete an item
await client.mutate('items.delete', {
    contextId: appContext.tenant.id,
    itemId: 'item-to-delete'
});


3. Subscriptions: Real-Time Updates

Subscriptions allow your app to receive real-time updates when data changes in Sitecore. This is particularly useful for Page Builder extensions:
// Subscribe to page changes
const unsubscribe = client.subscribe('pages.changed', (data) => {
    console.log('Page changed:', data);
    // Update your UI accordingly
});

// Don't forget to unsubscribe when your component unmounts
useEffect(() => {
    return () => {
        if (unsubscribe) {
            unsubscribe();
        }
    };
}, []);

4. Context Management: Application vs. Pages

The SDK provides two types of context:
Application Context is available in all extension points and includes:
  • Tenant and resource IDs
  • Environment details
  • User information and permissions
  • Organization data
Pages Context is only available in Page Builder extension points and includes:
  • Current page information
  • Site details and configuration
  • Real-time updates via subscriptions
Understanding which context is available in your extension point is crucial for building the right user experience.
Example of both contexts as they feed the Page Content Panel Extension point

Server-Side Logic with Next.js

For full-stack apps, you'll need to make server-side API calls to Sitecore. The SDK provides an experimental server-side client for this purpose:
// app/api/items/route.ts
import { experimental_createXMCClient } from '@sitecore-marketplace-sdk/xmc';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
    try {
        // Get the access token from the Authorization header
        const authHeader = request.headers.get('Authorization');
        if (!authHeader) {
            return NextResponse.json(
                { error: 'Missing authorization' },
                { status: 401 }
            );
        }

        const accessToken = authHeader.replace('Bearer ', '');

        // Get the context ID from the request body
        const { contextId, itemData } = await request.json();

        // Create the XMC client
        const xmcClient = experimental_createXMCClient({
            accessToken,
            contextId
        });

        // Make the API call
        const result = await xmcClient.items.create({
            parentId: itemData.parentId,
            templateId: itemData.templateId,
            name: itemData.name,
            fields: itemData.fields
        });

        return NextResponse.json(result);
    } catch (error) {
        console.error('Server-side API call failed:', error);
        return NextResponse.json(
            { error: 'Failed to create item' },
            { status: 500 }
        );
    }
}
The key pattern here is:
  1. Get the access token from the Authorization header
  2. Get the context ID from the request
  3. Create the XMC client with both
  4. Make your API calls
  5. Handle errors appropriately

Styling: Blok vs. shadcn/ui

Sitecore recommends using Blok, their official design system, for Marketplace apps. Blok ensures your app looks and feels like a native part of Sitecore, and it includes built-in accessibility compliance.

However, many developers (myself included) prefer shadcn/ui for its flexibility and modern design. There's even a project called blok-shadcn that combines the best of both worlds, providing shadcn/ui components styled to match Sitecore's design language.
For my AI Challenge app, I used shadcn/ui with custom Sitecore-inspired theming, and the results have been excellent.

Development Best Practices

After building several Marketplace apps, here are some best practices I've learned:

1. Local Development with Hot Reload

During development, set your deployment URL to https://localhost:3000 (or whatever port you're using). This allows you to test your app in the actual Sitecore UI while developing locally. You'll need to use HTTPS, even locally. For Vite apps, use vite-plugin-mkcert. For Next.js, use mkcert to generate self-signed certificates.

2. Start Simple, Add Complexity Gradually

Don't try to build a full-stack app with AI integration on your first attempt. Start with a simple client-side app that displays some data. Get comfortable with the SDK, understand the extension points, and then add complexity.

3. Leverage TypeScript

The SDK is built with TypeScript, and the type definitions are excellent. Use them. They'll save you hours of debugging and make your code more maintainable.

4. Handle Errors Gracefully

The Marketplace SDK can throw errors for various reasons: network issues, authentication problems, invalid queries, etc. Always handle errors gracefully and provide meaningful feedback to users.

5. Test in Multiple Environments

Make sure your app works in all the environments where it will be used: local, dev, staging, and production. Pay special attention to CORS issues and authentication edge cases.

Hosting and Deployment

Since you host your own apps, you have complete control over your deployment strategy. Here are some popular options:
  • Vercel: Excellent for Next.js apps. Automatic deployments from Git, great performance, generous free tier.
  • Netlify: Great for static sites and Vite apps. Easy to set up, good CI/CD integration.
  • Azure Static Web Apps: Good choice if you're already in the Azure ecosystem.
  • AWS Amplify: Another solid option with good integration with other AWS services.
For my AI Challenge app, I'm using Vercel, and the deployment workflow is seamless: push to Git, and Vercel automatically builds and deploys.

Learning Resources: A New Developer Course

To further support the community, the Sitecore Learning team is releasing a new developer course for the Marketplace. While the course isn't available yet, you can see the announcement from Learning Team on LinkedIn. This is a fantastic initiative that will help onboard even more developers to the Marketplace ecosystem.

My AI Challenge Experience: Lessons Learned

Participating in the AI Challenge has been an incredible learning experience. Our proposal made us to the finals, and today I'm completing the final submission. 

Here are some key lessons I've learned:

  • The Marketplace enables rapid innovation. I was able to go from idea to working prototype in a matter of days, not weeks or months.
  • The SDK is that good.
  • Full-stack architecture is essential for AI apps. If you're building anything that involves AI services, you'll need server-side logic.
  • The experimental XMC client makes this straightforward.
My team and I ended up creting an AI marketplace extension that optimizes any of your page-under-edit for the agentic engines, by creating brand-aware content that will be consumed by agents parsing the site and therefore - affecting its LLM's output. IT also suggests how this page could get optimized according to schema.org markup for the better LLM parsers consumption:

Throughout the challenge, I've been in contact with other developers, sharing ideas and solving problems together. This is what makes Sitecore and its community very special.

What's Next?

The Sitecore Marketplace is more than just a new feature; it's a fundamental shift in how Sitecore approaches product development. By empowering the community to build and share apps, Sitecore is effectively multiplying their own development capacity. This is brilliant strategy.

The community has always been Sitecore's greatest strength. We've built modules, shared knowledge, and pushed the platform forward for years. The Marketplace is Sitecore's way of saying: 

"We trust you. We value you. Let's build the future together."

If you're a Sitecore developer or architect, I encourage you to start exploring the Marketplace today. Build something small, get comfortable with the SDK, and then tackle something more ambitious. The barrier to entry is low, and the potential is enormous.
What will you build? A custom field? A dashboard widget? An AI-powered content assistant? 

The possibilities are endless. I'd love to hear your thoughts.

Resolving broken images issue on XM Cloud starterkit running in local containers

A guide on how to resolve the media problem with the default vanilla StarterKit (foundation head) when images do not show on a site when accessed by a hostname.

Reproducing the problem

  1. Pull the latest starter kit, run init.ps1 , and up.ps1 as normal. Everything is fresh and clean; nothing else than that.
  2. When CM spins up, create a site collection followed by a site with the default name (nextjs-starter) matching the name of the JSS app.
  3. Open the home page in Experience Editor and drop an Image component to that page, upload and choose the image, save, and confirm it looks fine in EE.
  4. Open the home page again on the site: https://nextjs.xmc-starter-js.localhost and see the error

The error

Invalid src prop (https://cm/-/jssmedia/Project/Tenant/Habitat/LA.jpg?h=408&iar=0&w=612&ttc=63890432287&tt=4BBE52A4592C2DBCAE59361096C0E4D3&hash=0CA75E384F533EE5539236785DCF0E22) on `next/image`, hostname "cm" is not configured under images in your `next.config.js`

What happens?

The hostname of a local CM container is applied to the generated media URL (https://cm). That is not right, as CM hostname is not accessible outside of containers, and only works within the internal Docker network at the HTTP protocol, not HTTPS. After playing with the image URL, it was clear it works by a relative path, trimming the hostname from the start. Both URLs below are valid and show the image:

Testing an assumption

I thought if there was a problem on the datasource resolver of the Image component, but the resolver wasn't set. Just for the sake of experiment, decided to explicitly set image component to use `/sitecore/system/Modules/Layout Service/Rendering Contents Resolvers/Datasource Resolver`, since it has the same effect as when not set, along with unchecking Include Server URL in Media URLs settings for that resolver.

And on the resolver, unchecking as below:

That effectively resolved the issue. My guess was valid, so let's now seek a permanent solution.

The working solution

Once the assumption of removing a hostname is proven, let's take it out at the CM level so that it universally applies to your environments and you don't need to precisely set resolvers on each individual component. The below config patch does exactly what we need.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <layoutService>
      <configurations>
        <config name="sxa-jss">
          <rendering>
            <renderingContentsResolver>
              <!-- Set the desired values for the JSS configuration -->
              <IncludeServerUrlInMediaUrls>false</IncludeServerUrlInMediaUrls>
            </renderingContentsResolver>
          </rendering>
        </config>
      </configurations>
    </layoutService>
  </sitecore>
</configuration>

That resolved my problem: the desired image loaded well.

Hope this helps you as well!

Changing item ID in XM Cloud from item's context menu

Historically, changing the ID of an item in Sitecore was widely believed to be impossible, particularly within the constraints of XM Cloud.

In the XP era, I experimented with a custom package nearly a decade ago that did successfully change item IDs. However, achieving this required considerable effort: decompiling out-of-the-box DLLs, reverse engineering the code, and then rebuilding DLLs to match various Sitecore versions and dependencies. The process was complex and not ideal, dependent on runtime specifics. I managed to make it function, but lots have changed since then.

With XM Cloud, many of those challenges have been eliminated. Notably, the platform now includes Sitecore PowerShell Extensions (SPE) out of the box. This makes it feasible to develop a universal script that can change the ID of any item in the content tree, invoked directly from the context menu without the need for server-level modifications. All it takes is a simple package containing a single Sitecore item.

Why change ID?

1. Database Merges and Migrations

  • Resolving ID Conflicts During Database Merges: When combining multiple Sitecore databases (e.g., from separate instances or acquisitions), duplicate IDs can occur if items were created independently. Changing the ID of conflicting items in one database allows seamless integration without data loss.
  • Content Migration from Legacy Systems: During upgrades or migrations from older Sitecore versions or other CMS platforms, imported items might have IDs that clash with existing ones. Changing IDs ensures uniqueness while preserving content structure.
  • Multi-Environment Synchronization: If dev, staging, and production environments diverge (e.g., due to unsynced TDS/Unicorn serializations), changing IDs in one environment can align them without recreating everything.
  • Cross-Database Item Transfers: When moving items between master, core, or web databases, ID collisions might arise; changing the ID facilitates clean transfers.

2. Integration with External Systems

  • Aligning with External Data Feeds or APIs: If an external system (e.g., a CRM like Salesforce or a data warehouse) references Sitecore items by GUIDs but uses its own fixed IDs, changing Sitecore's item ID to match the external one ensures synchronization without modifying the external system.
  • Third-Party Tool Compatibility: Some integrations (e.g., e-commerce plugins or analytics tools) might require specific ID formats or mappings; changing IDs bridges mismatches.
  • Bi-Directional Sync with Other Platforms: In setups with real-time syncing (e.g., via Sitecore Data Exchange Framework), ID changes might be needed to resolve sync errors caused by GUID mismatches.
  • Federated Identity Management: For systems integrating Sitecore with identity providers, where user-generated content items need ID alignment for security or tracking.

3. Item Recovery and Error Correction

  • Recovering from Accidental Deletions: If a critical item is deleted (and not sent to the Recycle Bin) but referenced extensively, recreating it with a new ID and updating links/indexes can restore functionality faster than manual fixes, especially if the original ID causes reference breakage.
  • Fixing Overwritten or Forgotten Items in Version Control: In team environments using TDS or Unicorn, if an item is overwritten or omitted during serialization, changing its ID can resolve conflicts and reintegrate it without rebuilding the entire project.
  • Correcting Database Corruption or Duplicates: Rare DB errors (e.g., from failed restores) might create duplicate IDs; changing one item's ID eliminates the duplication.
  • Repairing Broken References After Errors: If an item's ID leads to index or link database issues (e.g., due to partial publishes), changing it and triggering a reference update can fix widespread breakage.

4. Development and Testing Scenarios

  • Simulating ID Conflicts in Testing: Developers might change IDs to test merge scripts, index rebuilds, or reference update pipelines in controlled environments.
  • Refactoring Large Content Trees: During site redesigns, changing IDs of template items or folders can help reorganize without disrupting live content, especially if combined with link repairs.
  • Debugging Custom Modules: To isolate issues in custom code that relies on specific IDs (e.g., caching or workflows), temporarily changing an ID can aid troubleshooting.
  • Performance Optimization Testing: Changing IDs to force index refreshes or cache invalidations in load testing, helping identify bottlenecks.

4. Other creative scenarios

  • Proof of authoring: In certain cases, you may want to intentionally leave a trace or proof of authoring in the item's ID so that you can prove the fact that you've done this job at some stage later. Once working on a questionable project, I used this trick to encode my phonу number as the last digits of the site definition item. This does not affect any functionality, but at the same time cannot be just a random coincidence.
  • Easter eggs: In some mostly non-production cases, you may want to have some fun with issuing creative naming IDs. HEX-based codes allow plenty of creativity there. For example, all these are valid HEX numbers: 
    • Four chars long: BEEF, CAFE, BABE, FACE, CODE, FEED, etc.
    • Eight chars long: DEADFACE, C0FFEEED, C0DEC0DE, BEEFC0DE, etc.
    • Twelve char long: DEADC0FFEEED, DEADFADEBEEF, AD1DA5AD1DA5, etc.

 

Key Features of the PowerShell Module

The PowerShell module provided below delivers the following capabilities:

  • Assigns a new ID to any item: Accepts user input for the new ID, supporting not only standard content items but also templates, media, system items, and more.

  • Processes hierarchical structures: Handles items with children, fields, and sections, while maintaining the integrity of Standard Values for templates.

  • Updates derived content items: Sets the correct TemplateID on all derived items, preventing "object reference not set" errors.

  • Moves and patches descendants: Migrates and updates all child sections and fields under the new template as needed.

  • Refreshes all references: Updates any base templates, field sources, or XML fields that reference the old template.

  • Cache management: Flushes Sitecore caches and, optionally, rebuilds the Link Database and triggers a publish.

  • Efficient querying: Utilizes fast queries to ensure every relevant content item is included.

  • Comprehensive edge case handling: Covers all major scenarios and exceptions.

How It Works

Since Sitecore does not provide an API for changing item IDs directly, the script implements a robust workaround:

  1. Duplicate the source item (with all fields) under a new ID using the SPE API, which supports custom ID assignment.

  2. Move all children from the old item to the newly created item.

  3. Update all references in the database to point from the old ID to the new one.

  4. Delete the original item once all dependencies have been switched.

Note: Due to the item deletion step involved, this script cannot be used on items that originate from resource files; only items stored in a database are eligible.

Usage

Download

[Download package: ChangeItemID-1.0.zip]

Dictionary in XM Cloud

Localization is more than translation - it's about delivering a seamless, culturally relevant experience for every visitor. In Sitecore XM Cloud, dictionary items provide a simple, robust way to centralize UI text and labels for all languages, directly accessible in your Next.js frontend. Below, I’ll walk through the practical setup, usage patterns, and expert recommendations based on Sitecore’s official documentation and proven community practices.

What Are Dictionary Items?

Dictionary items in XM Cloud let you store and manage all your UI phrases centrally.
Instead of hardcoding "Read More", "Submit",  or error messages, you create dictionary keys under your site’s /Dictionary item. Each key holds localized values for every language you support.

Why:
  • Centralized: Update once, used everywhere
  • Multi-language: Add a new language, all keys covered
  • Consistent: No more stray hardcoded strings

When to Use Dictionary vs. Fields

Typically, you would use dictionary entries for generic text reused in many places (e.g., button labels like "Submit", form field placeholders, system messages). For content that might be specific to a single page or component (e.g., a marketing tagline, section heading, or lengthy description), consider making it a field in a content item or datasource rather than a shared dictionary entry. This gives content authors more flexibility to customize text in context. 

A hybrid approach can work too, for example, use a dictionary phrase as a fallback but allow an override via a field in the CMS if the editor wants a custom message in one place.

How Dictionary Items Work in XM Cloud

In XM Cloud, dictionary items are stored at: /sitecore/content/{collection}/{site}/Dictionary and is fetched via GraphQL through the Layout/Dictionary service. Essentially, for each page request, a GraphQL query runs to get all dictionary entries under the site’s dictionary path for the given language. The Next.js SDK’s DictionaryServiceFactory handles this, so you typically don’t need to write this query yourself. If curious, the query filters by the dictionary root item path, language, and the Dictionary Entry template, and returns all key-phrase pairs. Because this data is pulled at request-time (or build-time for static pages), the translations are ready by the time React components render. No client-side extra fetch is needed for dictionary data. It’s included in the page HTML/props payload.

Each dictionary item represents a key (e.g., "read"), and under it, each language version holds the translated phrase.

For multi-site or SXA setups, you can create a shared dictionary (often in a Shared site or at the tenant root), and then set up a fallback so multiple sites reuse the same dictionary keys. On the integration side, XM Cloud provides a Dictionary Service API, available via REST or GraphQL, which populates the dictionary data at build or runtime for your Next.js app.

Sitecore’s Next.js SDK (part of Headless JSS) comes pre-configured to utilize dictionary data for localization. Here’s how the pieces fit together:

  • Next.js Internationalization (i18n) Settings. In your Next.js app’s next.config.js, define the locales your site supports and a default locale. These locales should match the languages enabled in Sitecore. For example:
    i18n: {
      locales: ['en', 'fr-CA', 'es'], 
      defaultLocale: 'en'
    }
    This allows Next.js to handle locale-specific routes, such as /fr-ca/page-name for French Canadian. The defaultLocale should correspond to your site’s default language, often pulled from a config setting of package.json in the Sitecore starters. If you add a new language to Sitecore, update this config and redeploy the app so Next.js knows about the new locale.
  • Page Props and Dictionary Data. The Sitecore Next.js SDK fetches dictionary entries for each page request (or build) and injects them into the page properties. Specifically, the JSS Page Props Factory (a plugin in the Next.js sample app) uses the Dictionary Service to retrieve all dictionary phrases for the current site/language, and includes them as a dictionary object in pageProps. This happens server-side, either at build time for static generation or on the server for SSR, so that the dictionary is ready by the time the React components render.
  • I18n Provider. The Next.js app uses the next-localization library (a lightweight i18n solution) to provide translations. In the app’s custom _app.tsx, an <I18nProvider> is configured to wrap the page component, initialized with the locale and the dictionary data from Sitecore. For example:
    // In _app.tsx
    import { I18nProvider } from 'next-localization';
    function App({ Component, pageProps }) {
      const { dictionary, locale, ...rest } = pageProps;
      return (
        <I18nProvider lngDict={dictionary} locale={locale}>
          <Component {...rest} />
        </I18nProvider>
      );
    }
    Here, pageProps.dictionary contains all key–phrase pairs for the current locale, and pageProps.locale is the language code. The I18nProvider makes a translation function available to all components via React context.
  • Using the Translation Hook. Inside your React components, you use the useI18n() hook (from next-localization) to access the localization context. See the below paragraph for the exact dictionary consumption code sample.

The Single useTranslation Hook Pattern (with Fallback Defaults)

To keep things tidy, many teams prefer a single useTranslations hook. Here’s a pattern that ensures you never show a blank label, even if the dictionary entry is missing, by supplying a fallback default:

import { useI18n } from 'next-localization';

export function useTranslation() {
  const i18n = useI18n();
  // Utility: always show a fallback if dictionary value is missing
  const getTranslation = (key, fallback) => i18n?.t(key) || fallback;

  return {
    readLabel: getTranslation('read', 'Read'),
    submitLabel: getTranslation('submit', 'Submit'),
    // Add more keys as needed
  };
}

// Usage in a component
const translations = useTranslation();

return (
  <div>
    <button>{translations.readLabel}</button>
  </div>
);

Why this is better:

  • No undefined or blank UI if a dictionary key is missing
  • All translations accessible via one hook - simple, scalable
  • Easy to add more keys as your UI grows

Using Shared SXA Dictionaries & Fallback Domains

If you’re using SXA or multiple sites, you can set up a shared dictionary and configure fallback so every site can leverage the same keys.

Setup Steps:

  1. Create a Shared Dictionary. Place it in a shared location, such as /sitecore/content/{collection}/Shared/Dictionary
  2. Set Dictionary Domain. In Sitecore, under your site’s Dictionary item, set the Dictionary Domain field to the shared dictionary’s path. (Official Doc)
  3. Configure Fallback. On your site’s dictionary, enable "fallback domain" to point to the shared dictionary.

In your Next.js app, the Dictionary Service will automatically merge keys from both the site and shared domains. 

Storybook: Mocking Dictionaries for Isolated UI Development

When using Storybook, components expecting dictionary values may break outside the app context. To avoid this, you can mock the next-localization context in .storybook/preview.tsx:

import { I18nProvider } from 'next-localization';

const mockDictionary = {
  read: 'Read',
  submit: 'Submit',
  // Add other keys as needed
};

export const decorators = [
  (Story) => (
    <I18nProvider lngDict={mockDictionary} locale="en">
      <Story />
    </I18nProvider>
  ),
];
Why this matters:
  • Ensures all components render correctly in isolation
  • Lets designers and QA see accurate UI text in stories
  • Helps catch missing dictionary keys before production

Troubleshooting Dictionary Issues

If you encounter an issue where dictionary translations are not appearing in the Next.js app, it is highly likely that it happens because of one of the very frequently missed things. Consider the following checks:
  • Verify Config and App Name. Make sure the Next.js app’s configured site name (app name) matches the Sitecore site name. This is typically set in your app’s package.json (config.appName) or .env and used by the JSS config. If it mismatches Sitecore’s expected name, the dictionary service might look in the wrong place. In XM Cloud projects, the app name is usually the same as your site name in Sitecore.
  • Check Dictionary Domain Path. As noted, ensure the site’s dictionary domain/path settings in Sitecore are correct. If the Dictionary item was renamed or moved, update the site settings or the config patch so the head application knows where to fetch phrases.
  • Publish Content. Confirm that the dictionary items are published to the target environment (Edge). In a development scenario, if using Connected Mode, ensure the dictionary items exist in the expected database. After editing dictionary entries in the Pages or Content Editor interface, run a publish. Unpublished items will not appear in the GraphQL results.
  • For preview mode issues, rebuild the search index. On XM Cloud, indexes are auto-rebuilt on deploy, but if you import items or restore content, an index rebuild might be necessary.
  • Ensure a valid site root is defined. If you see errors like “Valid value for rootItemId not provided” in logs or GraphQL responses, it indicates the dictionary GraphQL service couldn’t auto-resolve the site’s root. Ensure your site template is correct; if fixing it, you can remove any hardcoded rootItemId overrides you may have added during debugging.

Bonus: Dictionary Migration and Conversion SPE Script

The below script copies dictionary items from an old XP database (referred to as old) to the master database of the locally-run XM Cloud environment and also converts Dictionary entries from a custom format of the Habitat website (which many folks have used as a starter kit, regardless) to the XM Cloud format, which is hard-defined by a dictionary template coming from the items resource file:
param(
    [string]$SourceDatabase = "old",
    [string]$SourcePath = "/sitecore/content/Habitat/Global/Dictionary",
    [string]$TargetDatabase = "master",
    [string]$TargetPath = "/sitecore/content/Zont/Habitat/Dictionary"
)

$srcFolderTemplate = "{98E4BDC6-9B43-4EB2-BAA3-D4303C35852E}"
$srcEntryTemplate = "{EC4DD3F2-590D-404B-9189-2A12679749CC}"
$srcPhraseField = "{DDACDD55-5B08-405F-9E58-04F09AED640A}"

$trgFolderTemplate = "{267D9AC7-5D85-4E9D-AF89-99AB296CC218}"
$trgEntryTemplate = "{6D1CD897-1936-4A3A-A511-289A94C2A7B1}"
$trgKeyField = "{580C75A8-C01A-4580-83CB-987776CEB3AF}"
$trgPhraseField = "{2BA3454A-9A9C-4CDF-A9F8-107FD484EB6E}"

function Copy-DictionaryItemsRecursive {
    param(
        [Sitecore.Data.Items.Item]$SrcParent,
        [Sitecore.Data.Items.Item]$TrgParent,
        [Sitecore.Data.Database]$SrcDB,
        [Sitecore.Data.Database]$TrgDB,
        [int]$Depth = 0
    )

    $indent = (" " * ($Depth * 2))
    $children = $SrcParent.GetChildren()
    Write-Output "$indent- Processing source: $($SrcParent.Paths.FullPath) | Child count: $($children.Count)"

    foreach ($child in $children) {
        Write-Output "$indent  - Processing child: $($child.Paths.FullPath) | TemplateID: $($child.TemplateID) | Name: $($child.Name)"
        $templateId = $child.TemplateID.ToString()
        $newItemName = $child.Name
        $newItem = $null

        try {
            # Check if this item already exists by name under TrgParent
            $existingByName = $TrgParent.Children | Where-Object { $_.Name -eq $newItemName }
            if ($existingByName) {
                Write-Output "$indent    - Using existing target item (by name): $($existingByName.Paths.FullPath)"
                $newItem = $existingByName
            } else {
                # Prepare template item
                $templateItem = $null
                if ($templateId -eq $srcFolderTemplate) {
                    $templateItem = $TrgDB.GetTemplate($trgFolderTemplate)
                    Write-Output "$indent    - Using target template (folder): $($templateItem -ne $null)"
                } elseif ($templateId -eq $srcEntryTemplate) {
                    $templateItem = $TrgDB.GetTemplate($trgEntryTemplate)
                    Write-Output "$indent    - Using target template (entry): $($templateItem -ne $null)"
                }
                if (-not $templateItem) { throw "$indent    - ERROR: Target template not found for $newItemName" }

                # Attempt to create under the current TrgParent
                Write-Output "$indent    - Attempting to Add '$newItemName' under $($TrgParent.Paths.FullPath)"
                $newItem = $TrgParent.Add($newItemName, $templateItem)
                if (-not $newItem) { throw "$indent    - ERROR: Failed to add item $newItemName under $($TrgParent.Paths.FullPath)" }
                Write-Output "$indent    - Created item: $($newItem.Paths.FullPath) [ID: $($newItem.ID)]"
            }

            # Recurse or fill entry fields
            if ($templateId -eq $srcFolderTemplate) {
                Write-Output "$indent    - Recursing into folder: $($child.Paths.FullPath)"
                Copy-DictionaryItemsRecursive -SrcParent $child -TrgParent $newItem -SrcDB $SrcDB -TrgDB $TrgDB -Depth ($Depth + 1)
            } elseif ($templateId -eq $srcEntryTemplate) {
                foreach ($lang in $child.Languages) {
                    $srcItemLang = $SrcDB.GetItem($child.ID, $lang)
                    if (-not $srcItemLang) { throw "$indent    - ERROR: Source item language $lang missing for $($child.Paths.FullPath)" }
                    $trgItemLang = $TrgDB.GetItem($newItem.ID, $lang)
                    if (-not $trgItemLang) {
                        $trgItemLang = $newItem.Versions.AddVersion($lang)
                        if (-not $trgItemLang) { throw "$indent    - ERROR: Failed to add language version $lang to $($newItem.Paths.FullPath)" }
                    }
                    $phraseValue = $srcItemLang.Fields[$srcPhraseField].Value
                    $trgItemLang.Editing.BeginEdit()
                    $trgItemLang.Fields[$trgKeyField].Value = $newItemName
                    $trgItemLang.Fields[$trgPhraseField].Value = $phraseValue
                    $trgItemLang.Editing.EndEdit()
                    Write-Output "$indent    - Populated entry ($lang): $($newItem.Paths.FullPath) [ID: $($newItem.ID)]"
                }
            } else {
                Write-Output "$indent    - Skipping unsupported template: $($child.Name) [$templateId]"
            }
        } catch {
            Write-Output "$indent    - ERROR: $($_.Exception.Message) at item $($child.Paths.FullPath)"
            throw
        }
    }
}

try {
    $srcDB = Get-Database $SourceDatabase
    $trgDB = Get-Database $TargetDatabase
    $srcParent = $srcDB.GetItem($SourcePath)
    $trgParent = $trgDB.GetItem($TargetPath)
    Write-Output "Start: Copying dictionary items from $SourcePath [$SourceDatabase] to $TargetPath [$TargetDatabase] ..."
    if (-not $srcParent) { throw "Source parent item not found: $SourcePath in DB: $SourceDatabase" }
    if (-not $trgParent) { throw "Target parent item not found: $TargetPath in DB: $TargetDatabase" }

    Copy-DictionaryItemsRecursive -SrcParent $srcParent -TrgParent $trgParent -SrcDB $srcDB -TrgDB $trgDB -Depth 0
    Write-Output "Dictionary copy completed successfully."
} catch {
    Write-Output "SCRIPT ERROR: $($_.Exception.Message)"
    exit 1
}

Summary & Best Practices

  • Use a single, type-safe useTranslation hook for clarity and safety
  • Always supply fallback defaults to prevent UI errors
  • Prefer shared dictionaries for multi-site projects? Configure fallback domains in Sitecore for reusability
  • Mock dictionary data in Storybook to maintain robust component previews

Hope you find this post helpful!

Auto-migrating any Sitecore XP website to XM Cloud is actually possible!

I developed an AI-powered migration framework that streamlines the migration of Sitecore XP websites to XM Cloud, automating and eliminating 70% to 90% of the manual effort typically required. This solution accurately preserves each page’s presentation structure, including the full hierarchy of components and placeholders the entire set of migrated content, and the rendered HTML output. All Sitecore assets retain their original structure, hierarchy, item IDs, and paths after migration.

The framework requires only two inputs: 1) a backup of the master database and 2) access to any operable site environment, may it be UAT, Production, or even Local. Since direct database operations in the cloud are not possible, the actual migration always runs locally. Having the original source code is optional, but providing it greatly enhances the accuracy of component markup conversion.

At a high level, the migration framework performs the following steps:

  • conducts pre-execution sanity checks and flags potential issues, such as media items exceeding 50Mb or broken layout/references at the origin
  • copies the actual content from XP to XM Cloud
  • converts rendering definitions to ensure XM Cloud compatibility
  • converts ASP.NET MVC Razor Views (*.cshtml files) into Next.js components with TypeScript (*.tsx files)
  • migrates site presentation from the XP format to the XM Cloud format, processing each page individually
  • rebinds datasources as needed for XM Cloud compatibility

For demonstration purposes, I used the well-known Habitat site running on Sitecore 9.2, which I was able to recover from some of my archived VM snapshots. Having a running instance of the site is essential, regardless of the environment. In this example, I configured the VM's hostname rssbplatform.dev.local via my hosts file so it would be accessible from my host machine.

It’s important to note that Sitecore XP and XM Cloud handle presentation differently in defining the page's presentation and dealing with the dynamic placeholders. Unlike XM Cloud, which has a rendered page layout JSON per each of the pages, MVC-powered XP websites did not keep the hierarchy: resolving and preserving the correct hierarchy during migration requires access to a working runtime instance of the source site..

Disclaimer: the demonstration video shows the entire migration of a Habitat website in just 13 minutes, starting with the content migration and ending up recreating the site on XM Cloud, including all the converted pages with the entire layout/structure converted to each of them.

I am not a front-end developer in the first place; thus, I decided to skip this part of conversion for now. When it comes to static files, there are two ways:

  • If you don't have an original source code, just place compiled JS/CSS files under /public folder in Next.js app and adjust the references; no changes are possible.
  • With the source original code, you can compile and deploy static files under /publiс folder; that includes any changes you may have down the way.

Please take into account that not all styles are picking up automatically: that is expected and falls into a remaining 10%-25% of things to do manually. 

When converting styles, they fall into three main categories:

  • Fixed styles: These are automatically picked up during conversion.
  • Parametrized styles: Since the migration framework processes rendering parameters, many of these are also migrated automatically (at least default values).
  • Dynamic styles: Styles generated at runtime or calculated via scripts require custom build logic to be reimplemented on the Next.js side.

With further stages of development, the goal is to entirely convert parametrized and as much of dynamic classes as it is possible.

Below is a video of the entire migration process where I talk it through and explain the steps:

What previously took months of manual labor for a decent team can now get done in minutes. Isn't that incredible?

I wish to hear your thoughts. Drop me a message!

Using these XM Cloud hotkeys can significantly boost your productivity

Do you use hotkeys? 

I am pretty sure most of you intuitively press Ctrl + S in order to save the changes in Content Editor. Highly likely, you also do any of these below:

  • Del -for deleting an item, and all of its children
  • F2 - rename an item

However, there are plenty of other meaningful shortcuts for you to use:

  • F9 - publish site dialog
  • Ctrl + D - duplicates an item, one of my favorite ever.
  • Ctrl + Win - opens a Sitecore "Start" menu (however, that does not work in 100% of cases)
  • F7 - Validation Results
  • Left / Right keyboard arrows allow expanding and collapsing items' nodes

View togglers:

  • Ctrl + Shift + Alt + R: Toggle raw values. This is one of my best savers when I need to grab an ID for a lookup from a reference field.
  • Ctrl + Shift + Alt + T: Toggle the display of standard fields, which helps improve Content Editor performance when off
  • Ctrl + Shift + Alt + L: Toggle the protection for a selected item.

Content Editor Tabs:

IMG

They typically work as Alt + <first_letter_of_a_tab>:

  • Alt + H: Home
  • Alt + N: Navigate
  • Alt + R: Review
  • Alt + P: Publish
  • Alt + V: Versions
  • Alt + C: Configure
  • Alt + S: Security

 ... but since several tabs start from the same character, less straightforward hotkeys are used:

  • Alt + I: View

 

Predictive search

In Content Editor, hit Ctrl + / and you'll see the bottom right search area activate, waiting for your input. Type in at least two characters for it to progressively show the results, as you keep typing.

 

Mouse Gestures

Dragging and item with Ctrl button held creates a copy of that item right at the location where you release the button.

 

How do I know these shortcuts?

There is a master shortcut Alt + F1 that brings up helpful labels exposing the hotkeys for the specific functions on a Ribbon:

Hope this helps you staying more productive!

Using Pipeline Profiler

What is a Pipeline Profiler?

Firstly, let's answer what a pipeline is. A pipeline in Sitecore is essentially an ordered sequence of processes (called processors) that run to perform a specific task in the system. Sitecore’s pipeline profiler is a built-in diagnostic tool that tracks how long each pipeline and processor takes to execute. 

Why is this useful? In complex Sitecore solutions, it can be difficult to know what exactly is happening inside the pipelines at runtime, especially after deploying to a cloud or container environment. Pipeline profiling helps answering questions like: "Is my custom pipeline processor actually running, and in the correct order?" or "Which part of the request pipeline is slowing down page loads?". In other words, it provides visibility into the inner workings and performance of Sitecore’s processing framework.

By default, pipeline profiling is disabled for performance reasons, but when enabled, it records metrics like execution counts and timings for each pipeline processor. This data is exposed via an admin page so you can see which pipelines or steps are consuming the most time. 

Why and When to Enable Pipeline Profiling

You should consider enabling pipeline profiling only when you need to diagnose or debug pipeline behavior, especially for diagnosing bottlenecks and verifying pipeline behavior in real scenarios. It’s not something to leave on all the time (more on that in considerations below), but it’s invaluable as a temporary diagnostic tool during development, testing, or troubleshooting.

How to Enable Pipeline Profiling

You just toggle it via a config patch or environment setting.

<add key="env:define" value="Profiling" />

This above env:define setting named "Profiling" acts as a flag that enables the pipeline profiling configuration. Use the full configuration patch file, as below to enable the profiler:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <!-- Enable pipeline profiling -->
      <setting name="Pipelines.Profiling.Enabled">
        <patch:attribute name="value">true</patch:attribute>
      </setting>
      <!-- (Optional) Enable CPU time measurements for profiling -->
      <setting name="Pipelines.Profiling.MeasureCpuTime">
        <patch:attribute name="value">false</patch:attribute>
      </setting>
    </settings>
  </sitecore>
</configuration>

 

How to access the Pipeline Profiler page

After enabling profiling and restarting, log in to Sitecore and navigate to the special admin page on your CM instance URL: /sitecore/admin/pipelines.aspx. You must be logged in as an administrator to access it, like you do for other admin pages.

You should see a table of all pipelines that have executed since startup:

There's a Refresh button on the page; clicking it will update the stats to include any pipelines run since the last refresh. You can filter by pipeline name or simply scroll to see various pipelines, such as initialize, renderLayout, item:saved, publishItem, etc. Each pipeline row can be expanded to show all the processors within it, in the order they execute. The key columns to pay attention to are:

  • #Executions: how many times that pipeline or processor has run since the last reset. For example, after a site startup, you might see the initialize pipeline ran once, whereas something else runs for every HTTP request, so it could be, say, 50 if you've made 50 page requests so far.
  • Wall Time / Time per Execution: the total time spent in that pipeline or processor, and the average time for a single execution. It is particularly useful for spotting slow components; if a single run of a processor takes, say, 500ms on average, that’s a potential performance hot-spot to investigate.

  • % Wall Time: the percentage of the pipeline’s total time that a given processor accounted for. This helps identify which processor within a pipeline is the major contributor to the total time. For example, if one processor is 90% of the pipeline’s execution time, that’s your primary suspect.
  • Max Wall Time: the longest single execution time observed for that pipeline or processor. This is useful to identify spikes or inconsistent performance. If the max time is significantly higher than the average, it means one of the executions had an outlier slow run.

Remember to Remove/Undo the Config

If you added a config patch or environment variable for profiling, make sure to remove or disable it after you're done. For example, in XM Cloud SaaS, that means taking it out of your code (and redeploying) or toggling off the environment variable in your cloud portal settings. This prevents someone from accidentally leaving it on. It also ensures your deployments are clean and you don't want to carry a debugging setting to higher environments unknowingly.

Enabling pipeline profiling in XM Cloud is straightforward and gives Sitecore developers a powerful lens to observe the inner workings of the platform. Whether you’re chasing down a production slowdown or ensuring your custom pipelines run as expected, this feature can save you countless hours. As always, use it wisely!

XM Cloud productivity tip: Use Cloud Portal Auto Login extension for Chrome

It may be that you're as annoyed as I am by the way certain things are implemented. Imagine you spin up local XM Cloud in containers, type Up.ps1, hit enter, and go away to make some coffee. It typically takes some time while Docker gets updated images and performs the rest of its business, but right in the middle, it gets interrupted by this:

The only purpose of this screen is for you to confirm, simply clicking the "CONFIRM" button, and then keep waiting for approximately the same amount of time you already spent. It won't progress until you actually press this button.

That isn't nice, what a time waster, I thought, and started thinking about the ways of automating clicking the confirmation button, so that the entire Docker start process goes uninterrupted. Created a Chrome extension that appeared to be the easiest one, so there I went.

Then I thought it would be a good idea also to automate submissions to the other screens when you process to the portal, namely this one:

The question comes next question is how to safely keep the email identifier separately from the extension. Browser local storage seems to be a good compromise, so let's go that way. Once users come to the portal for the first time, they enter their email ID as normal. At the time of submission, the extension intercepts the button click and stores the value encrypted. On the next visit to this screen, the value is decrypted and gets automatically submitted into the field by the extension.

I use basic base64 encryption, but with an additional step of character rotation (also known as the Caesar Cipher). This prevents a stranger from a straightforward decipherment of the value taken directly from the local storage. Email ID isn't a very sensitive piece of information, so I am not going wild with protecting its value.

Finally, when the user deliberately logs out by using a drop-down menu from the top right corner, the extension removes the value from the browser's storage. This is done purposely, for example, if one logs out, they usually do that in order to log in as the other user, or maybe they just want out of the system - in any case, we must ensure no further automatic logins persist, and that is exactly what happens.

Currently, it is under moderation in the Chrome store, so you may load this archive by this link and progress with the "Load unpacked" extension, as shown below:

Source code is available here: https://github.com/MartinMiles/cloud-portal-auto-login

Hope you like that one. If you have any comments or thoughts, reach out to me!