Content Migration Tip 1 Handling Clones in Sitecore Serialization

Created: 14 Mar 2025, last update: 24 Mar 2025

Content Migration Tip 1 - Handling Clones in Sitecore Serialization

This is the first part of a series on content migration with Sitecore CLI serialization. Unlike previous articles that focused on data transformation, this series is about moving content while avoiding serialization and deserialization issues.

The Problem: Clone-Related Errors in Sitecore Serialization
When migrating a large number of Sitecore items using Sitecore CLI serialization, you might encounter issues with clones. If there's only one problematic clone, fixing it manually is feasible. But with hundreds or thousands of items, clone-related errors can waste hours of migration time.

Why does this happen?
 - Some items have an invalid __Source Item field. These items need to be fixed or uncloned before migration.
 - Sitecore serialization is order-dependent. The clone source must be existing before its clones. If clones are imported before their source, the process fails.

Example Sitecore CLI Clone Error:

[master] Applying changes...

GraphQL.ExecutionError: [A] /sitecore/content/Home/mysite/Copy myitem (2aa346b1-55e5-4794-ab4f-619c60d81b40): [Inner Exception] The clone source has incorrect value. Please fix '__Source Item' value ('Advanced' section) to continue. Item id:{2AA346B1-55E5-4794-AB4F-619C60D81B40}

The Solution: Package Clone Sources Before Serialization
One way to avoid clone errors is to use a Sitecore package instead of serialization.

 - A full package of all content isn't always practical. (a package do not have the order-dependent)
 - However, a small package containing only clone sources and their parent items can prevent errors.

Automating the Fix with a Sitecore PowerShell Script
The Sitecore PowerShell script provided below automates this process:

 - It scans a subtree for all clones using the search index (ensure indexing is up to date).
 - It checks if the clone source exists in the same subtree.
 - It creates a Sitecore package containing only valid clone sources.

How to Fix Clone Issues in 3 Steps

 1) Run the PowerShell script to find all clone sources in your subtree and create a package.

 2) Install the package on the target environment.

 3) Run sitecore ser push, the import should now complete without clone errors since the clone sources exist.

This approach is efficient even for large-scale migrations with thousands of clones.

# Settings
$startPath = "/sitecore/content/SNP"
$clonesourcepath = "/sitecore/content/SNP"
$uncloneByError = $true  # Set to $false to only log without uncloning

# Find all clones using Find-Item (Fast via Solr index)
$criteria = @(
    @{Filter = "Equals"; Field = "_isclone"; Value = "true"},
    @{Filter = "StartsWith"; Field = "_fullpath"; Value = $startPath}
)

# Search using the correct index and criteria
$props = @{ Index = "sitecore_master_index"; Criteria = $criteria }
Write-Host "🔎 Searching for clones in the index..."
$clonedItems = Find-Item @props | Select-Object -Property ItemId

# Collect unique clones
$uniqueClones = $clonedItems | Sort-Object ItemId -Unique

# Total number of unique clones
$total = $uniqueClones.Count
Write-Host "📌 Total number of unique clones: $total"

# Lists for source items and errors
$clonesourceList = @()
$cloneErrors = @()
$counter = 0

foreach ($clone in $uniqueClones) {
    $counter++
    $sourceItemID = $clone.ItemId
    $sourceItem = Get-Item -Path "master:" -ID $sourceItemID -ErrorAction Stop
    Write-Host "📍 [$counter/$total] Clone: $($sourceItem.Paths.Path) ($sourceItemID)"

    if ($sourceItem -and $sourceItem."__Source Item") {
        $sourceUri = $sourceItem."__Source Item"

        try {
            # Try to retrieve the source item
            $clonesource = Get-Item -Path "master:" -Uri $sourceUri -ErrorAction Stop
            $clonesourceList += $sourceUri
        }
        catch {
            # Clone source not found -> Log and unclone if necessary
            $errorMsg = "❌ Clone '$($sourceItem.Paths.Path)' $sourceItemID has an invalid source: $sourceUri"
            Write-Host $errorMsg -ForegroundColor Red
            $cloneErrors += $errorMsg

            if ($uncloneByError) {
                Write-Host "🔄 Uncloning item: $($sourceItem.Paths.Path)" -ForegroundColor Cyan
                try {
                    Unclone-Item -Item $sourceItem -ErrorAction Stop
                    Write-Host "✅ Clone successfully uncloned: $($sourceItem.Paths.Path)" -ForegroundColor Green
                }
                catch {
                    Write-Host "⚠️  Could not unclone the item using unclone: $($sourceItem.Paths.Path)" -ForegroundColor Yellow
                            $sourceItem.Editing.BeginEdit()
                            $sourceItem."__Source Item" = ""
                            $sourceItem."__Source" = ""
                            $sourceItem.Editing.EndEdit()
                            Write-Host "✅ Clone successfully uncloned by clearing the source field!! $($sourceItem.Paths.Path)" -ForegroundColor Green
                }
            }
        }
    }
}

# Create a unique list of valid source items
$uniqueCloneSources = $clonesourceList | Sort-Object -Unique

# HashSet for unique paths
$pathSet = @{}

foreach ($csuri in $uniqueCloneSources) {
    $clonesource = Get-Item -Path "master:" -Uri $csuri
    $fullPath = $clonesource.Paths.Path

    # Check if the path falls within the desired scope
    if ($fullPath -like "$clonesourcepath*") {
        # Remove the clonesourcepath
        $relativePath = $fullPath -replace "^$clonesourcepath", ""

        # Split the path and build the subpaths
        $parts = $relativePath -split "/"
        $currentPath = ""

        foreach ($part in $parts) {
            if ($part -ne "") {
                $currentPath += "/$part"

                # Add to the set if it does not exist yet
                if (-not $pathSet.ContainsKey($currentPath)) {
                    $pathSet[$currentPath] = $true
                }
            }
        }
    }
}

# Sort and display the unique subpaths
$sortedPaths = $pathSet.Keys | Sort-Object
Write-Host "📂 Unique Clone Source Paths:"
$sortedPaths | ForEach-Object { Write-Host "- $_" }
Write-Host "Tip you can use this paths to serialize this clone source items and parents, write some code to generate the config. or just create a package with the items"

# Package name
$packageName = "CloneSourceItems"

# Start a new package (default Sitecore package location is used)
$package = New-Package $packageName

# Set package metadata
$package.Sources.Clear();

$package.Metadata.Author = "Jan Bluemink";
$package.Metadata.Publisher = "Scripting the clone sources";
$package.Metadata.Version = "1.0";
$package.Metadata.Readme = 'Only clone sources, after installing you should be able to use CLI serialization with clone conflicts.'

# Add all items in $sortedPaths to the package
foreach ($relativePath in $sortedPaths) {
    # Reconstruct the full path
    $fullItemPath = "$clonesourcepath$relativePath"

    # Retrieve the item
    $source = Get-Item "master:$fullItemPath" | New-ExplicitItemSource -Name 'clonesource' -InstallMode Skip
    if ($source) {
        $package.Sources.Add($source)
        Write-Host "✅ Added to package: $fullItemPath"
    } else {
        Write-Host "⚠️  Item not found: $fullItemPath" -ForegroundColor Yellow
    }
}

# Save package
Export-Package -Project $package -Path "$($package.Name)-$($package.Metadata.Version).zip" -Zip

# Offer the user to download the package
Download-File "$SitecorePackageFolder\$($package.Name)-$($package.Metadata.Version).zip"

Write-Host "🎁 Package ready for download: $($package.PackagePath)" -ForegroundColor Green

AI and PowerShell Automation
The PowerShell script used in this process has been optimized with clear icons and user-friendly help descriptions, thanks to AI. While AI assistants can help generate basic PowerShell scripts, fully automated  this kind of Sitecore PowerShell scripting is not yet possible without lots of manual adjustments and corrections. For now, human expertise is still required to ensure accuracy and reliability in Sitecore automation.

Script Overview
This script searches for clones in a specific location using the search index, so make sure your index is up to date before running it. It is highly optimized for performance, handling hundreds of thousands of items and thousands of clones efficiently. The main use case is enabling CLI serialization for a subtree where both the clone sources and their clones exist within the same structure. If the clone source is outside the subtree, the import order becomes an issue. The script first finds all clones in a subtree, checks if their sources are also within that tree, and adds them to the export list. At the end, the script creates a Sitecore package containing all clone sources within the tree. Alternatively, since the script already identifies all necessary items, it can be modified to generate a CLI config file for serializing clone sources first. However, in most real-world scenarios, a small package approach is faster and more reliable, even when dealing with thousands of clones.

Links

Sitecore content migration - Part 1: Media analysis
Sitecore content migration - Part 2: Media migration
Sitecore content migration - Part 3: Converting content