Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save MartinMiles/56579e3a2c103f913991710f4ed89ce7 to your computer and use it in GitHub Desktop.
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
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
$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+", ""
$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+", ""
$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+", ""
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.
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+", ""
@MartinMiles
Copy link
Author

Output for a singe page script:
image

@MartinMiles
Copy link
Author

Output for a whole site script:
image

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