Compare Two Site Settings
This PnP PowerShell script compares the configuration settings between two SharePoint Online sites. This is essential for ensuring consistency across your SharePoint environment, identifying configuration drift, and maintaining standardized site configurations.
Purpose
Site settings comparison helps with:
- Identifying configuration differences between development, staging, and production sites
- Ensuring consistent governance and compliance settings
- Troubleshooting site-specific issues by comparing working vs non-working sites
- Auditing site configurations for compliance reporting
- Standardizing site templates and provisioning processes
Prerequisites
- PnP PowerShell module installed
- Site collection administrator permissions on both sites
- Access to both SharePoint Online sites being compared
- PowerShell execution policy allowing script execution
Site Settings Extraction Script
# SharePoint Site Configuration Comparison Script
# This script exports detailed site settings to compare staging vs prod
# Install PnP.PowerShell if not already installed
# Install-Module -Name PnP.PowerShell -Force -AllowClobber
param(
[Parameter(Mandatory=$false)]
[string]$SiteUrl = "https://tenantName.sharepoint.com/sites/SiteName/",
[Parameter(Mandatory=$false)]
[string]$OutputPath = "Output Path",
[Parameter(Mandatory=$false)]
[string]$ClientId = ""
)
Write-Host "Connecting to $SiteUrl..." -ForegroundColor Cyan
Connect-PnPOnline -Url $SiteUrl -ClientId $ClientId -Interactive
$siteName = $SiteUrl.Split('/')[-1]
if ($siteName -eq "") {
$siteName = $SiteUrl.Split('/')[-2]
}
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$reportFile = Join-Path $OutputPath "$siteName`_config.json"
Write-Host "Gathering site configuration..." -ForegroundColor Cyan
$config = @{}
# 1. Basic Site Information
Write-Host " - Basic site information" -ForegroundColor Gray
$web = Get-PnPWeb -Includes Title, Url, Language, WebTemplate, Configuration, Created, LastItemModifiedDate
$config.BasicInfo = @{
Title = $web.Title
Url = $web.Url
Language = $web.Language
WebTemplate = $web.WebTemplate
Configuration = $web.Configuration
Created = $web.Created
LastModified = $web.LastItemModifiedDate
}
# 2. Site Features
Write-Host " - Site and web features" -ForegroundColor Gray
$config.SiteFeatures = Get-PnPFeature -Scope Site | Select-Object DisplayName, DefinitionId | Sort-Object DisplayName
$config.WebFeatures = Get-PnPFeature -Scope Web | Select-Object DisplayName, DefinitionId | Sort-Object DisplayName
# 3. Search Settings
Write-Host " - Search settings" -ForegroundColor Gray
try {
$searchSettings = Get-PnPProperty -ClientObject $web -Property AllProperties
$config.SearchSettings = @{
NoCrawl = $searchSettings.NoCrawl
SearchBoxInNavBar = $web.AllProperties["SearchBoxInNavBar"]
SearchScope = $web.AllProperties["SearchScope"]
}
} catch {
$config.SearchSettings = "Error retrieving: $_"
}
# 4. Content Types (Site level)
Write-Host " - Content types" -ForegroundColor Gray
$config.ContentTypes = Get-PnPContentType | Select-Object Name, Id, Group, Hidden | Sort-Object Name
# 5. Site Pages Library Settings
Write-Host " - Site Pages library" -ForegroundColor Gray
try {
$sitePagesLib = Get-PnPList -Identity "Site Pages" -Includes EnableModeration, EnableVersioning, MajorVersionLimit, EnableMinorVersions, RootFolder, NoCrawl, Hidden
$config.SitePagesLibrary = @{
Title = $sitePagesLib.Title
EnableModeration = $sitePagesLib.EnableModeration
EnableVersioning = $sitePagesLib.EnableVersioning
MajorVersionLimit = $sitePagesLib.MajorVersionLimit
EnableMinorVersions = $sitePagesLib.EnableMinorVersions
NoCrawl = $sitePagesLib.NoCrawl
Hidden = $sitePagesLib.Hidden
ItemCount = $sitePagesLib.ItemCount
ContentTypesEnabled = $sitePagesLib.ContentTypesEnabled
}
# Get content types enabled on Site Pages
$sitePagesContentTypes = Get-PnPContentType -List "Site Pages" | Select-Object Name, Id
$config.SitePagesContentTypes = $sitePagesContentTypes
} catch {
$config.SitePagesLibrary = "Error retrieving: $_"
}
# 6. Regional Settings
Write-Host " - Regional settings" -ForegroundColor Gray
$regionalSettings = Get-PnPProperty -ClientObject $web -Property RegionalSettings
$config.RegionalSettings = @{
LocaleId = $regionalSettings.LocaleId
TimeZone = $regionalSettings.TimeZone.Description
}
# 7. Site Collection App Catalog
Write-Host " - App catalog and apps" -ForegroundColor Gray
try {
$apps = Get-PnPApp
$config.InstalledApps = $apps | Select-Object Title, Id, Deployed, AppCatalogVersion | Sort-Object Title
} catch {
$config.InstalledApps = "Error retrieving apps"
}
# 8. Navigation
Write-Host " - Navigation settings" -ForegroundColor Gray
$config.Navigation = @{
QuickLaunch = Get-PnPNavigationNode -Location QuickLaunch | Select-Object Title, Url, Id
TopNavigationBar = Get-PnPNavigationNode -Location TopNavigationBar | Select-Object Title, Url, Id
}
# 9. Property Bag (Custom properties)
Write-Host " - Property bag values" -ForegroundColor Gray
$propertyBag = Get-PnPPropertyBag
$config.PropertyBag = $propertyBag
# 10. Search Schema (Managed Properties relevant to pages)
Write-Host " - Search schema (managed properties)" -ForegroundColor Gray
try {
# This requires search admin permissions
$config.SearchSchema = "Requires Search Admin - run separately"
} catch {
$config.SearchSchema = "Not accessible"
}
# 11. Site Permissions
Write-Host " - Site permissions" -ForegroundColor Gray
$config.SiteGroups = Get-PnPGroup | Select-Object Title, Id, LoginName | Sort-Object Title
# 12. Lists and Libraries Summary
Write-Host " - Lists and libraries" -ForegroundColor Gray
$lists = Get-PnPList -Includes Title, ItemCount, BaseTemplate, Hidden, NoCrawl, EnableModeration
$config.ListsAndLibraries = $lists | Where-Object { -not $_.Hidden } | Select-Object Title, ItemCount, BaseTemplate, NoCrawl, EnableModeration | Sort-Object Title
# 13. Hub Site Association
Write-Host " - Hub site info" -ForegroundColor Gray
try {
$hubSite = Get-PnPHubSite -Identity $SiteUrl -ErrorAction SilentlyContinue
if ($hubSite) {
$config.HubSite = @{
Title = $hubSite.Title
Id = $hubSite.Id
SiteUrl = $hubSite.SiteUrl
}
} else {
$config.HubSite = "Not a hub site or not associated"
}
} catch {
$config.HubSite = "Not a hub site or not associated"
}
# 14. Site Scripts and Site Designs Applied
Write-Host " - Site designs" -ForegroundColor Gray
try {
$siteDesigns = Get-PnPSiteDesign
$config.SiteDesigns = $siteDesigns | Select-Object Title, Id
} catch {
$config.SiteDesigns = "Not accessible"
}
# 15. Indexing Status
Write-Host " - Indexing status" -ForegroundColor Gray
$config.IndexingStatus = @{
NoCrawl = $web.NoCrawl
RequestReindex = "Check site settings manually"
}
# Export to JSON
Write-Host "`nExporting configuration to $reportFile..." -ForegroundColor Cyan
$config | ConvertTo-Json -Depth 10 | Out-File -FilePath $reportFile -Encoding UTF8
Write-Host "Configuration exported successfully!" -ForegroundColor Green
Write-Host "File location: $reportFile" -ForegroundColor Yellow
# Disconnect
Disconnect-PnPOnline
Write-Host "`nTo compare two sites, run this script against both sites, then use the comparison script." -ForegroundColor Cyan
Settings Comparison Script
# Site Configuration Comparison Script
# Compares two site configuration JSON files and highlights differences
param(
[Parameter(Mandatory=$false)]
[string]$StagingConfigPath = "siteName_config.json",
[Parameter(Mandatory=$false)]
[string]$ProdConfigPath = "siteName2_config.json",
[Parameter(Mandatory=$false)]
[string]$OutputReportPath = "comparison_report.html"
)
Write-Host "Loading configurations..." -ForegroundColor Cyan
$staging = Get-Content $StagingConfigPath | ConvertFrom-Json
$prod = Get-Content $ProdConfigPath | ConvertFrom-Json
$differences = @()
$detailedSections = @{}
function Compare-Objects {
param($obj1, $obj2, $path)
if ($obj1 -is [System.Management.Automation.PSCustomObject] -and $obj2 -is [System.Management.Automation.PSCustomObject]) {
$props1 = $obj1.PSObject.Properties.Name
$props2 = $obj2.PSObject.Properties.Name
$allProps = $props1 + $props2 | Select-Object -Unique
foreach ($prop in $allProps) {
$newPath = if ($path) { "$path.$prop" } else { $prop }
if ($props1 -notcontains $prop) {
$script:differences += [PSCustomObject]@{
Property = $newPath
Issue = "Missing in Staging"
StagingValue = "N/A"
ProdValue = $obj2.$prop
}
}
elseif ($props2 -notcontains $prop) {
$script:differences += [PSCustomObject]@{
Property = $newPath
Issue = "Missing in Prod"
StagingValue = $obj1.$prop
ProdValue = "N/A"
}
}
else {
Compare-Objects -obj1 $obj1.$prop -obj2 $obj2.$prop -path $newPath
}
}
}
elseif ($obj1 -is [Array] -and $obj2 -is [Array]) {
if ($obj1.Count -ne $obj2.Count) {
$script:differences += [PSCustomObject]@{
Property = $path
Issue = "Array count mismatch"
StagingValue = "Count: $($obj1.Count)"
ProdValue = "Count: $($obj2.Count)"
}
}
}
else {
if ($obj1 -ne $obj2) {
# Ignore timestamp differences
if ($path -notmatch "Created|LastModified|timestamp") {
$script:differences += [PSCustomObject]@{
Property = $path
Issue = "Value mismatch"
StagingValue = $obj1
ProdValue = $obj2
}
}
}
}
}
# Function to create detailed comparison tables for arrays
function Get-DetailedArrayComparison {
param($stagingArray, $prodArray, $sectionName, $keyProperty)
$html = "$sectionName - Detailed Comparison"
if ($stagingArray -and $prodArray) {
# Create comparison table
$html += @"
"@
# Get all unique items
$allItems = @()
if ($stagingArray) {
$allItems += $stagingArray | ForEach-Object { $_.$keyProperty }
}
if ($prodArray) {
$allItems += $prodArray | ForEach-Object { $_.$keyProperty }
}
$allItems = $allItems | Select-Object -Unique | Sort-Object
foreach ($item in $allItems) {
$inStaging = $stagingArray | Where-Object { $_.$keyProperty -eq $item }
$inProd = $prodArray | Where-Object { $_.$keyProperty -eq $item }
$status = ""
$rowClass = ""
if ($inStaging -and $inProd) {
$status = "✓ Present in both"
$rowClass = "info"
}
elseif ($inStaging -and -not $inProd) {
$status = "⚠ Missing in Prod"
$rowClass = "warning"
}
elseif (-not $inStaging -and $inProd) {
$status = "⚠ Missing in Staging"
$rowClass = "warning"
}
# Get additional details if available
$stagingDetails = if ($inStaging) {
($inStaging.PSObject.Properties | Where-Object { $_.Name -ne $keyProperty } | ForEach-Object { "$($_.Name): $($_.Value)" }) -join ""
} else { "N/A" }
$prodDetails = if ($inProd) {
($inProd.PSObject.Properties | Where-Object { $_.Name -ne $keyProperty } | ForEach-Object { "$($_.Name): $($_.Value)" }) -join ""
} else { "N/A" }
$html += @"
"@
}
$html += @"
Item
In Staging
In Prod
Status
$item
$stagingDetails
$prodDetails
$status
"@
}
else {
$html += "No data available for comparison"
}
return $html
}
Write-Host "Comparing configurations..." -ForegroundColor Cyan
# Compare each section
$sections = @(
"BasicInfo",
"SiteFeatures",
"WebFeatures",
"SearchSettings",
"ContentTypes",
"SitePagesLibrary",
"SitePagesContentTypes",
"RegionalSettings",
"InstalledApps",
"Navigation",
"PropertyBag",
"SiteGroups",
"ListsAndLibraries",
"HubSite",
"IndexingStatus"
)
foreach ($section in $sections) {
Write-Host " Comparing $section..." -ForegroundColor Gray
if ($staging.$section -and $prod.$section) {
Compare-Objects -obj1 $staging.$section -obj2 $prod.$section -path $section
}
elseif (!$staging.$section -and $prod.$section) {
$differences += [PSCustomObject]@{
Property = $section
Issue = "Section missing in Staging"
StagingValue = "N/A"
ProdValue = "Present"
}
}
elseif ($staging.$section -and !$prod.$section) {
$differences += [PSCustomObject]@{
Property = $section
Issue = "Section missing in Prod"
StagingValue = "Present"
ProdValue = "N/A"
}
}
}
# Generate detailed comparison sections
Write-Host " Generating detailed comparisons..." -ForegroundColor Gray
$detailedSections["ContentTypes"] = Get-DetailedArrayComparison -stagingArray $staging.ContentTypes -prodArray $prod.ContentTypes -sectionName "Content Types" -keyProperty "Name"
$detailedSections["SiteFeatures"] = Get-DetailedArrayComparison -stagingArray $staging.SiteFeatures -prodArray $prod.SiteFeatures -sectionName "Site Features" -keyProperty "DisplayName"
$detailedSections["WebFeatures"] = Get-DetailedArrayComparison -stagingArray $staging.WebFeatures -prodArray $prod.WebFeatures -sectionName "Web Features" -keyProperty "DisplayName"
$detailedSections["SitePagesContentTypes"] = Get-DetailedArrayComparison -stagingArray $staging.SitePagesContentTypes -prodArray $prod.SitePagesContentTypes -sectionName "Site Pages Content Types" -keyProperty "Name"
$detailedSections["InstalledApps"] = Get-DetailedArrayComparison -stagingArray $staging.InstalledApps -prodArray $prod.InstalledApps -sectionName "Installed Apps" -keyProperty "Title"
$detailedSections["ListsAndLibraries"] = Get-DetailedArrayComparison -stagingArray $staging.ListsAndLibraries -prodArray $prod.ListsAndLibraries -sectionName "Lists and Libraries" -keyProperty "Title"
# Generate HTML Report
Write-Host "`nGenerating comparison report..." -ForegroundColor Cyan
$htmlReport = @"
Site Configuration Comparison
body { font-family: 'Segoe UI', Arial, sans-serif; margin: 20px; background: #f5f5f5; }
h1 { color: #0078d4; }
h2 { color: #0078d4; margin-top: 40px; border-bottom: 2px solid #0078d4; padding-bottom: 10px; }
h3 { color: #005a9e; margin-top: 30px; }
.summary { background: white; padding: 20px; border-radius: 8px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
table { width: 100%; border-collapse: collapse; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
th { background: #0078d4; color: white; padding: 12px; text-align: left; position: sticky; top: 0; }
td { padding: 10px; border-bottom: 1px solid #ddd; vertical-align: top; }
tr:hover { background: #f9f9f9; }
.critical { background: #fff3cd; }
.warning { background: #d1ecf1; }
.info { background: #d4edda; }
.section-header { background: #e7e7e7; font-weight: bold; }
.detail-section { margin: 30px 0; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.toc { background: white; padding: 20px; border-radius: 8px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.toc a { display: block; padding: 5px 0; color: #0078d4; text-decoration: none; }
.toc a:hover { text-decoration: underline; }
SharePoint Site Configuration Comparison
Summary
Staging Config: $StagingConfigPath
Prod Config: $ProdConfigPath
Total Differences Found: $($differences.Count)
Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
Table of Contents
Differences Summary
Content Types Comparison
Site Pages Content Types Comparison
Site Features Comparison
Web Features Comparison
Installed Apps Comparison
Lists and Libraries Comparison
Property Bag Comparison
Differences Summary
"@
if ($differences.Count -eq 0) {
$htmlReport += "No differences found! Sites are configured identically."
}
else {
$htmlReport += @"
"@
$currentSection = ""
foreach ($diff in ($differences | Sort-Object Property)) {
$section = $diff.Property.Split('.')[0]
if ($section -ne $currentSection) {
$htmlReport += ""
$currentSection = $section
}
$rowClass = ""
if ($diff.Property -match "SearchSettings|NoCrawl|IndexingStatus") {
$rowClass = "critical"
}
elseif ($diff.Property -match "ContentType|Feature") {
$rowClass = "warning"
}
$htmlReport += @"
"@
}
$htmlReport += @"
Property
Issue
Staging Value
Prod Value
$section
$($diff.Property)
$($diff.Issue)
$($diff.StagingValue)
$($diff.ProdValue)
"@
}
# Add detailed sections
$htmlReport += @"
Detailed Comparisons
$($detailedSections["ContentTypes"])
$($detailedSections["SitePagesContentTypes"])
$($detailedSections["SiteFeatures"])
$($detailedSections["WebFeatures"])
$($detailedSections["InstalledApps"])
$($detailedSections["ListsAndLibraries"])
"@
# Add Property Bag detailed comparison
$htmlReport += @"
Property Bag - Detailed Comparison
"@
if ($staging.PropertyBag -and $prod.PropertyBag) {
$allKeys = @()
$allKeys += $staging.PropertyBag.PSObject.Properties.Name
$allKeys += $prod.PropertyBag.PSObject.Properties.Name
$allKeys = $allKeys | Select-Object -Unique | Sort-Object
foreach ($key in $allKeys) {
$stagingVal = $staging.PropertyBag.$key
$prodVal = $prod.PropertyBag.$key
$status = ""
$rowClass = ""
if ($stagingVal -and $prodVal) {
if ($stagingVal -eq $prodVal) {
$status = "✓ Same"
$rowClass = "info"
}
else {
$status = "⚠ Different"
$rowClass = "warning"
}
}
elseif ($stagingVal -and -not $prodVal) {
$status = "⚠ Missing in Prod"
$rowClass = "warning"
}
elseif (-not $stagingVal -and $prodVal) {
$status = "⚠ Missing in Staging"
$rowClass = "warning"
}
# Highlight search-related properties
if ($key -match "search|crawl|index|scope" -and $status -ne "✓ Same") {
$rowClass = "critical"
}
$htmlReport += @"
"@
}
}
$htmlReport += @"
Property Key
Staging Value
Prod Value
Status
$key
$($stagingVal)
$($prodVal)
$status
"@
$htmlReport += @"
Key Areas to Check:
Search Settings: Check NoCrawl, SearchScope settings
Content Types: Verify ContentTypeId values match
Site Features: Ensure all features are activated
Site Pages Library: Check NoCrawl and moderation settings
Indexing Status: Verify site is being crawled
Property Bag: Look for search-related custom properties
"@
# Save HTML report
$htmlReport | Out-File -FilePath $OutputReportPath -Encoding UTF8
# Also save CSV for easy filtering
$csvPath = $OutputReportPath -replace '\.html$', '.csv'
$differences | Export-Csv -Path $csvPath -NoTypeInformation
Write-Host "`nComparison complete!" -ForegroundColor Green
Write-Host "HTML Report: $OutputReportPath" -ForegroundColor Yellow
Write-Host "CSV Report: $csvPath" -ForegroundColor Yellow
Write-Host "`nTotal differences found: $($differences.Count)" -ForegroundColor Cyan
# Display critical differences in console
$criticalDiffs = $differences | Where-Object { $_.Property -match "SearchSettings|NoCrawl|IndexingStatus|ContentType" }
if ($criticalDiffs) {
Write-Host "`nCRITICAL DIFFERENCES (may affect search):" -ForegroundColor Red
$criticalDiffs | Format-Table -AutoSize
}
# Open the report
Start-Process $OutputReportPath
Usage Notes
- Update the site URLs to match your SharePoint environment
- Run the first script twice, once for each site you want to check against the other
- Then make sure the names and path in the second script are the same as the output from the first script
- Review permissions before running to ensure adequate access
- Extend the comparison logic to include additional site properties as needed
What Gets Compared
- Basic Properties: Title, description, language, template, UI version
- Features: Site collection and web-level features
- Lists & Libraries: Conten types
- Content Types: Lists and library
- Site Columns: Custom webparts
- Navigation: Navigation