SharePoint OnlineTechnology

Sync SharePoint MegaMenu across Site Collections in SharePoint Online

Ever needed to sync or copy SharePoint’s OOTB Mega Menu across different Site Collections ? Well, you might come across a requirement as such some day!

This post covers the operations involved in copying the Mega Menu from a given Site Collection to another Site Collection. At a high level, the script does the following,

  1. Retrieves the Site Navigation from source Site Collection – (Copy from)
  2. Retrieves the Site Navigation from destination Site Collection – (Copy to)
  3. Compares the Source and Destination to check if there are any differences – this step is optional. I’d recommend this to avoid unnecessary syncing when the source and destination are identical.
  4. Deletes the existing Site Collection Navigation from the destination – Verify that you are not deleting the source site collection’s navigation 😉
  5. Uses the Source Site Navigation collection retrieved in step 1 to populate the Site Navigation in the Destination.
param([Parameter(Mandatory=$false)]
    [System.Management.Automation.PSCredential]$credentials = (Get-Credential)
)

$srcNavObjects = $null

function Get-SPSiteNavigation{
        param(
                [Parameter(Mandatory=$true)]
                [string] $Identity,
                [Parameter(Mandatory=$false)]
                [string]$prefixUrl

        )
        begin{
                $exportNavCol = @()
                # This counter is used in order to maintain the order of the navigation. 
                # The navigation is returned in the order it appears.
                $counter = 1
                Write-Debug "$((Get-Date).ToString("dd/MM/yyyy HH:mm:ss")) Get-SPSiteNavigation function started."
        }
        process{
                # Connect to the  site with the navigation to base all the other sites on
                $connection = Connect-PnPOnline -Url $identity -Credentials $credentials
                $site = Get-PnPSite

                Write-Debug "$((Get-Date).ToString("dd/MM/yyyy HH:mm:ss")) Exporting navigation from $($site.Url)..."

                # Get the master navigation
                $navigationNodes = Get-PnPNavigationNode -Location QuickLaunch -Connection $connection
                
                # Iterate through the navigation and capture all the nodes on all 3 levels
                foreach($navigationNode in $navigationNodes){
                        $parentNode = Get-PnPNavigationNode -id $navigationNode.Id
                        
                        if($parentNode.Title -eq 'Recent'){
                            Continue
                        }

                        $navInfo = New-Object PSObject -property @{
                                Level = "Level 1"
                                Id = $navigationNode.Id
                                Title = $navigationNode.Title
                                Url =  $navigationNode.Url
                                ParentId = "0"
                                ParentTitle = ""
                                Visible = $navigationNode.IsVisible
                                Order = $counter
                        }
                        # Add the navInfo collection to the collection we're going to export.
                        $exportNavCol += $navInfo
                        $counter++

                        # Get the second level navigation
                        $navigation = Get-PnPNavigationNode -Id $navigationNode.Id
                        $children = $navigation.Children

                        # If children exist proceed
                        if($children){
                                foreach($child in $children){
                                        # Get the node and further information about the link
                                        $childNode = Get-PnPNavigationNode -Id $child.Id
                                        $navInfo = New-Object PSObject -property @{
                                                Level = "Level 2"
                                                Id = $childNode.Id
                                                Title = $childNode.Title
                                                Url = $childNode.Url
                                                ParentId = $parentNode.Id
                                                ParentTitle = $parentNode.Title
                                                Visible = $childNode.IsVisible
                                                Order = $counter
                                        }

                                        # Add the navInfo collection to the collection we're going to export.
                                        $exportNavCol += $navInfo
                                        $counter++

                                        # Get the third level navigation
                                        $subChildren = $childNode.Children

                                        # if children exist proceed
                                        if($subChildren) {
                                                foreach($subChild in $subChildren) {
                                                        # Get the node and further information about the link
                                                        $subChildNode = Get-PnPNavigationNode -Id $subChild.Id
                                                        $navInfo = New-Object PSObject -property @{
                                                                Level = "Level 3"
                                                                Id = $subChildNode.Id
                                                                Title = $subChildNode.Title
                                                                Url = $subChildNode.Url
                                                                ParentId = $childNode.Id
                                                                ParentTitle = $childNode.Title
                                                                Visible = $childNode.IsVisible
                                                                Order = $counter
                                                        }
                                                        $exportNavCol += $navInfo
                                                        $counter++
                                                }
                                        }       
                                }
                        }
                }
                Disconnect-PnPOnline -Connection $connection
        }
        end{                
                # Rebuild collection with sort
                $navObjects = @()
                $navObjects = $exportNavCol | Sort-Object Order

                Write-Debug "$((Get-Date).ToString("dd/MM/yyyy HH:mm:ss")) Get-SPSiteNavigation finished."
                
                return $navObjects
        }
}       

function Copy-SPSiteNavigation {
        param (
                [Parameter(Mandatory=$true)]
                [string] $Identity
        )
        begin{
                $newNavigationArray = @()
                Write-Debug "$((Get-Date).ToString("dd/MM/yyyy HH:mm:ss")) Copy-SPSiteNavigation function started."
        }
        process{
                # Connect to the target site with where the navigation will be added
                $connection = Connect-PnPOnline -Url $identity -Credentials $credentials
                $site = Get-PnPSite

                $currentLevel = 1
                $parentId = 0
            
                # Import navigation CSV
                $navigationCol = $srcNavObjects #Import-Csv $importCsv

                Write-Debug "$((Get-Date).ToString("dd/MM/yyyy HH:mm:ss")) Removing existing navigation."
                
                # Remove existing  navigation
                Remove-PnPNavigationNode -All -Force

                # Remove any remaining nodes. Experienced instances where the RemovePnpNavigationNode -All hasn't worked.
                
                $navNodes = Get-PnPNavigationNode -Location QuickLaunch -Connection $connection
                $navNodes | Remove-PnPNavigationNode -Force

                Write-Debug "$((Get-Date).ToString("dd/MM/yyyy HH:mm:ss")) Importing navigation from $($importCsv) to $($site.Url)..."
                            
                foreach($navigationObject in $navigationCol){
                       Try{
                        Write-Debug "$((Get-Date).ToString("dd/MM/yyyy HH:mm:ss")) Adding navigation node: $($navigationObject.Title)"
                        #Write-Debug "$($navigationObject.ParentId) $($navigationObject.Title)"

                        # If the parent is 0 then it is a top-level navigation node
                        if($navigationObject.ParentId -eq 0) {
                                $parentId = 0
                                $level = 1                    
                                if($navigationObject.Url.Length -gt 0) {
                                        # Navigation node has a Url so create it as a top level menu item
                                        $addedNav = Add-PnPNavigationNode -Url $navigationObject.Url -Title $navigationObject.Title -Location QuickLaunch
                                }else{
                                        # Navigation node hasn't a Url so create it as a top level header item
                                        $addedNav = Add-PnPNavigationNode -Title $navigationObject.Title -Location QuickLaunch
                                }
                        }else{
                                # Get the parent for the child node from the master navigation array
                                $parentNav = $navigationCol | Where-Object{$_.Id -eq $navigationObject.ParentId}
                                if($parentNav.Url.Length -gt 0) {
                                        # Navigation node has a Url so get the newly created navigation node from the new array
                                        # via the Title and Url to get the new Id for the parent item and add it to the navigation
                                        $newNav = $newNavigationArray | Where-Object {$_.Title -eq $parentNav.Title -and $_.Url -eq $parentNav.Url}
                                        if($currentLevel -ne $navigationObject.Level){
                                                $parentId = $newNav.Id
                                                $level = $navigationObject.Level
                                        } 
                                        $addedNav = Add-PnPNavigationNode -Url $navigationObject.Url -Title $navigationObject.Title -Location QuickLaunch -Parent $parentId
                                }else{
                                        # Navigation node does not have a Url so get the newly created navigation node from the new array
                                        # via the Title only to get the new Id for the parent item and add it to the navigation
                                        $newNav = $newNavigationArray | Where-Object{$_.Title -eq $parentNav.Title}
                                        if($currentLevel -ne $navigationObject.Level){
                                                $parentId = $newNav.Id
                                                $level = $navigationObject.Level
                                        } 
                                        $addedNav = Add-PnPNavigationNode -Title $navigationObject.Title -Location QuickLaunch -Parent $parentId
                                }
                                $parentNav = $null
                        }

                        $currentLevel = $navigationObject.Level

                        # Capture the navigation node created to store their IDs
                        $navigationObject = New-Object PSObject -property @{
                                Id = $addedNav.Id
                                Url = $addedNav.Url
                                Title = $addedNav.Title
                                Level = $level
                                ParentId = $parentId
                        }
    
                        # Add the recently added navigation to the new navigation array
                        $newNavigationArray += $navigationObject
                       }
                       Catch{
                            $_ | Out-File $($PSScriptRoot + "/NavigationSyncLog.txt")
                       }
                }    
        }
        end{
                Disconnect-PnPOnline -Connection $connection
        }         
}



$DebugPreference = "Continue" #Continue; SilentlyContinue

$srcNavUrl = "https://[tenant].sharepoint.com/sites/source"
$destNavUrl = "https://[tenant].sharepoint.com/sites/dest"

$srcNavObjects  = Get-SPSiteNavigation -identity $srcNavUrl -prefixUrl "https://[tenant].sharepoint.com"
$destNavObjects = Get-SPSiteNavigation -identity $destNavUrl -prefixUrl ""

$propertiesToCompare = "Visible","Title","Url","ParentTitle","Order"
$compareRes = Compare-Object -ReferenceObject $srcNavObjects -DifferenceObject $destNavObjects -Property $propertiesToCompare -CaseSensitive #-ExcludeDifferent
$diff = $compareRes | Where {$_.SideIndicator -eq "<="}

#Run the script only if the nav in source site collection has updates.
if($diff)
{
    Write-Host "Changes detected.. The Navigation items will be re-synchronised" -ForegroundColor Yellow
    $sitesToReplicateMegaMenu = @("https://[tenant].sharepoint.com/sites/dest")
    foreach($site in $sitesToReplicateMegaMenu){
        Write-Host "Creating Mega Menu in $site..." -ForegroundColor Yellow
        Copy-SPSiteNavigation -identity $site
        Write-Host "Mega Menu in $site has been created successfully" -ForegroundColor Green
    }
}else{
    Write-Host "The Navigation items are identical. No sync is required" -ForegroundColor Green
}

Save the script as syncMegaMenu.ps1

PS C:\dev\scripts> .\syncMegaMenu.ps1 $credendtials = (Get-Credential)
Pass the credentials parameter as argument
PS C:\dev\scripts> .\syncMegaMenu.ps1 -credentials $credentials


Leave a Reply

Your email address will not be published. Required fields are marked *