Skip to content

Instantly share code, notes, and snippets.

@MartinMiles
Last active June 9, 2025 10:36
Show Gist options
  • Save MartinMiles/d0367398247cbbd280386998f750efdd to your computer and use it in GitHub Desktop.
Save MartinMiles/d0367398247cbbd280386998f750efdd to your computer and use it in GitHub Desktop.
<#
.SYNOPSIS
Lists every item (and its template's Standard Values) that actually has
presentation details (shared and/or final layouts) beneath a chosen site root.
.PARAMETER SiteRoot
The content path to start from. Defaults to "/sitecore/content/Zont/Habitat/Home".
.PARAMETER DatabaseName
The Sitecore database to query (master/web/old/etc.). Defaults to "master".
#>
param(
[string]$SiteRoot = "/sitecore/content/Zont/Habitat/Home",
[string]$DatabaseName = "master"
)
# ---------------------------------------------------------------------------
# Safety first
# ---------------------------------------------------------------------------
$ErrorActionPreference = 'Stop'
# ---------------------------------------------------------------------------
# Ensure the drive for the requested database exists
# ---------------------------------------------------------------------------
if (-not (Get-PSDrive -Name $DatabaseName -ErrorAction SilentlyContinue)) {
New-PSDrive -Name $DatabaseName -PSProvider Sitecore -Root "/" -Database $DatabaseName -ErrorAction Stop | Out-Null
}
$rootPath = "${DatabaseName}:$SiteRoot"
if (-not (Test-Path $rootPath -ErrorAction SilentlyContinue)) {
throw "Site root '$SiteRoot' not found in database '$DatabaseName'."
}
# Helper – true if either shared OR final layout XML is present
function Test-Presentation {
param([string]$layoutFieldValue)
if ([string]::IsNullOrWhiteSpace($layoutFieldValue)) { return $false }
$parts = $layoutFieldValue -split '¤', 2
$sharedXml = $parts[0]
$finalXml = if ($parts.Length -gt 1) { $parts[1] } else { "" }
return -not [string]::IsNullOrWhiteSpace($sharedXml) -or
-not [string]::IsNullOrWhiteSpace($finalXml)
}
# ---------------------------------------------------------------------------
# Gather all items with presentation and their Standard Values (if any)
# ---------------------------------------------------------------------------
$results = @()
$allItems = Get-ChildItem -Path $rootPath -Recurse -ErrorAction Stop
$index = 0
$total = $allItems.Count
foreach ($item in $allItems) {
$index++
Write-Progress -Activity "Scanning presentation" `
-Status "$index of $total items" `
-PercentComplete (($index / $total) * 100)
$rawLayout = $item["__Renderings"]
if (Test-Presentation $rawLayout) {
# Record the page item itself
$results += [PSCustomObject]@{
ItemPath = $item.Paths.Path
ItemID = $item.ID.Guid.ToString("B").ToUpper()
Kind = "Page"
SharedLayout = (-not [string]::IsNullOrWhiteSpace(($rawLayout -split '¤',2)[0]))
FinalLayout = (($rawLayout -split '¤',2).Length -gt 1) -and
(-not [string]::IsNullOrWhiteSpace(($rawLayout -split '¤',2)[1]))
}
# Now inspect the template's Standard Values (only once per template)
$stdItem = $item.Template.StandardValues
if ($stdItem -and -not ($results | Where-Object { $_.ItemID -eq $stdItem.ID.Guid.ToString("B").ToUpper() })) {
$stdLayout = $stdItem["__Renderings"]
if (Test-Presentation $stdLayout) {
$results += [PSCustomObject]@{
ItemPath = $stdItem.Paths.Path
ItemID = $stdItem.ID.Guid.ToString("B").ToUpper()
Kind = "Standard Values"
SharedLayout = (-not [string]::IsNullOrWhiteSpace(($stdLayout -split '¤',2)[0]))
FinalLayout = (($stdLayout -split '¤',2).Length -gt 1) -and
(-not [string]::IsNullOrWhiteSpace(($stdLayout -split '¤',2)[1]))
}
}
}
}
}
Write-Progress -Activity "Scanning presentation" -Completed -Status "Done"
# ---------------------------------------------------------------------------
# Present the findings
# ---------------------------------------------------------------------------
if ($results.Count -eq 0) {
Write-Warning "No items under '$SiteRoot' have presentation in database '$DatabaseName'."
} else {
$results |
Sort-Object -Property ItemPath, Kind |
Format-Table -Property ItemPath, Kind, SharedLayout, FinalLayout -AutoSize
}
<#
Script: Replace Multiple Placeholders Across Entire Site
Site root: default "/sitecore/content/Zont/Habitat"
DB: master
- Processes every page item under site root and each template’s __Standard Values.
- Updates Shared (__Renderings) and Final (__Final Renderings) layouts.
- Swaps three placeholder names.
- Logs all changes in a summary table.
#>
param(
[string] $SiteRoot = "/sitecore/content/Zont/Habitat",
[string] $OldPlaceholder1 = "page-layout",
[string] $NewPlaceholder1 = "headless-main",
[string] $OldPlaceholder2 = "header-top",
[string] $NewPlaceholder2 = "headless-header",
[string] $OldPlaceholder3 = "footer",
[string] $NewPlaceholder3 = "headless-footer"
)
# 1) Mount master: if absent
if (-not (Get-PSDrive -Name master -ErrorAction SilentlyContinue)) {
New-PSDrive -Name master -PSProvider Sitecore -Root "/" -Database "master" -ErrorAction Stop | Out-Null
}
# 2) Get master database
$db = [Sitecore.Configuration.Factory]::GetDatabase("master")
if (-not $db) { throw "Cannot load master database." }
# 3) Prepare global log
$script:logEntries = New-Object System.Collections.ArrayList
# 4) Function: parse layout XML & swap placeholders
function Replace-PlaceholdersInLayout {
param(
[string] $layoutXml,
[string] $contextItemID,
[string] $contextName,
[string] $languageName
)
$layoutDef = [Sitecore.Layouts.LayoutDefinition]::Parse($layoutXml)
$changed = $false
# mapping pairs
$mappings = @(
@{ Old = $OldPlaceholder1; New = $NewPlaceholder1 },
@{ Old = $OldPlaceholder2; New = $NewPlaceholder2 },
@{ Old = $OldPlaceholder3; New = $NewPlaceholder3 }
)
foreach ($devDef in $layoutDef.Devices) {
$deviceName = $devDef.Name
foreach ($rendDef in $devDef.Renderings) {
$oldPh = $rendDef.Placeholder
if ([string]::IsNullOrEmpty($oldPh)) { continue }
$newPh = $null
foreach ($map in $mappings) {
if ($oldPh -eq $map.Old) {
$newPh = $map.New; break
}
$prefix = "/$($map.Old)/"
if ($oldPh.StartsWith($prefix)) {
$suffix = $oldPh.Substring($prefix.Length)
$newPh = "/$($map.New)/$suffix"; break
}
}
if ($newPh -and $newPh -ne $oldPh) {
$rendDef.Placeholder = $newPh
$changed = $true
$entry = [PSCustomObject]@{
ItemID = $contextItemID
Context = $contextName
Language = $languageName
Device = $deviceName
RenderingID = $rendDef.RenderingID
OldPlaceholder = $oldPh
NewPlaceholder = $newPh
}
$null = $script:logEntries.Add($entry)
}
}
}
if ($changed) {
return $layoutDef.ToXml()
}
else {
return $null
}
}
# 5) Load all page items under site root
$siteRootItem = $db.GetItem($SiteRoot)
if (-not $siteRootItem) {
throw "Site root not found at path: $SiteRoot"
}
$allItems = @( $siteRootItem ) + ( Get-ChildItem -Path "master:$SiteRoot" -Recurse )
# 6) Process Standard Values for every unique template
$allItems |
Select-Object -ExpandProperty TemplateID -Unique |
ForEach-Object {
$tplId = $_.ToString()
$templateItem = $db.GetItem($tplId)
if (-not $templateItem) {
Write-Warning "Template not found: $tplId. Skipping Standard Values."
return
}
$std = $templateItem.Children | Where-Object Name -EQ "__Standard Values"
if (-not $std) {
Write-Host "ℹ️ No __Standard Values under template: $($templateItem.Name)"
return
}
# Shared Layout
$fld = $std.Fields["__Renderings"]
if ($fld -and $fld.Value.Trim()) {
$newXml = Replace-PlaceholdersInLayout `
-layoutXml $fld.Value `
-contextItemID $std.ID.ToString() `
-contextName "Standard Values Shared" `
-languageName ""
if ($newXml) {
$std.Editing.BeginEdit()
try { $fld.Value = $newXml; Write-Host "✅ Updated Standard Values Shared for $($templateItem.Name)" }
finally { $std.Editing.EndEdit() }
}
}
# Final Layout
$fld2 = $std.Fields["__Final Renderings"]
if ($fld2 -and $fld2.Value.Trim()) {
$newXml = Replace-PlaceholdersInLayout `
-layoutXml $fld2.Value `
-contextItemID $std.ID.ToString() `
-contextName "Standard Values Final" `
-languageName ""
if ($newXml) {
$std.Editing.BeginEdit()
try { $fld2.Value = $newXml; Write-Host "✅ Updated Standard Values Final for $($templateItem.Name)" }
finally { $std.Editing.EndEdit() }
}
}
}
# 7) Process each page item × each language
foreach ($item in $allItems) {
foreach ($lang in $item.Languages) {
$ver = $db.GetItem($item.ID, $lang)
if (-not $ver) {
Write-Host "⚠️ $($item.Paths.FullPath) missing in $($lang.Name). Skipping."
continue
}
# Shared (__Renderings)
$f = $ver.Fields["__Renderings"]
if ($f -and $f.Value.Trim()) {
$newXml = Replace-PlaceholdersInLayout `
-layoutXml $f.Value `
-contextItemID $ver.ID.ToString() `
-contextName "Shared Layout" `
-languageName $lang.Name
if ($newXml) {
$ver.Editing.BeginEdit()
try { $f.Value = $newXml; Write-Host "✅ [$($lang.Name)] Shared updated: $($ver.Paths.FullPath)" }
finally { $ver.Editing.EndEdit() }
}
}
# Final (__Final Renderings)
$f2 = $ver.Fields["__Final Renderings"]
if ($f2 -and $f2.Value.Trim()) {
$newXml = Replace-PlaceholdersInLayout `
-layoutXml $f2.Value `
-contextItemID $ver.ID.ToString() `
-contextName "Final Layout" `
-languageName $lang.Name
if ($newXml) {
$ver.Editing.BeginEdit()
try { $f2.Value = $newXml; Write-Host "✅ [$($lang.Name)] Final updated: $($ver.Paths.FullPath)" }
finally { $ver.Editing.EndEdit() }
}
}
}
}
# 8) Summary log
if ($script:logEntries.Count -gt 0) {
Write-Host "`n===== Placeholder Replacement Log =====`n"
$script:logEntries |
Sort-Object Context, Language, Device |
Format-Table `
@{Label="Item ID"; Expression={$_.ItemID}}, `
@{Label="Context"; Expression={$_.Context}}, `
@{Label="Language"; Expression={$_.Language}}, `
@{Label="Device"; Expression={$_.Device}}, `
@{Label="Rendering ID"; Expression={$_.RenderingID}}, `
@{Label="Old Placeholder";Expression={$_.OldPlaceholder}}, `
@{Label="New Placeholder";Expression={$_.NewPlaceholder}} `
-AutoSize
Write-Host "`nTotal changes: $($script:logEntries.Count)"
}
else {
Write-Host "`n✅ No matching placeholders found across the site. No updates made."
}
Set-Location -Path $PSScriptRoot
# Load connection settings
$config = Get-Content -Raw -Path ./config.LOCAL.json | ConvertFrom-Json
# Import SPE and start a remote session
Import-Module -Name SPE
$session = New-ScriptSession -ConnectionUri $config.connectionUri `
-Username $config.username `
-SharedSecret $config.SPE_REMOTING_SECRET
Invoke-RemoteScript -Session $session -ScriptBlock {
[string]$SiteRoot = "/sitecore/content/Zont/Habitat/Home";
[string]$LayoutItemPath = "/sitecore/layout/Layouts/Foundation/JSS Experience Accelerator/Presentation/JSS Layout";
[string]$DatabaseName = "master";
$ErrorActionPreference = 'Stop' # fail fast, full trace
# ------------------------------------------------------------------------------
# 1) Ensure the requested Sitecore drive exists
# ------------------------------------------------------------------------------
if (-not (Get-PSDrive -Name $DatabaseName -ErrorAction SilentlyContinue)) {
New-PSDrive -Name $DatabaseName -PSProvider Sitecore -Root "/" -Database $DatabaseName -ErrorAction Stop | Out-Null
}
# 2) Get the database object
$db = [Sitecore.Configuration.Factory]::GetDatabase($DatabaseName)
if ($null -eq $db) { throw "Cannot retrieve Sitecore database '$DatabaseName'." }
# 3) Resolve the target layout item once
$layoutItem = Get-Item -Path ("${DatabaseName}:" + $LayoutItemPath) -ErrorAction Stop
$layoutGuid = $layoutItem.ID.Guid.ToString("B").ToUpper()
# 4) Helper – returns $true if Shared or Final layout XML exists
function Test-Presentation {
param([string]$layoutFieldValue)
if ([string]::IsNullOrWhiteSpace($layoutFieldValue)) { return $false }
$parts = $layoutFieldValue -split '¤', 2
return (-not [string]::IsNullOrWhiteSpace($parts[0])) -or
(($parts.Length -gt 1) -and -not [string]::IsNullOrWhiteSpace($parts[1]))
}
# 5) Gather pages + template Standard Values with presentation
$rootPath = "${DatabaseName}:$SiteRoot"
if (-not (Test-Path $rootPath)) { throw "Site root '$SiteRoot' not found in '$DatabaseName'." }
$itemsToProcess = @()
$templateHandled = @{}
foreach ($item in Get-ChildItem -Path $rootPath -Recurse -ErrorAction Stop) {
if (Test-Presentation $item["__Renderings"]) {
$itemsToProcess += $item
# Also process template’s Standard Values (once per template)
$std = $item.Template.StandardValues
if ($std -and -not $templateHandled.ContainsKey($std.ID)) {
if (Test-Presentation $std["__Renderings"]) {
$itemsToProcess += $std
}
$templateHandled[$std.ID] = $true
}
}
}
if ($itemsToProcess.Count -eq 0) {
Write-Warning "No presentation found under '$SiteRoot'. Nothing to update."
return
}
# 6) Replace Shared Layout for a single item
function Set-SharedLayout {
param(
[Sitecore.Data.Items.Item]$TargetItem,
[string]$NewLayoutGuid
)
$sharedXml = $TargetItem.Fields[[Sitecore.FieldIDs]::LayoutField].Value
# Load or create <r> root
[xml]$xmlDoc = New-Object System.Xml.XmlDocument
if ([string]::IsNullOrWhiteSpace($sharedXml)) {
$xmlDoc.LoadXml('<r xmlns:p="p" xmlns:s="s" p:p="1"><d/></r>')
} else {
$xmlDoc.LoadXml($sharedXml)
}
$root = $xmlDoc.DocumentElement
$nsP = $root.GetNamespaceOfPrefix("p")
$nsS = $root.GetNamespaceOfPrefix("s")
$dNode = $xmlDoc.SelectSingleNode("/r/d")
if ($null -eq $dNode) { throw "Item $($TargetItem.Paths.Path) has no <d> node." }
$dNode.SetAttribute("l", $nsS, $NewLayoutGuid) # set layout
$dNode.SetAttribute("before",$nsP, "*") # keep renderings
$TargetItem.Editing.BeginEdit()
$TargetItem.Fields[[Sitecore.FieldIDs]::LayoutField].Value = $root.OuterXml
$TargetItem.Editing.EndEdit()
}
# 7) Loop through all collected items and update their Shared Layout
$processed = 0
foreach ($itm in $itemsToProcess) {
try {
Set-SharedLayout -TargetItem $itm -NewLayoutGuid $layoutGuid
Write-Host "? Updated: $($itm.Paths.Path)"
$processed++
}
catch {
if ($itm -and $itm.Editing.IsEditing) { $itm.Editing.CancelEdit() }
Write-Error "? Failed on $($itm.Paths.Path): $_"
throw # stop on first error
}
}
Write-Host "?? Finished. Updated $processed item(s). New Shared Layout > $LayoutItemPath"
}
# Tear down session
Stop-ScriptSession -Session $session
<#
.SYNOPSIS
• Scans all items beneath a site root (plus each template’s Standard Values)
to find those that already contain presentation details.
• For every such item it rewrites the Shared Layout to point to a
specific layout item – preserving all renderings and leaving the
Final Layout untouched.
• Stops on the first error and surfaces the full exception.
.PARAMETER SiteRoot
The starting content path. Default: "/sitecore/content/Zont/Habitat/Home".
.PARAMETER LayoutItemPath
The layout item that must replace the current Shared Layout.
Default: "/sitecore/layout/Layouts/Foundation/JSS Experience Accelerator/Presentation/JSS Layout".
.PARAMETER DatabaseName
Which Sitecore database to work in (master/web/old/etc.). Default: "master".
#>
param(
[string]$SiteRoot = "/sitecore/content/Zont/Habitat/Home",
[string]$LayoutItemPath = "/sitecore/layout/Layouts/Foundation/JSS Experience Accelerator/Presentation/JSS Layout",
[string]$DatabaseName = "master"
)
$ErrorActionPreference = 'Stop' # fail fast, surface all details
# ─────────────────────────────────────────────────────────────────────
# 1) Ensure the requested Sitecore drive exists
# ─────────────────────────────────────────────────────────────────────
if (-not (Get-PSDrive -Name $DatabaseName -ErrorAction SilentlyContinue)) {
New-PSDrive -Name $DatabaseName -PSProvider Sitecore -Root "/" -Database $DatabaseName -ErrorAction Stop | Out-Null
}
# 2) Grab the database object once
$db = [Sitecore.Configuration.Factory]::GetDatabase($DatabaseName)
if ($null -eq $db) { throw "Cannot retrieve Sitecore database '$DatabaseName'." }
# 3) Resolve the layout item just once (throw if missing)
$layoutItem = Get-Item -Path ("${DatabaseName}:" + $LayoutItemPath) -ErrorAction Stop
$layoutGuid = $layoutItem.ID.Guid.ToString("B").ToUpper()
# 4) Helper – detect any presentation in the Shared or Final XML
function Test-Presentation {
param([string]$layoutFieldValue)
if ([string]::IsNullOrWhiteSpace($layoutFieldValue)) { return $false }
$parts = $layoutFieldValue -split '¤', 2
$sharedXml = $parts[0]
$finalXml = if ($parts.Length -gt 1) { $parts[1] } else { "" }
return -not [string]::IsNullOrWhiteSpace($sharedXml) -or
-not [string]::IsNullOrWhiteSpace($finalXml)
}
# 5) Collect *pages* under the site root + each template’s Standard Values
$rootPath = "${DatabaseName}:$SiteRoot"
if (-not (Test-Path $rootPath)) { throw "Site root '$SiteRoot' not found in '$DatabaseName'." }
$itemsToProcess = @()
$templateDone = @{}
$allItems = Get-ChildItem -Path $rootPath -Recurse -ErrorAction Stop
$total = $allItems.Count
$index = 0
foreach ($item in $allItems) {
$index++
Write-Progress -Activity "Scanning for presentation" `
-Status "$index / $total" `
-PercentComplete (($index/$total)*100)
if (Test-Presentation $item["__Renderings"]) {
$itemsToProcess += $item
# also queue the template's Standard Values (once)
$std = $item.Template.StandardValues
if ($std -and -not $templateDone.ContainsKey($std.ID)) {
if (Test-Presentation $std["__Renderings"]) {
$itemsToProcess += $std
}
$templateDone[$std.ID] = $true
}
}
}
Write-Progress -Activity "Scanning for presentation" -Completed -Status "Done"
if ($itemsToProcess.Count -eq 0) {
Write-Warning "Nothing under '$SiteRoot' has presentation. No layout updates performed."
return
}
# 6) Function to replace Shared Layout on a single item
function Set-SharedLayout {
param(
[Sitecore.Data.Items.Item]$TargetItem,
[string]$NewLayoutGuid
)
# a) Read shared layout XML
$sharedXml = $TargetItem.Fields[[Sitecore.FieldIDs]::LayoutField].Value
# b) Load or create the <r> document
[xml]$xmlDoc = $null
if ([string]::IsNullOrWhiteSpace($sharedXml)) {
$xmlDoc = New-Object System.Xml.XmlDocument
$root = $xmlDoc.CreateElement("r")
$root.SetAttribute("xmlns:p","p")
$root.SetAttribute("xmlns:s","s")
$root.SetAttribute("p:p","1")
$xmlDoc.AppendChild($root) | Out-Null
} else {
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.LoadXml($sharedXml)
}
$root = $xmlDoc.DocumentElement
$nsP = $root.GetNamespaceOfPrefix("p")
$nsS = $root.GetNamespaceOfPrefix("s")
$dNode = $xmlDoc.SelectSingleNode("/r/d")
if ($null -eq $dNode) { throw "No <d> device node found under <r> for item $($TargetItem.Paths.Path)." }
# c) Overwrite s:l and p:before attributes
$dNode.SetAttribute("l", $nsS, $NewLayoutGuid)
$dNode.SetAttribute("before", $nsP, "*")
# d) Write back
$TargetItem.Editing.BeginEdit()
$TargetItem.Fields[[Sitecore.FieldIDs]::LayoutField].Value = $root.OuterXml
$TargetItem.Editing.EndEdit()
}
# 7) Loop through every collected item and replace its Shared Layout
$processed = 0
foreach ($itm in $itemsToProcess) {
$processed++
Write-Progress -Activity "Updating layouts" `
-Status "$processed / $($itemsToProcess.Count) : $($itm.Paths.Path)" `
-PercentComplete (($processed/$itemsToProcess.Count)*100)
try {
Set-SharedLayout -TargetItem $itm -NewLayoutGuid $layoutGuid
Write-Host "✅ Updated: $($itm.Paths.Path)"
}
catch {
if ($itm -and $itm.Editing.IsEditing) { $itm.Editing.CancelEdit() }
Write-Error "❌ Failed on $($itm.Paths.Path): $_"
throw # stop on first error as requested
}
}
Write-Progress -Activity "Updating layouts" -Completed -Status "Done"
Write-Host "`n🎉 Finished. Updated $processed item(s). New Shared Layout → $LayoutItemPath (ID $layoutGuid)."
@MartinMiles
Copy link
Author

Get-ItemsWithPresentation outputs:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment