Mathias Osterkamp

Specialist – focus development Microsoft technology stack

Powershell - Create Navigation

Create managed metadata navigation from Powershell CSOM

Managed Navigation with Powershell

Goal

Our goal is a complete setup for a test site collection with managed metadata navigation and existing pages with different levels. The challenge is the correct configuration for the site with powershell and also create the correct terms. With the script you can set the amount of navigation entries.

Preparation

We use SharePoint PnP Powershell Package to get help with some tasks.

  • PnP Powershell 2019 Download
  • CSOM dlls “Microsoft.SharePoint.Client.dll” “Microsoft.SharePoint.Client.Runtime.dll” “Microsoft.SharePoint.Client.Taxonomy.dll” “Microsoft.SharePoint.Client.Publishing.dll” from “\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\isapi\” , more details here
  • Create a classic team site collection

Key Points

The Invoke-SetupFeatures enables needed features for publishing, what is the requirement.

The Invoke-SetupTermSet creates your termset and Invoke-SetupNavigation enables navigation features. We also use Invoke-EnableTagging to allow tagging, that means we also override the default permission behaviour. (Metadata navigation is not visible to users with read permissions) more information.

Furthermore we collect the page layout item Get-PageLayoutItem and create our terms Invoke-CreateTerms. For each term we create also first a new site page Add-PublishingPageWithContent and connect this page with the new created term.

The Invoke-UpdateTaxonomyCache checks changes for taxonomy and avoids save conflicts.

We also had some issues to crawl a big amount (over 3000) of friendly pages from search. That means you can create the pages, but the search index does not collect it any more - at least the friendly url version.

The official limit for “Number of terms in managed navigation term set” from Microsoft is at 2000.

After some research, on a full crawl for example the complete termset is crawled with all pages. That leads to a timeout. You can override this in search central administration here. We had to set it to a high value, the crawl tooks over 12 minutes - so that is maybe in some cases no optimal solution.

Complete Script

# -------------------------------------------------------------------------------------
# Configuration
# -------------------------------------------------------------------------------------
$siteurl = "https://contoso.com/sites/sitecollection1"
$termSetName = "GlobalNav"

# -------------------------------------------------------------------------------------
# Functions
# -------------------------------------------------------------------------------------
function Invoke-CreateTerms($context, $PublishingWeb, $PageLayoutItem, $termStore, $termObject, $path, $level, $maxlevel, $maxItemsPerLevel, $overallMax) {

    $max = $maxItemsPerLevel
    $level = $level + 1
 
    for ($i = 1; $i -lt $max + 1; $i++) {
        if ($global:countTerms -lt $overallMax) {
            if ($path -eq "") {
                $newPath = "$i"
            }
            else {
                $newPath = "$($path)_$($i)"
            }
       
            $termName = "Term$newPath"

            $newpage = Add-PublishingPageWithContent $context $PublishingWeb $PageLayoutItem "$($termName).aspx" $termName $termName
            $url = $newpage["FileRef"]
            $global:countTerms++
            Write-Host "Create term $($termName) - $($global:countTerms)/$overallMax..." -f Yellow -NoNewline
            $newTerm = $termObject.CreateTerm($termName, 1033, [System.Guid]::NewGuid().toString())
            $newTerm.SetLocalCustomProperty('_Sys_Nav_TargetUrl', $url)
            Write-Host "Done" -f Green
        
            if ($level -lt $maxlevel ) {       
                Invoke-CreateTerms $context $PublishingWeb $PageLayoutItem $termStore $newTerm $newPath $level $maxlevel $maxItemsPerLevel $overallMax 
            }
        }
    }
    $termStore.CommitAll()
    Invoke-PnPQuery

}
function Get-PageLayoutItem($PageLayoutName) {
    $Ctx = Get-PnPContext
    Write-host -f Yellow "Getting Page Layout..." -NoNewline
    #Get the Page Layout
    $RootWeb = $Ctx.Site.RootWeb
    $MasterPageList = $RootWeb.Lists.GetByTitle('Master Page Gallery')
    $CAMLQuery = New-Object Microsoft.SharePoint.Client.CamlQuery
    $CAMLQuery.ViewXml = "<View><Query><Where><Eq><FieldRef Name='FileLeafRef' /><Value Type='Text'>$PageLayoutName</Value></Eq></Where></Query></View>"
    $PageLayouts = $MasterPageList.GetItems($CAMLQuery)
    $Ctx.Load($PageLayouts)
    $Ctx.ExecuteQuery()

    $PageLayoutItem = $PageLayouts[0]
    $Ctx.Load($PageLayoutItem)
    $Ctx.ExecuteQuery()
    Write-host -f Green "Done"
    return $PageLayoutItem
}
function Add-PublishingPageWithContent($Ctx, $PublishingWeb , $PageLayoutItem , $PageName, $PageTitle, $PageContent) {
   

    #Create Publishing page
    Write-host -f Yellow "Creating New Page $PageName ..." -NoNewline
    $PageInfo = New-Object Microsoft.SharePoint.Client.Publishing.PublishingPageInformation 
    $PageInfo.Name = $PageName
    $PageInfo.PageLayoutListItem = $PageLayoutItem
    $Page = $PublishingWeb.AddPublishingPage($PageInfo) 
    $Ctx.ExecuteQuery()

    $ListItem = $Page.ListItem
    $Ctx.Load($ListItem)
    $Ctx.ExecuteQuery()
 
    #Update Page Contents
    $ListItem["Title"] = $PageTitle
    $ListItem["PublishingPageContent"] = $PageContent
    $ListItem.Update()
    $Ctx.ExecuteQuery()

 
    #Publish the page

    $ListItem.File.CheckIn([string]::Empty, [Microsoft.SharePoint.Client.CheckinType]::MajorCheckIn)
    $ListItem.File.Publish([string]::Empty)
    $Ctx.ExecuteQuery()
    Write-host -f Green "Done"


    return $ListItem
}
function Invoke-SetupFeatures() {

    #publihsing Infrastructure site feature check
    $FeaturePublishingInfraSiteId = "f6924d36-2fa8-4f0b-b16d-06b7250180fa" #Site Scoped Publishing Feature
    $Feature = Get-PnPFeature -Scope Site -Identity $FeaturePublishingInfraSiteId
    If ($null -eq $Feature.DefinitionId) {    
        Write-host -f Yellow "Activating Publishing Infrastructure Site Feature..." -NoNewline
        Enable-PnPFeature -Scope Site -Identity $FeaturePublishingInfraSiteId -Force
        Write-host -f Green "Done"
    }
    Else {
        Write-host -f Yellow "Publishing Infrastructure Site Feature already activated..." -NoNewline
        Write-host -f Green "Done"
    }

    #publishing Infrastructure web feature check
    $FeaturePublishingInfraWebId = "94c94ca6-b32f-4da9-a9e3-1f3d343d7ecb" 
    $Feature = Get-PnPFeature -Scope Web -Identity $FeaturePublishingInfraWebId
    If ($null -eq $Feature.DefinitionId) {    
        Write-host -f Yellow "Activating Publishing Infrastructure Web Feature..."
        Enable-PnPFeature -Scope Web -Identity $FeaturePublishingInfraWebId -Force
        Write-host -f Green "Done"
    }
    Else {
        Write-host -f Yellow "Publishing Infrastructure Web Feature already activated..." -NoNewline
        Write-host -f Green "Done"
    }

    #Wait complete all
    Start-Sleep -Seconds 10
}
function Invoke-SetupTermSet($termSetName) {

    $context = Get-PnPContext
    $CurrentSite = Get-PnPSite
    $taxonomySession = [Microsoft.SharePoint.Client.Taxonomy.TaxonomySession]::GetTaxonomySession($context)
    $TermStore = $taxonomySession.GetDefaultSiteCollectionTermStore();
    $SiteCollectionTermGroup = $TermStore.GetSiteCollectionGroup($CurrentSite, $false)
    $context.Load($taxonomySession)
    $context.Load($SiteCollectionTermGroup)
    $context.Load($TermStore)
    Invoke-PnPQuery
    $termgroupname = $SiteCollectionTermGroup.Name
    
    $termSets = Get-PnPTermSet  -TermGroup $termgroupname
    $exists = ($termSets | Where-Object { $_.Name -eq $termSetName } | Measure-Object).Count -gt 0
    if ($exists -eq $false ) {
        Write-Host "Created termset $termSetName ..." -f Yellow  -NoNewline
        $termSet = New-PnPTermSet -Name $termSetName -TermGroup $SiteCollectionTermGroup -Lcid 1033  -IsOpenForTermCreation
        $TermStore.CommitAll()
        Write-Host "Done" -f Green  
    }
    else {
        
        Start-Sleep -Seconds 5
        Write-Host "Termset already exists $($termSet.Name)" -f Yellow  -NoNewline
        $termSet = Get-PnPTermSet -Identity $termSetName -TermGroup $termgroupname 
        Write-Host "Done" -f Green
    }

}
function Invoke-SetupNavigation($termSetName) {

    Write-Host "Reset Navigation..." -f Yellow -NoNewline
    $context = Get-PnPContext
    $CurrentSite = Get-PnPSite
    $taxonomySession = [Microsoft.SharePoint.Client.Taxonomy.TaxonomySession]::GetTaxonomySession($context)

    $TermStore = $taxonomySession.GetDefaultSiteCollectionTermStore();
    $SiteCollectionTermGroup = $TermStore.GetSiteCollectionGroup($CurrentSite, $false)
    $termsets = $SiteCollectionTermGroup.TermSets
    $context.Load($taxonomySession)
    $context.Load($SiteCollectionTermGroup)
    $context.Load($termsets)
    $context.Load($TermStore)
    Invoke-PnPQuery

    $navigationTermSet = $termsets | Where-Object { $_.Name -eq $termSetName } 

    $context.Load($navigationTermSet)
    Invoke-PnPQuery
    Write-Host "Done" -f Green

    Write-Host "Set taxonomy navigation..." -f Yellow -NoNewline
    $context = Get-PnPContext
    $Web = Get-PnPWeb
    $navigationSettings = New-Object Microsoft.SharePoint.Client.Publishing.Navigation.WebNavigationSettings $context, $Web
    $navigationSettings.ResetToDefaults()
    $navigationSettings.GlobalNavigation.Source = 1
    $navigationSettings.CurrentNavigation.Source = 1
    $navigationSettings.Update($taxonomySession)
    Invoke-PnPQuery
    Start-Sleep -Seconds 2

  
    $context = Get-PnPContext
    $Web = Get-PnPWeb
    $navigationSettings = New-Object Microsoft.SharePoint.Client.Publishing.Navigation.WebNavigationSettings $context, $Web
    $navigationSettings.CurrentNavigation.Source = "taxonomyProvider"
    $navigationSettings.CurrentNavigation.TermStoreId = $TermStore.Id
    $navigationSettings.CurrentNavigation.TermSetId = $navigationTermSet.Id
    $navigationSettings.GlobalNavigation.Source = "taxonomyProvider"
    $navigationSettings.GlobalNavigation.TermStoreId = $TermStore.Id
    $navigationSettings.GlobalNavigation.TermSetId = $navigationTermSet.Id
    $navigationSettings.Update($taxonomySession)

    $Web.AllProperties["__IncludeSubSitesInNavigation"] = $True
    #Show pages in global navigation
    $Web.AllProperties["__IncludePagesInNavigation"] = $False
 
    #Update Settings
    $Web.Update()
    $TermStore.CommitAll()
    Invoke-PnPQuery

    Write-Host "Done" -f Green
  
}
function Invoke-UpdateTaxonomyCache() {
    $context = Get-PnPContext
    Write-Host "Update taxonomy cache..." -f Yellow -NoNewline
    $TaxonomySession = Get-PnPTaxonomySession
    $TaxonomySession.UpdateCache()
    $context.Load($TaxonomySession)
    Invoke-PnPQuery
    Write-Host "Done" -f Green
}
function Invoke-EnableTagging($termSetName) {

    Write-Host "Enable tagging for termset..." -f Yellow -NoNewline
    Start-Sleep -Seconds 5
    $context = Get-PnPContext
    $CurrentSite = Get-PnPSite
    $taxonomySession = [Microsoft.SharePoint.Client.Taxonomy.TaxonomySession]::GetTaxonomySession($context)
    $TermStore = $taxonomySession.GetDefaultSiteCollectionTermStore();
    $SiteCollectionTermGroup = $TermStore.GetSiteCollectionGroup($CurrentSite, $false)
    $termsets = $SiteCollectionTermGroup.TermSets
    $context.Load($taxonomySession)
    $context.Load($SiteCollectionTermGroup)
    $context.Load($termsets)
    $context.Load($TermStore)
    Invoke-PnPQuery
    
    $navigationTermSet = $termsets | Where-Object { $_.Name -eq $termSetName } 
    
    $context.Load($navigationTermSet)
    Invoke-PnPQuery
    
    $navigationTermSet.IsOpenForTermCreation = $true
    $navigationTermSet.IsAvailableForTagging = $true
    $TermStore.CommitAll()
    
    Start-Sleep -Seconds 2
    Write-Host "Done" -f Green
}

function Invoke-LoadPnp() {
    Write-Host "Load Libraries..." -f Yellow -NoNewline

    $rootpath = $PSScriptRoot
    $path = Join-Path -Path $rootpath -ChildPath "SharePointPnPPowerShell2019\3.29.2101.0\"
    $sharepointPowershellModulePath = Join-Path -Path $path -ChildPath "SharePointPnPPowerShell2019.psd1" 
    if ($null -eq (Get-Module  -Name "SharePointPnPPowerShell2019")) {
        Import-Module $sharepointPowershellModulePath -DisableNameChecking
        Disable-PnPPowerShellTelemetry -Force | Out-Null
    }
    #Load SharePoint CSOM Assemblies
    Add-Type -Path "$path\Microsoft.SharePoint.Client.dll"
    Add-Type -Path "$path\Microsoft.SharePoint.Client.Runtime.dll"
    Add-Type -Path "$path\Microsoft.SharePoint.Client.Taxonomy.dll"
    Add-Type -Path "$path\Microsoft.SharePoint.Client.Publishing.dll"
    Write-Host "Done" -f Green
}

# -------------------------------------------------------------------------------------
# Load PnP and Client Libraries
# -------------------------------------------------------------------------------------
Invoke-LoadPnp
# -------------------------------------------------------------------------------------
# Connect
# -------------------------------------------------------------------------------------
$global:countTerms = 0

Connect-PnPOnline -Url $siteurl -CurrentCredentials
$Web = Get-PnPWeb -Includes Title, WebTemplate, Configuration

# -------------------------------------------------------------------------------------
# Test Template
# -------------------------------------------------------------------------------------
Write-Host "Site $($Web.Title): $($Web.WebTemplate)#$($Web.Configuration) it should be STS#0"
if ($Web.WebTemplate -ne "STS" -and $Web.Configuration -ne 0) {
    Write-Host "Wrong template" -ForegroundColor Red
}

# -------------------------------------------------------------------------------------
# Setup Features
# -------------------------------------------------------------------------------------
Invoke-SetupFeatures

# -------------------------------------------------------------------------------------
# Setup Termset
# -------------------------------------------------------------------------------------
Invoke-SetupTermSet $termSetName
Invoke-UpdateTaxonomyCache
# -------------------------------------------------------------------------------------
# Setup Navigation
# -------------------------------------------------------------------------------------
Invoke-SetupNavigation $termSetName
Invoke-UpdateTaxonomyCache

# -------------------------------------------------------------------------------------
# Setup Tagging
# -------------------------------------------------------------------------------------
Invoke-EnableTagging $termSetName

# -------------------------------------------------------------------------------------
# Create Content
# -------------------------------------------------------------------------------------
$Ctx = Get-PnPContext
$PublishingWeb = [Microsoft.SharePoint.Client.Publishing.PublishingWeb]::GetPublishingWeb($Ctx, $Ctx.Web) 
$Ctx.Load($PublishingWeb)
$Ctx.ExecuteQuery()
$PageLayoutItem = Get-PageLayoutItem 'ArticleLeft.aspx'
$context = Get-PnPContext
$CurrentSite = Get-PnPSite
$taxonomySession = [Microsoft.SharePoint.Client.Taxonomy.TaxonomySession]::GetTaxonomySession($context)

$termStore = $taxonomySession.GetDefaultSiteCollectionTermStore();
$SiteCollectionTermGroup = $TermStore.GetSiteCollectionGroup($CurrentSite, $false)
$termsets = $SiteCollectionTermGroup.TermSets
$context.Load($taxonomySession)
$context.Load($SiteCollectionTermGroup)
$context.Load($termsets)
$context.Load($TermStore)
Invoke-PnPQuery

$termSet = $termsets | Where-Object { $_.Name -eq $termSetName } 

$context.Load($termSet)
Invoke-PnPQuery
Invoke-CreateTerms -context $context -PublishingWeb $PublishingWeb -PageLayoutItem $PageLayoutItem -termStore $termStore -termObject $termSet -path "" -level 0 -maxlevel 3 -maxItemsPerLevel 30 -overallMax 3300 

Invoke-UpdateTaxonomyCache