Last active
June 9, 2025 10:57
-
-
Save MartinMiles/56579e3a2c103f913991710f4ed89ce7 to your computer and use it in GitHub Desktop.
We need to find which nested placeholders are defined within renderings on a page, this script does it (say, for `ArticleAsideLeft` it returns `col-wide-1-0-1`). We need this later to set `Layout Service Placeholders` field on XM Cloud renderings
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | |
# Configuration | |
$siteRoot = "/sitecore/content/Zont/Habitat" | |
$deviceName = "Default" | |
$componentFieldId = "{037FE404-DD19-4BF7-8E30-4DADF68B27B0}" | |
$jsonTemplateId = "{5A4F1511-DC87-41F2-8B85-CAC25C30E08F}" | |
# Debugging | |
$debugRenderingId = '' #"{A85A0B65-D3B7-48B8-B362-F17AA3CA7DF1}" | |
$debugPlaceholder = '' #"col-narrow-2" | |
# 1) Mount master drive if needed | |
if (-not (Get-PSDrive -Name master -ErrorAction SilentlyContinue)) { | |
New-PSDrive -Name master -PSProvider Sitecore -Root "/" -Database "master" | Out-Null | |
} | |
# 2) Get databases & devices | |
$database = [Sitecore.Configuration.Factory]::GetDatabase("master") | |
$rootItem = $database.GetItem($siteRoot) | |
if (-not $rootItem) { throw "Site root '$siteRoot' not found." } | |
$device = $database.Resources.Devices.GetAll() | Where-Object { $_.Name -eq $deviceName } | |
if (-not $device) { throw "Device '$deviceName' not found." } | |
# 3) Build placeholder-key > definition-ID map | |
$fieldId = [Sitecore.Data.ID]::Parse("{7256BDAB-1FD2-49DD-B205-CB4873D2917C}") # Placeholder Key field | |
$templateId = [Sitecore.Data.ID]::Parse("{5C547D4E-7111-4995-95B0-6B561751BF2E}") # Placeholder Settings template | |
$phSettingsRoot = $database.GetItem("/sitecore/layout/Placeholder Settings") | |
if (-not $phSettingsRoot) { throw "Placeholder Settings folder not found." } | |
$allPhSettings = @($phSettingsRoot.Axes.GetDescendants()) | |
$placeholderMap = @{} | |
foreach ($ps in $allPhSettings) { | |
if ($ps.TemplateID -eq $templateId) { | |
$keyVal = $ps.Fields[$fieldId].Value | |
if ($keyVal -and -not $placeholderMap.ContainsKey($keyVal)) { | |
# store the item ID (includes braces) | |
$placeholderMap[$keyVal] = $ps.ID.ToString() | |
} | |
} | |
} | |
# 4) Gather items with presentation | |
$itemsWithPresentation = $rootItem.Axes.GetDescendants() | Where-Object { | |
$_["__Renderings"] -or ($_.__StandardValues -and $_.__StandardValues["__Renderings"]) | |
} | |
# 5) Initialize caches & map | |
$renderingMap = @{} | |
$renderingCache = @{} | |
foreach ($item in $itemsWithPresentation) { | |
try { | |
$renderings = $item.Visualization.GetRenderings($device, $true) | |
if (-not $renderings) { continue } | |
# collect all placeholder-paths on this page | |
$placeholderPaths = $renderings | | |
Where-Object { $_.Placeholder } | | |
ForEach-Object { $_.Placeholder.TrimStart("/") } | |
foreach ($ref in $renderings) { | |
$rid = $ref.RenderingID.ToString() | |
# a) Cache rendering metadata if missing | |
if (-not $renderingCache.ContainsKey($rid)) { | |
$rItem = $database.GetItem($ref.RenderingID) | |
if (-not $rItem) { continue } | |
# always pull the component-name field | |
$comp = "" | |
if ($rItem.Fields[$componentFieldId] -and $rItem.Fields[$componentFieldId].HasValue) { | |
$comp = $rItem.Fields[$componentFieldId].Value | |
} | |
$renderingCache[$rid] = @{ | |
RenderingPath = $rItem.Paths.FullPath | |
ComponentName = $comp | |
} | |
} | |
$meta = $renderingCache[$rid] | |
$prefix = $ref.Placeholder.TrimStart("/") | |
# b) Determine declared placeholders (normalized) | |
$declaredPlaceholders = $placeholderPaths | | |
Where-Object { $_ -like "$prefix/*" } | | |
ForEach-Object { | |
$rel = $_.Substring($prefix.Length + 1) | |
if ($rel -notmatch "/") { $rel } else { $rel.Split("/")[0] } | |
} | | |
Sort-Object -Unique | | |
ForEach-Object { | |
# strip dynamic suffix e.g. section-{GUID}-0 > section | |
if ($_ -match '^(.+?)-\{[0-9A-Fa-f-]+\}-\d+$') { $matches[1] } | |
else { $_ } | |
} | |
# c) Ensure map entry | |
if (-not $renderingMap.ContainsKey($rid)) { | |
$renderingMap[$rid] = [ordered]@{ | |
RenderingID = $rid | |
ComponentName = $meta.ComponentName | |
RenderingPath = $meta.RenderingPath | |
Placeholders = @() # will become array of keys | |
} | |
} | |
# d) Populate and debug | |
foreach ($ph in $declaredPlaceholders) { | |
if ($rid -eq $debugRenderingId -and $ph -eq $debugPlaceholder) { | |
$slotPath = "$prefix/$ph" | |
$childRefs = $renderings | Where-Object { | |
$_.Placeholder.TrimStart("/") -eq $slotPath | |
} | |
foreach ($childRef in $childRefs) { | |
$childRid = $childRef.RenderingID.ToString() | |
# resolve child path | |
if (-not $renderingCache.ContainsKey($childRid)) { | |
$cItem = $database.GetItem($childRef.RenderingID) | |
$cPath = if ($cItem) { $cItem.Paths.FullPath } else { "[unknown]" } | |
} else { | |
$cPath = $renderingCache[$childRid].RenderingPath | |
} | |
Write-Host "Debug: about to add placeholder '$ph' for rendering $debugRenderingId on page ID $($item.ID) - $($item.Paths.FullPath); child rendering $childRid ($cPath)" | |
} | |
} | |
if (-not ($renderingMap[$rid].Placeholders -contains $ph)) { | |
$renderingMap[$rid].Placeholders += $ph | |
} | |
} | |
} | |
} | |
catch { | |
throw "Error on $($item.Paths.FullPath): $_" | |
} | |
} | |
# 6) Convert placeholder-name arrays > pipe-separated definition IDs | |
foreach ($entry in $renderingMap.Values) { | |
$ids = @() | |
foreach ($key in $entry.Placeholders) { | |
if ($placeholderMap.ContainsKey($key)) { | |
$ids += $placeholderMap[$key] | |
} | |
} | |
# replace the array with a string of GUIDs joined by '|' | |
$entry.Placeholders = $ids -join "|" | |
} | |
# 7) Filter out empty placeholders and emit JSON | |
$filteredOutput = $renderingMap.Values | Where-Object { $_.Placeholders } | |
($filteredOutput | ConvertTo-Json -Depth 10) -replace "\s+", "" | |
} | |
# Tear down session | |
Stop-ScriptSession -Session $session |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$siteRoot = "/sitecore/content/Zont/Habitat" | |
$deviceName = "Default" | |
$componentFieldId = "{037FE404-DD19-4BF7-8E30-4DADF68B27B0}" | |
$jsonTemplateId = "{5A4F1511-DC87-41F2-8B85-CAC25C30E08F}" | |
# Mount master drive if needed | |
if (-not (Get-PSDrive -Name master -ErrorAction SilentlyContinue)) { | |
New-PSDrive -Name master -PSProvider Sitecore -Root "/" -Database "master" | Out-Null | |
} | |
$database = [Sitecore.Configuration.Factory]::GetDatabase("master") | |
$rootItem = $database.GetItem($siteRoot) | |
if (-not $rootItem) { throw "Site root '$siteRoot' not found." } | |
$device = $database.Resources.Devices.GetAll() | Where-Object { $_.Name -eq $deviceName } | |
if (-not $device) { throw "Device '$deviceName' not found." } | |
$itemsWithPresentation = $rootItem.Axes.GetDescendants() | Where-Object { | |
$_["__Renderings"] -or ($_.__StandardValues -and $_.__StandardValues["__Renderings"]) | |
} | |
$total = $itemsWithPresentation.Count | |
$current = 0 | |
$renderingMap = @{} | |
$renderingCache = @{} | |
foreach ($item in $itemsWithPresentation) { | |
$current++ | |
Write-Progress -Activity "Processing Pages" ` | |
-Status "$current of $total" ` | |
-PercentComplete (($current / $total) * 100) | |
try { | |
$renderings = $item.Visualization.GetRenderings($device, $true) | |
if (-not $renderings) { continue } | |
$placeholderPaths = $renderings | | |
Where-Object { $_.Placeholder } | | |
ForEach-Object { $_.Placeholder.TrimStart("/") } | |
foreach ($ref in $renderings) { | |
$rid = $ref.RenderingID.ToString() | |
# Cache rendering metadata | |
if (-not $renderingCache.ContainsKey($rid)) { | |
$rItem = $database.GetItem($ref.RenderingID) | |
if (-not $rItem) { continue } | |
$path = $rItem.Paths.FullPath | |
$comp = "" | |
if ( | |
$rItem.TemplateID.ToString().ToUpper() -eq $jsonTemplateId ` | |
-and $rItem.Fields[$componentFieldId].HasValue | |
) { | |
$comp = $rItem.Fields[$componentFieldId].Value | |
} | |
$renderingCache[$rid] = @{ | |
RenderingPath = $path | |
ComponentName = $comp | |
} | |
} | |
$meta = $renderingCache[$rid] | |
$prefix = $ref.Placeholder.TrimStart("/") | |
$declaredPlaceholders = $placeholderPaths | | |
Where-Object { $_ -like "$prefix/*" } | | |
ForEach-Object { | |
$rel = $_.Substring($prefix.Length + 1) | |
if ($rel -notmatch "/") { $rel } else { $rel.Split("/")[0] } | |
} | | |
Sort-Object -Unique | |
if (-not $renderingMap.ContainsKey($rid)) { | |
$renderingMap[$rid] = [ordered]@{ | |
RenderingID = $rid | |
ComponentName = $meta.ComponentName | |
RenderingPath = $meta.RenderingPath | |
Placeholders = @() | |
} | |
} | |
foreach ($ph in $declaredPlaceholders) { | |
if (-not ($renderingMap[$rid].Placeholders -contains $ph)) { | |
$renderingMap[$rid].Placeholders += $ph | |
} | |
} | |
} | |
} | |
catch { | |
throw "Error on $($item.Paths.FullPath): $_" | |
} | |
} | |
# Only keep renderings that have at least one placeholder | |
$filteredOutput = $renderingMap.Values | Where-Object { $_.Placeholders.Count -gt 0 } | |
# Output compact JSON | |
($filteredOutput | ConvertTo-Json -Depth 10) -replace "\s+", "" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$itemPath = "/sitecore/content/Zont/Habitat/Home/AAA" | |
if (-not (Get-PSDrive -Name master -ErrorAction SilentlyContinue)) { | |
New-PSDrive -Name master -PSProvider Sitecore -Root "/" -Database "master" | Out-Null | |
} | |
$database = [Sitecore.Configuration.Factory]::GetDatabase("master") | |
$pageItem = $database.GetItem($itemPath) | |
if (-not $pageItem) { return } | |
$deviceItem = $database.Resources.Devices.GetAll() | Where-Object { $_.Name -eq "Default" } | |
if (-not $deviceItem) { $deviceItem = $database.Resources.Devices.GetAll()[0] } | |
$renderingRefs = $pageItem.Visualization.GetRenderings($deviceItem, $true) | |
if (-not $renderingRefs) { | |
"[]" | |
return | |
} | |
# Step 1: Get all placeholder paths | |
$allPaths = $renderingRefs | | |
Where-Object { $_.Placeholder } | | |
ForEach-Object { $_.Placeholder.TrimStart("/") } | |
# Step 2: Build UID → Rendering info dictionary | |
$uidToInfo = @{} | |
foreach ($ref in $renderingRefs) { | |
$uid = $ref.UniqueId.ToString() | |
$defItem = $database.GetItem($ref.RenderingID) | |
if ($defItem -and -not $uidToInfo.ContainsKey($uid)) { | |
$componentName = "" | |
$jsonTemplate = $database.GetItem("/sitecore/templates/Foundation/JavaScript Services/Json Rendering") | |
if ($jsonTemplate -and $defItem.TemplateID -eq $jsonTemplate.ID) { | |
$componentName = $defItem["componentName"] | |
} | |
$uidToInfo[$uid] = @{ | |
RenderingID = $ref.RenderingID.ToString() | |
ComponentName = $componentName | |
RenderingPath = $defItem.Paths.FullPath | |
DeclaredPlaceholders = @() | |
PrefixPath = $ref.Placeholder.TrimStart("/") | |
} | |
} | |
} | |
# Step 3: For each UID, collect placeholders it introduces | |
foreach ($uid in $uidToInfo.Keys) { | |
$prefix = $uidToInfo[$uid].PrefixPath | |
$declared = $allPaths | Where-Object { | |
$_ -like "$prefix/*" | |
} | ForEach-Object { | |
$relative = $_.Substring($prefix.Length + 1) | |
if ($relative -notmatch "/") { | |
$relative | |
} else { | |
$relative.Split("/")[0] | |
} | |
} | Sort-Object -Unique | |
$uidToInfo[$uid].DeclaredPlaceholders = $declared | |
} | |
# Step 4: Build final ordered output | |
$final = @() | |
foreach ($uid in $uidToInfo.Keys) { | |
$entry = [ordered]@{ | |
RenderingID = $uidToInfo[$uid].RenderingID | |
ComponentName = $uidToInfo[$uid].ComponentName | |
RenderingPath = $uidToInfo[$uid].RenderingPath | |
Placeholders = $uidToInfo[$uid].DeclaredPlaceholders | |
} | |
if (-not $entry.Placeholders) { | |
$entry.Placeholders = @() | |
} | |
$final += $entry | |
} | |
# Step 5: Output minified JSON | |
($final | ConvertTo-Json -Depth 10) -replace "\s+", "" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$siteRoot = "/sitecore/content/Zont/Habitat" | |
$deviceName = "Default" | |
$componentFieldId = "{037FE404-DD19-4BF7-8E30-4DADF68B27B0}" | |
$jsonTemplateId = "{5A4F1511-DC87-41F2-8B85-CAC25C30E08F}" | |
$debugRenderingId = '' # "{A85A0B65-D3B7-48B8-B362-F17AA3CA7DF1}" | |
$debugPlaceholder = '' # "col-narrow-2" | |
# Mount master drive if needed | |
if (-not (Get-PSDrive -Name master -ErrorAction SilentlyContinue)) { | |
New-PSDrive -Name master -PSProvider Sitecore -Root "/" -Database "master" | Out-Null | |
} | |
$database = [Sitecore.Configuration.Factory]::GetDatabase("master") | |
$rootItem = $database.GetItem($siteRoot) | |
if (-not $rootItem) { throw "Site root '$siteRoot' not found." } | |
$device = $database.Resources.Devices.GetAll() | Where-Object { $_.Name -eq $deviceName } | |
if (-not $device) { throw "Device '$deviceName' not found." } | |
$itemsWithPresentation = $rootItem.Axes.GetDescendants() | Where-Object { | |
$_["__Renderings"] -or ($_.__StandardValues -and $_.__StandardValues["__Renderings"]) | |
} | |
$renderingMap = @{} | |
$renderingCache = @{} | |
foreach ($item in $itemsWithPresentation) { | |
try { | |
$renderings = $item.Visualization.GetRenderings($device, $true) | |
if (-not $renderings) { continue } | |
# collect placeholder keys on this page | |
$placeholderPaths = $renderings | | |
Where-Object { $_.Placeholder } | | |
ForEach-Object { $_.Placeholder.TrimStart("/") } | |
foreach ($ref in $renderings) { | |
$rid = $ref.RenderingID.ToString() | |
# cache rendering metadata if missing | |
if (-not $renderingCache.ContainsKey($rid)) { | |
$rItem = $database.GetItem($ref.RenderingID) | |
if (-not $rItem) { continue } | |
# always read component name field | |
$comp = "" | |
if ($rItem.Fields[$componentFieldId] -and $rItem.Fields[$componentFieldId].HasValue) { | |
$comp = $rItem.Fields[$componentFieldId].Value | |
} | |
$renderingCache[$rid] = @{ | |
RenderingPath = $rItem.Paths.FullPath | |
ComponentName = $comp | |
} | |
} | |
$meta = $renderingCache[$rid] | |
$prefix = $ref.Placeholder.TrimStart("/") | |
# determine declared placeholders | |
$declaredPlaceholders = $placeholderPaths | | |
Where-Object { $_ -like "$prefix/*" } | | |
ForEach-Object { | |
$rel = $_.Substring($prefix.Length + 1) | |
if ($rel -notmatch "/") { $rel } | |
else { $rel.Split("/")[0] } | |
} | | |
Sort-Object -Unique | | |
ForEach-Object { | |
# normalize dynamic keys: section-{GUID}-n → section | |
if ($_ -match '^(.+?)-\{[0-9A-Fa-f-]+\}-\d+$') { $matches[1] } | |
else { $_ } | |
} | |
# ensure map entry | |
if (-not $renderingMap.ContainsKey($rid)) { | |
$renderingMap[$rid] = [ordered]@{ | |
RenderingID = $rid | |
ComponentName = $meta.ComponentName | |
RenderingPath = $meta.RenderingPath | |
Placeholders = @() | |
} | |
} | |
foreach ($ph in $declaredPlaceholders) { | |
# debug for our target rendering and placeholder | |
if ($rid -eq $debugRenderingId -and $ph -eq $debugPlaceholder) { | |
$slotPath = "$prefix/$ph" | |
$childRefs = $renderings | Where-Object { | |
$_.Placeholder.TrimStart("/") -eq $slotPath | |
} | |
foreach ($childRef in $childRefs) { | |
$childRid = $childRef.RenderingID.ToString() | |
# resolve child rendering path without ?: | |
if (-not $renderingCache.ContainsKey($childRid)) { | |
$cItem = $database.GetItem($childRef.RenderingID) | |
if ($cItem) { | |
$cPath = $cItem.Paths.FullPath | |
} | |
else { | |
$cPath = "[unknown]" | |
} | |
} | |
else { | |
$cPath = $renderingCache[$childRid].RenderingPath | |
} | |
Write-Host "Debug: about to add placeholder '$ph' for rendering $debugRenderingId on page ID $($item.ID) - $($item.Paths.FullPath); child rendering $childRid ($cPath)" | |
} | |
} | |
# add placeholder if new | |
if (-not ($renderingMap[$rid].Placeholders -contains $ph)) { | |
$renderingMap[$rid].Placeholders += $ph | |
} | |
} | |
} | |
} | |
catch { | |
throw "Error on $($item.Paths.FullPath): $_" | |
} | |
} | |
# filter out empty placeholders | |
$filteredOutput = $renderingMap.Values | Where-Object { $_.Placeholders.Count -gt 0 } | |
# output compact JSON | |
($filteredOutput | ConvertTo-Json -Depth 10) -replace "\s+", "" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
You're a world-class Sitecore PowerShell Extensions (SPE) expert. Write a single Sitecore PowerShell ISE script that: | |
- Targets the item at `/sitecore/content/Zont/Habitat/Home/DDD` | |
- Inspects the page's Final Layout definition for the "Default" device | |
- For each rendering instance (UID) on the page, produces a JSON array containing: | |
1. `"RenderingID"`: the rendering definition ID (r:id from layout XML) | |
2. `"ComponentName"`: the value of the `componentName` field (field ID `{037FE404-DD19-4BF7-8E30-4DADF68B27B0}`) **only if** the rendering is based on the `/sitecore/templates/Foundation/JavaScript Services/Json Rendering` template — otherwise empty | |
3. `"RenderingPath"`: full path of the rendering item | |
4. `"Placeholders"`: an array of placeholder keys **declared inside** this rendering instance. These are sub-placeholders *within* the rendering, **not** the one it is placed into. If none — return an empty array | |
Additional requirements: | |
- Field order must be exactly: `RenderingID`, `ComponentName`, `RenderingPath`, then `Placeholders` | |
- Output must be in minified JSON | |
- Use `[ordered]@{}` to guarantee key order | |
- Placeholders must strip parent path prefixes (e.g. if rendering is in `/headless-main`, and declares `/headless-main/col-wide-1-0-1`, return only `"col-wide-1-0-1"`) | |
Return the full code as a single copy-pastable script ready to execute in Sitecore PowerShell ISE. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
You're a world-renowned Sitecore PowerShell Extensions expert. Write a fully optimized Sitecore PowerShell ISE script that performs the following: | |
Goal: | |
Iterate all page items under the path `/sitecore/content/Zont/Habitat` (default site root), and build a single merged JSON output mapping all unique renderings to the placeholders they declare inside themselves. | |
Requirements: | |
- A "page" is any item under the site root that has Final or Shared layout set, directly or via standard values (check `__Renderings`) | |
- For each unique rendering (identified by RenderingID), return: | |
1. `"RenderingID"` – rendering definition ID | |
2. `"ComponentName"` – the value of the `componentName` field (field ID `{037FE404-DD19-4BF7-8E30-4DADF68B27B0}`) if the rendering uses the `/sitecore/templates/Foundation/JavaScript Services/Json Rendering` template (template ID `{5A4F1511-DC87-41F2-8B85-CAC25C30E08F}`); otherwise empty | |
3. `"RenderingPath"` – full path to the rendering item | |
4. `"Placeholders"` – list of unique placeholders declared *inside* the rendering, determined by analyzing subpaths of the placeholder it is assigned to (e.g., if rendering is in `headless-main`, and declares `headless-main/col-wide-1-0-1`, only return `col-wide-1-0-1`) | |
Behavior: | |
- Must run for the `Default` device only | |
- Must merge placeholders across multiple uses of the same rendering (same RenderingID) across different pages | |
- Must show progress per page item | |
- Must stop on first error and report which item failed | |
- Output must be a minified JSON array with fields ordered: `"RenderingID"`, `"ComponentName"`, `"RenderingPath"`, `"Placeholders"` | |
Optimize for performance: | |
- Cache rendering items by RenderingID | |
- Use only necessary string operations (trim/split) | |
- Avoid repeated field lookups or rendering template checks | |
Final Output: | |
Return a single `[ordered]@{}` object per rendering definition, collect them into an array, and output as minified JSON at the end using: | |
```powershell | |
($finalOutput | ConvertTo-Json -Depth 10) -replace "\s+", "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Output for a singe page script:
