How to migrate content after a relaunch?

  • 20/07/2023
  • Technical
  • Sitecore XP, Third-party Integrations, Best Practices, Code Examples

Content migration - every sitecore developer will stumble upon this topic sooner or later in his career.

It is impossible to give step-by-step instructions for this, as the requirements vary for each use case, but I will do my best to point out what to look for during a content migration.

Preparation

Make a list of all the items on the old instance. For each element, check whether it is still in use. If not, you don't have to map it either, so you can cross it off the list. Now try to find a counterpart among the new modules for each old module on the list and map the properties.


Safe effort

If a module is only used a few times, you should weigh up whether it makes sense to include the module in the content migration or not. Sometimes it is more efficient to map rarely occurring modules manually.


Coordination

Always try to get everyone involved on board and let people from the content area support you with the content migration. You will get to a point where it takes expertise to map the properties properly.


Clean up

A content migration is always a good opportunity to get rid of unnecessary content and and get the project to a proper state.


Languages

In the case of multilingual applications, do not forget to map the items in all required languages.


Versions and workflows

Clarify whether you actually need all versions of an item. In most cases it is enough if you map the latest published version and any newer drafts.


Users

Make sure that the user you are performing the content migration with has all the necessary permissions.

If it is a requirement, that the content migration does not affect the processing history, you can switch user for each item to the user from the "__Updated by" field. In addition, I would recommend creating a fallback user with all possible permissions, which will be used if anything goes wrong.


Technology

I'm sure, there are a lot of different ways to migrate content, but i will introduce you to the approach that I have had the best experience with - Sitecore PowerShell Extension (SPE).


Hands on

Below you can find a few code snippets which roughly illustrate the content migration process.

Start by getting all the pages you want to map from the old system and passing them on to the associated method. I didn't check for versions here to keep the sample compact.


$pagesToMap = Get-ChildItem -Path "/sitecore/content/DemoOld/Pages" -Language "en" -Recurse | Where-Object {$_.TemplateName -eq 'ContentPage'}

$pagesToMap | ForEach-Object {
    MapPage($_)
}
                                

Now map the page, get the right version for every module on the placehoder and pass it on to the associated method again.


Function MapPage($oldDatasource){

    # Get the user who last updated the datasource item
    $user = Get-User -Identity $oldDatasource.Fields["__Updated by"].Value

    # Use the fallback user, in case something went wrong
    if(-Not $user){
        $user = Get-User -Identity "sitecore\contentmigration"
    }

    # Use SecurityDisabler
    New-UsingBlock(New-Object -TypeName "Sitecore.SecurityModel.SecurityDisabler"){

        # Use UserSwitcher
        New-UsingBlock (New-Object Sitecore.Security.Accounts.UserSwitcher $user) {

            $newDatasource = New-Item -Path "sitecore/content/Demo/Home/$($oldDatasource.Name)" -ItemType "/sitecore/templates/Demo/PageTemplates/ContentPage" -Language "en"

            # Create datasource folder under the page
            $ModulesDatasourcePath = "sitecore/content/Demo/Home/$($oldDatasource.Name)/Content"
            New-Item -Path "$($ModulesDatasourcePath)" -ItemType "/sitecore/templates/Demo/FolderTemplates/ContentFolder" -Language "en" | Out-Null


            # Map properties
            $newDatasource.Editing.BeginEdit()
            $newDatasource["PageTitle"] = $oldDatasource["PageHeadline"]
            $newDatasource["PageSubTitle"] = $oldDatasource["PageSubheadline"]
            $newDatasource["PageIntro"] = $oldDatasource["IntroText"]
            $newDatasource.Editing.EndEdit() | Out-Null


            # Get every rendering from the placeholder
            Get-Rendering -Item $oldDatasource -FinalLayout -Language "en" |Where-Object{$_.Placeholder -eq "/phContent/phPageContent_123a45b-9ffd-4ac6-bbad-6dff506fecc0"} | ForEach-Object {

                # Check if the rendering has a datasource
                if ($_.Datasource){

                    # Get all versions of the datasource
                    [array]$versions = Get-Item -Path "master:" -ID $_.Datasource -Version *

                    # Check if the datasource has versions
                    if($versions){

                        # Start with the last (newest) version
                        For ($i = $versions.Count-1; $i -ge 0; $i--) {

                            $module = $versions[$i]

                            # Check if the Workflow state is "published"
                            if((($module)["__Workflow state"] -eq "{5EA2EE09-394C-4CEC-8E94-CEE6535B094F}")){

                                # Check the template of the module in order to call the associated method
                                if($module.TemplateName -eq 'ImageParagraph'){
                                    MapImageParagraphModuleToImageTextModule $module $ModulesDatasourcePath $newDatasource
                                }
                                elseif($module.TemplateName -eq 'AnotherModule'){
                                    MapAnotherModule $module $ModulesDatasourcePath $newDatasource
                                }
                                # else if...

                                # Leave the loop as soon as one version got mapped
                                break
                            }
                        }
                    }
                }
            }
        }
    }
}
                                

Last but not least map the actual module and add it's rendering to the new pages placeholder.


Function MapImageParagraphModuleToImageTextModule($ImageParagraphDatasource, $ImageTextModulesDatasourcePath, $TargetPage){

    # Get the user who last updated the datasource item
    $user = Get-User -Identity $ImageParagraphDatasource.Fields["__Updated By"].Value

    # Use the fallback user, in case something went wrong
    if(-Not $user){
        $user = Get-User -Identity "sitecore\contentmigration"
    }

    # Use SecurityDisabler
    New-UsingBlock(New-Object -TypeName "Sitecore.SecurityModel.SecurityDisabler"){

        # Use UserSwitcher
        New-UsingBlock (New-Object Sitecore.Security.Accounts.UserSwitcher $user) {

            # Create a new datasource item
            $ImageTextDatasource = New-Item -Path "$($ImageTextModulesDatasourcePath)/$($ImageParagraphDatasource.Name)" -ItemType "/sitecore/templates/Demo/ModuleTemplates/ImageTextModule" -Language "en"

            # Switch to editing mode
            $ImageTextDatasource.Editing.BeginEdit()

            # Map custom properties
            $ImageTextDatasource["Headline"] = $ImageParagraphDatasource["Topline"] # 1:1
            $ImageTextDatasource["MainText"] = $ImageParagraphDatasource["Paragraph"] # 1:1
            $ImageTextDatasource["Image"] = $ImageParagraphDatasource["Image"] # 1:1

            $ImageTextDatasource["ImagePosition"] = "F102189B-5PW2-46C2-9FV2-8K310D09478A" # Set fallback value
            if($ImageParagraphDatasource["ImageLeftHandSide"] -eq 1) {$ImageTextDatasource["ImagePosition"] = "D202232A-5CB2-46C2-9AD2-8C310D07188A"} # Checkbox => Droplink

            # Map Sitecore properties
            $ImageTextDatasource.Fields["__Lock"].Value = $ImageParagraphDatasource.Fields["__Lock"].Value
            $ImageTextDatasource.Fields["__Workflow state"].Value = $ImageParagraphDatasource.Fields["__Workflow state"].Value
            $ImageTextDatasource.Fields["__Default workflow"].Value = $ImageParagraphDatasource.Fields["__Default workflow"].Value
            $ImageTextDatasource.Fields["__Workflow"].Value = $ImageParagraphDatasource.Fields["__Workflow"].Value
            $ImageTextDatasource.Fields["__Created"].Value = $ImageParagraphDatasource.Fields["__Created"].Value

            # Leave editing mode
            $ImageTextDatasource.Editing.EndEdit() | Out-Null

            # Create a new rendering
            $ImageTextRendering = Get-Item -Database "master" -Path "/sitecore/layout/Renderings/Demo/ModuleRenderings/ImageTextModule" | New-Rendering

            # Add the created rendering to the placeholder
            Add-Rendering -Item $TargetPage -FinalLayout -Instance $ImageTextRendering -DataSource $ImageTextDatasource.ID -Placeholder "phContent" -Language "en"
        }
    }
}
                                

Now your content is in the right place!

Things that hit me:

  • Be aware of possible custom set character limits in the text properties of the new modules. Remove the limits during the migration period and log the affected modules in order to have a list of the texts that need to be improved later.
  • When creating new modules, make sure that there is no module with the same name in the target folder. If this is the case, add a unique abbreviation to the name and log the module in order to rename it later.

P.S.: For more informations about Sitecore and our Services, please visit www.cyber-solutions.at