Skip to content

Instantly share code, notes, and snippets.

@MartinMiles
Created June 11, 2025 19:48
Show Gist options
  • Save MartinMiles/77162ed433da76e1bca002975ca611e4 to your computer and use it in GitHub Desktop.
Save MartinMiles/77162ed433da76e1bca002975ca611e4 to your computer and use it in GitHub Desktop.
<#
.SYNOPSIS
Download a page and parse Sitecore debug comments into a JSON hierarchy.
.DESCRIPTION
Scans a Sitecore-rendered HTML page for <!-- start-component='…' --> and <!-- end-component='…' -->
markers, extracts metadata (name, id, uid, placeholder, path), and reconstructs a nested
component-to-placeholder tree. Strict UID matching ensures proper pairing of start/end tags,
while allowing the root layout to remain on the stack at the end.
.PARAMETER Url
The page URL to download and parse. Defaults to the local Habitat example.
.PARAMETER OutputPath
Optional. File path to write the resulting JSON. If omitted, JSON is written to the console.
.EXAMPLE
# Default URL, output to console
.\Get-PlaceholdersStructure.ps1
.EXAMPLE
# Custom URL, output to file
.\Get-PlaceholdersStructure.ps1 -Url "http://example.com" -OutputPath "page.json"
#>
param(
[Parameter(Mandatory=$false)]
[string]$Url = 'http://rssbplatform.dev.local/',
[Parameter(Mandatory=$false)]
[string]$OutputPath
)
# Download HTML with timeout
try {
Write-Host "Downloading HTML from $Url"
$req = [System.Net.HttpWebRequest]::Create($Url)
$req.Timeout = 10000
$res = $req.GetResponse()
$reader = New-Object System.IO.StreamReader($res.GetResponseStream())
$content = $reader.ReadToEnd()
$reader.Close(); $res.Close()
} catch {
Write-Error "Error downloading '$Url': $_"
exit 1
}
# Capture component markers
$pattern = "<!--\s*(start-component|end-component)\s*=\s*'(?<json>\{.*?\})'\s*-->"
$matches = [regex]::Matches($content, $pattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
if ($matches.Count -eq 0) {
Write-Error "No component markers found."
exit 1
}
# Parse JSON-like component metadata
function Convert-ComponentStringToObject {
param([string]$str)
$json = $str -replace '(\w+)\s*:', '"$1":'
try { return $json | ConvertFrom-Json -ErrorAction Stop } catch { throw "Invalid metadata JSON: $str" }
}
# Define root layout signature
$rootUid = '00000000-0000-0000-0000-000000000000'
$rootPlaceholder = ''
$rootName = 'Default'
# Build hierarchy using strict UID matching
$stack = @()
$root = $null
foreach ($m in $matches) {
$type = $m.Groups[1].Value
$rawJson = $m.Groups['json'].Value
$meta = Convert-ComponentStringToObject $rawJson
$isRoot = ($meta.uid -eq $rootUid -and $meta.placeholder -eq $rootPlaceholder -and $meta.name -eq $rootName)
if ($type -eq 'start-component') {
# Create new node
$node = [PSCustomObject]@{
name = $meta.name
id = $meta.id
uid = $meta.uid
placeholder = $meta.placeholder
path = $meta.path
placeholders = @{}
}
if (-not $root) {
# First start defines root
$root = $node
} else {
# Add to parent placeholders
$parent = $stack[-1]
$ph = $meta.placeholder
if (-not $parent.placeholders.ContainsKey($ph)) {
$parent.placeholders[$ph] = @()
}
$parent.placeholders[$ph] += $node
}
# Always push for matching (including root)
$stack += $node
} else {
# end-component
if ($isRoot) {
# ignore root end, but do not pop
continue
}
if (-not $stack) { throw "End without matching start (UID: $($meta.uid))" }
$top = $stack[-1]
if ($top.uid -ne $meta.uid) { throw "UID mismatch: expected '$($top.uid)', got '$($meta.uid)'" }
# Pop matched node
$stack = $stack[0..($stack.Count-2)]
}
}
# Only root may remain unmatched
if ($stack.Count -gt 1) {
Write-Error "Unmatched components remain after parsing:"
foreach ($n in $stack[0..($stack.Count-2)]) { Write-Error " - $($n.name) [UID: $($n.uid)]" }
exit 1
}
if (-not $root) { Write-Error "No root component parsed."; exit 1 }
# Output JSON
$json = $root | ConvertTo-Json -Depth 20
if ($OutputPath) {
try { $json | Out-File $OutputPath -Encoding UTF8; Write-Host "JSON written to $OutputPath" } catch { Write-Error "Write error: $_"; exit 1 }
} else {
Write-Output $json
}
{
"name": "Default",
"id": "{FE5D7FDF-89C0-4D99-9AA3-B5FBD009C9F3}",
"uid": "00000000-0000-0000-0000-000000000000",
"placeholder": "",
"path": "/Views/Website/Layouts/Default.cshtml",
"placeholders": {
"footer": [
{
"name": "Footer",
"id": "{299FD8EA-2CFD-4A69-9CA4-7EA24CD34A46}",
"uid": "e2040fb7-4151-456d-9315-5973e4eb4d9d",
"placeholder": "footer",
"path": "/Views/Common/Sublayouts/Footer.cshtml",
"placeholders": {
"postfooter": [
{
"name": "Copyright",
"id": "{7DAE9F23-890F-4E06-99A7-CCEDB74D3D8E}",
"uid": "a3bad4d4-8e21-4b30-8c4c-b04163653d62",
"placeholder": "postfooter",
"path": "/Views/Identity/Copyright.cshtml",
"placeholders": {}
}
],
"/footer/section": [
{
"name": "4 Column 2-2-4-4",
"id": "{98D6BC34-0185-42A0-B53B-CEB7B17008B6}",
"uid": "025588cb-dfc0-4b02-8223-0015c525ae52",
"placeholder": "/footer/section",
"path": "/Views/Common/Sublayouts/4 Column 2-2-4-4.cshtml",
"placeholders": {
"/footer/section/col-narrow-4": [
{
"name": "Contact Information",
"id": "{CA748838-E6D5-4BAD-9126-CAD10DD1B0DB}",
"uid": "84124ee2-e678-4144-8316-0d6874866604",
"placeholder": "/footer/section/col-narrow-4",
"path": "/Views/Identity/ContactInformation.cshtml",
"placeholders": {}
}
],
"/footer/section/col-narrow-2": [
{
"name": "Navigation Links",
"id": "{F366FBA9-EF26-4007-988A-63CB64649C46}",
"uid": "0e0c6dca-1a29-4975-84d7-e99498a64a72",
"placeholder": "/footer/section/col-narrow-2",
"path": "~/Views/Navigation/NavigationLinks.cshtml",
"placeholders": {}
}
],
"/footer/section/col-narrow-3": [
{
"name": "Content Teaser with Summary",
"id": "{781C6B36-4B02-46EA-8ADC-B92A11877F10}",
"uid": "47f5a079-6f63-4219-bc45-1a221b200906",
"placeholder": "/footer/section/col-narrow-3",
"path": "/Views/Teasers/TitleSummaryTeaser.cshtml",
"placeholders": {}
}
],
"/footer/section/col-narrow-1": [
{
"name": "Navigation Links",
"id": "{F366FBA9-EF26-4007-988A-63CB64649C46}",
"uid": "0e0c6dca-1a29-4975-84d7-e99498a64a72",
"placeholder": "/footer/section/col-narrow-1",
"path": "~/Views/Navigation/NavigationLinks.cshtml",
"placeholders": {}
}
]
}
}
]
}
}
],
"page-sidebar": [
{
"name": "xDB Panel",
"id": "{CED18F24-62C2-445E-B0D9-045E3325297B}",
"uid": "20ab9baa-c112-4567-a701-564423e650d2",
"placeholder": "page-sidebar",
"path": "~/Views/Demo/ExperienceData.cshtml",
"placeholders": {
"page-sidebar": [
{
"name": "xDB Panel",
"id": "{CED18F24-62C2-445E-B0D9-045E3325297B}",
"uid": "20ab9baa-c112-4567-a701-564423e650d2",
"placeholder": "page-sidebar",
"path": "~/Views/Demo/_ExperienceDataContent.cshtml",
"placeholders": {
"page-sidebar": [
{
"name": "xDB Panel",
"id": "{CED18F24-62C2-445E-B0D9-045E3325297B}",
"uid": "20ab9baa-c112-4567-a701-564423e650d2",
"placeholder": "page-sidebar",
"path": "~/Views/Demo/_Visits.cshtml",
"placeholders": {}
},
{
"name": "xDB Panel",
"id": "{CED18F24-62C2-445E-B0D9-045E3325297B}",
"uid": "20ab9baa-c112-4567-a701-564423e650d2",
"placeholder": "page-sidebar",
"path": "~/Views/Demo/_PersonalInfo.cshtml",
"placeholders": {}
},
{
"name": "xDB Panel",
"id": "{CED18F24-62C2-445E-B0D9-045E3325297B}",
"uid": "20ab9baa-c112-4567-a701-564423e650d2",
"placeholder": "page-sidebar",
"path": "~/Views/Demo/_OnsiteBehavior.cshtml",
"placeholders": {}
},
{
"name": "xDB Panel",
"id": "{CED18F24-62C2-445E-B0D9-045E3325297B}",
"uid": "20ab9baa-c112-4567-a701-564423e650d2",
"placeholder": "page-sidebar",
"path": "~/Views/Demo/_Referral.cshtml",
"placeholders": {}
}
]
}
}
]
}
}
],
"navbar": [
{
"name": "Main Navigation",
"id": "{E26A158B-FA1E-4E9B-8622-E4458243FF46}",
"uid": "07c9fe71-a5e2-4e5f-b276-5f2b73481d44",
"placeholder": "navbar",
"path": "/Views/Common/Sublayouts/Navbar.cshtml",
"placeholders": {
"navbar-right": [
{
"name": "Primary Menu",
"id": "{53B085DB-6824-4941-9F28-9011CB151832}",
"uid": "fbdb4313-35b5-4c26-a586-17f48fd2dc41",
"placeholder": "navbar-right",
"path": "~/Views/Navigation/PrimaryMenu.cshtml",
"placeholders": {}
},
{
"name": "Main Navigation Activity",
"id": "{8832BA30-99C3-4D20-9B1C-237D910A153F}",
"uid": "75551dec-a457-43a3-b76f-91742b5dde93",
"placeholder": "navbar-right",
"path": "/Views/Common/Sublayouts/NavbarActivity.cshtml",
"placeholders": {
"navbar-activity": [
{
"name": "Site Menu",
"id": "{604ECBB6-2BBF-45AF-AC6E-BA9F853E7A03}",
"uid": "462763bc-ec80-4fba-9714-b63a87b43259",
"placeholder": "navbar-activity",
"path": "~/Views/Multisite/SwitchSite.cshtml",
"placeholders": {}
},
{
"name": "Language Menu",
"id": "{2695013C-DD51-410B-9983-F271F16C727D}",
"uid": "3555e24e-fbe9-4d81-aac7-50408aa45ef0",
"placeholder": "navbar-activity",
"path": "~/Views/Language/LanguageSelector.cshtml",
"placeholders": {}
},
{
"name": "Login Menu",
"id": "{28BD7068-7FF6-4CE6-98E6-85CFE9967E14}",
"uid": "79242576-4e0a-4cbf-bc2b-8b0c47fe9b64",
"placeholder": "navbar-activity",
"path": "~/Views/Accounts/AccountsMenu.cshtml",
"placeholders": {
"navbar-activity": [
{
"name": "Login Menu",
"id": "{28BD7068-7FF6-4CE6-98E6-85CFE9967E14}",
"uid": "79242576-4e0a-4cbf-bc2b-8b0c47fe9b64",
"placeholder": "navbar-activity",
"path": "~/Views/Accounts/_Login.cshtml",
"placeholders": {}
},
{
"name": "Login Menu",
"id": "{28BD7068-7FF6-4CE6-98E6-85CFE9967E14}",
"uid": "79242576-4e0a-4cbf-bc2b-8b0c47fe9b64",
"placeholder": "navbar-activity",
"path": "~/Views/Accounts/_FedAuth.cshtml",
"placeholders": {}
}
]
}
},
{
"name": "Global Search",
"id": "{8C3A2CFC-18FB-4BF5-8584-50EA88978ED6}",
"uid": "9c8a45f2-7008-42c1-8b8a-80a88825c1d8",
"placeholder": "navbar-activity",
"path": "~/Views/Search/GlobalSearch.cshtml",
"placeholders": {}
}
]
}
}
],
"navbar-left": [
{
"name": "Logo",
"id": "{A47C8288-7D4C-45FF-B75F-4AEDBF58A02D}",
"uid": "7bd25bd6-c6c4-4fbe-9828-d29a39a2fec9",
"placeholder": "navbar-left",
"path": "/Views/Identity/Logo.cshtml",
"placeholders": {}
}
]
}
}
],
"header-top": [
{
"name": "Header Topbar",
"id": "{C28B9C5F-8E01-42EC-ADF6-3324FAF92C7D}",
"uid": "f963aa98-4957-45b5-95cd-b61c242063e1",
"placeholder": "header-top",
"path": "/Views/Common/Sublayouts/HeaderTop.cshtml",
"placeholders": {
"left-header-top": [
{
"name": "Menu with links",
"id": "{69B9C355-004B-4268-BB1E-2F725B8752B7}",
"uid": "05ffb7b4-377c-4c00-9520-0ed7735cadca",
"placeholder": "left-header-top",
"path": "~/Views/Navigation/LinkMenu.cshtml",
"placeholders": {}
}
],
"right-header-top": [
{
"name": "Menu with links",
"id": "{69B9C355-004B-4268-BB1E-2F725B8752B7}",
"uid": "630f4515-1f19-49a8-b986-d46add5ce024",
"placeholder": "right-header-top",
"path": "~/Views/Navigation/LinkMenu.cshtml",
"placeholders": {}
}
]
}
}
],
"page-layout": [
{
"name": "Page Header",
"id": "{A2216CA7-B205-4042-A345-72117D0086C3}",
"uid": "1df746c3-2d29-43e6-9de2-05622ded6f97",
"placeholder": "page-layout",
"path": "/Views/Common/Sublayouts/PageHeader.cshtml",
"placeholders": {
"/page-layout/page-header": [
{
"name": "Page Header Media Carousel",
"id": "{8B28417B-A5B3-44F1-BFCC-FE1D907336E7}",
"uid": "66fc1c19-fc71-40ce-9c78-2158011733e1",
"placeholder": "/page-layout/page-header",
"path": "/Views/MediaFeature/PageHeaderCarousel.cshtml",
"placeholders": {}
}
]
}
},
{
"name": "Section",
"id": "{D201A487-5099-48A1-8189-55944F70C6CB}",
"uid": "a3ca17ff-693c-46f1-87aa-b8c1833f1890",
"placeholder": "page-layout",
"path": "/Views/Common/Sublayouts/Section.cshtml",
"placeholders": {
"/page-layout/section-{A3CA17FF-693C-46F1-87AA-B8C1833F1890}-0": [
{
"name": "3 Column 4-4-4",
"id": "{7074CA39-734D-4953-A3D5-B7D09C37CE3B}",
"uid": "d180d469-533f-463c-b817-8d256484d2c4",
"placeholder": "/page-layout/section-{A3CA17FF-693C-46F1-87AA-B8C1833F1890}-0",
"path": "/Views/Common/Sublayouts/3 Column 4-4-4.cshtml",
"placeholders": {
"/page-layout/section-{A3CA17FF-693C-46F1-87AA-B8C1833F1890}-0/col-narrow-3-{D180D469-533F-463C-B817-8D256484D2C4}-0": [
{
"name": "Page Teaser",
"id": "{07AB411C-F894-4144-B069-48EF67431D32}",
"uid": "7a8fcd26-592f-4119-88f1-4401728131d3",
"placeholder": "/page-layout/section-{A3CA17FF-693C-46F1-87AA-B8C1833F1890}-0/col-narrow-3-{D180D469-533F-463C-B817-8D256484D2C4}-0",
"path": "/Views/PageContent/PageTeaser.cshtml",
"placeholders": {}
}
],
"/page-layout/section-{A3CA17FF-693C-46F1-87AA-B8C1833F1890}-0/col-narrow-2-{D180D469-533F-463C-B817-8D256484D2C4}-0": [
{
"name": "Page Teaser",
"id": "{07AB411C-F894-4144-B069-48EF67431D32}",
"uid": "aa93f815-74d9-4bce-80c3-ab23cafddc29",
"placeholder": "/page-layout/section-{A3CA17FF-693C-46F1-87AA-B8C1833F1890}-0/col-narrow-2-{D180D469-533F-463C-B817-8D256484D2C4}-0",
"path": "/Views/PageContent/PageTeaser.cshtml",
"placeholders": {}
}
],
"/page-layout/section-{A3CA17FF-693C-46F1-87AA-B8C1833F1890}-0/col-narrow-1-{D180D469-533F-463C-B817-8D256484D2C4}-0": [
{
"name": "Page Teaser",
"id": "{07AB411C-F894-4144-B069-48EF67431D32}",
"uid": "b9a6955b-bed8-4168-9971-2b63dab22860",
"placeholder": "/page-layout/section-{A3CA17FF-693C-46F1-87AA-B8C1833F1890}-0/col-narrow-1-{D180D469-533F-463C-B817-8D256484D2C4}-0",
"path": "/Views/PageContent/PageTeaser.cshtml",
"placeholders": {}
}
]
}
}
]
}
},
{
"name": "Section with Media",
"id": "{1149A3C7-C69A-4E6B-A6C3-32687EC2F8D4}",
"uid": "f4fe27af-b867-4e72-8808-843d5781a355",
"placeholder": "page-layout",
"path": "~/Views/MediaFeature/SectionMedia.cshtml",
"placeholders": {
"/page-layout/section-{F4FE27AF-B867-4E72-8808-843D5781A355}-0": [
{
"name": "3 Column 6-3-3",
"id": "{B21C8A38-51BC-4866-9589-6062D6390247}",
"uid": "0f43a3b5-7236-40e6-b86e-5a52b75bbd8b",
"placeholder": "/page-layout/section-{F4FE27AF-B867-4E72-8808-843D5781A355}-0",
"path": "/Views/Common/Sublayouts/3 Column 6-3-3.cshtml",
"placeholders": {
"/page-layout/section-{F4FE27AF-B867-4E72-8808-843D5781A355}-0/col-narrow-1-{0F43A3B5-7236-40E6-B86E-5A52B75BBD8B}-0": [
{
"name": "Quote",
"id": "{A1AD34DD-535E-47AF-A911-34C57CCC26F6}",
"uid": "afaf655f-81e2-4086-98e7-9aa6fd6720d9",
"placeholder": "/page-layout/section-{F4FE27AF-B867-4E72-8808-843D5781A355}-0/col-narrow-1-{0F43A3B5-7236-40E6-B86E-5A52B75BBD8B}-0",
"path": "/Views/Person/Quote.cshtml",
"placeholders": {}
}
],
"/page-layout/section-{F4FE27AF-B867-4E72-8808-843D5781A355}-0/col-narrow-2-{0F43A3B5-7236-40E6-B86E-5A52B75BBD8B}-0": [
{
"name": "Latest News",
"id": "{BDD730EB-F808-4C65-B94A-D2F03672EAFB}",
"uid": "6a48c9d4-62d2-4007-a253-8d3d9e6e29e3",
"placeholder": "/page-layout/section-{F4FE27AF-B867-4E72-8808-843D5781A355}-0/col-narrow-2-{0F43A3B5-7236-40E6-B86E-5A52B75BBD8B}-0",
"path": "~/Views/News/LatestNews.cshtml",
"placeholders": {}
}
],
"/page-layout/section-{F4FE27AF-B867-4E72-8808-843D5781A355}-0/col-wide-1-{0F43A3B5-7236-40E6-B86E-5A52B75BBD8B}-0": [
{
"name": "Call to Action",
"id": "{A25C9850-AAB7-46A6-84B7-0DFAB32FC357}",
"uid": "762dfb70-5d70-4205-be54-c9f364c88e7c",
"placeholder": "/page-layout/section-{F4FE27AF-B867-4E72-8808-843D5781A355}-0/col-wide-1-{0F43A3B5-7236-40E6-B86E-5A52B75BBD8B}-0",
"path": "/Views/Teasers/Jumbotron.cshtml",
"placeholders": {}
}
]
}
}
]
}
}
],
"head": [
{
"name": "HTML Metadata",
"id": "{E7AE3F87-CF66-40F2-A9F5-33ECF08BC777}",
"uid": "e6ca18d8-321d-4105-9c38-0622e94be400",
"placeholder": "head",
"path": "~/Views/Metadata/PageMetadata.cshtml",
"placeholders": {}
},
{
"name": "Facebook Open Graph Metadata",
"id": "{3389CAFC-000C-41E5-921C-92821B3DB7B5}",
"uid": "ef51b238-7ce2-4e39-ab87-d05424735160",
"placeholder": "head",
"path": "/Views/Social/OpenGraph.cshtml",
"placeholders": {}
}
]
}
}
Write a Windows PowerShell (5.1 or Core) script that does the following, with 2-space indentation and no Unicode/emojis:
1. Accepts two optional parameters:
- [string] $Url (default 'https://rssbplatform.dev.local/')
- [string] $OutputPath (optional)
2. Downloads the HTML at $Url using System.Net.HttpWebRequest with a 10-second timeout (connection and read/write), reads the full page into $content, and exits with error if download fails.
3. Uses a single regex to capture all Sitecore debug comments:
Pattern: `<!--\s*(start-component|end-component)\s*=\s*'(?<json>\{.*?\})'\s*-->`
4. Defines a helper function Convert-ComponentStringToObject that:
- Takes the captured `{…}` string,
- Quotes unquoted keys via `-replace '(\w+)\s*:', '"$1":'`,
- Parses with ConvertFrom-Json or throws on invalid JSON.
5. Identifies the outer “Default” layout markers by:
- uid == '00000000-0000-0000-0000-000000000000'
- placeholder == ''
- name == 'Default'
It must initialize a root node on that first start, ignore its end, and never report it as unmatched.
6. Builds a component tree with a stack:
- For each `start-component`:
- If it’s the root marker, create the root PSCustomObject:
```powershell
[PSCustomObject]@{
name = $meta.name
id = $meta.id
uid = $meta.uid
placeholder = $meta.placeholder
path = $meta.path
placeholders= @{}
}
```
then continue (do not push it).
- Otherwise, create a similar PSCustomObject node, append it to its parent:
- Parent = top of stack if stack not empty, else root.
- Ensure `$parent.placeholders[$meta.placeholder]` exists as an array, then add the node.
- Push the node onto the stack.
- For each `end-component`:
- If it’s the root marker, continue.
- Otherwise, enforce strict UID matching against the top of the stack; pop on match, throw on mismatch or empty stack.
7. After processing all markers, throw if the stack still contains any nodes (all non-root starts must match ends).
8. Recursively reverse each placeholder’s child array in-place to preserve the original HTML order:
```powershell
function Reverse-Order { param($node)
foreach ($ph in $node.placeholders.Keys) {
$rev = $node.placeholders[$ph] | Select-Object -Reverse
$node.placeholders[$ph] = $rev
foreach ($child in $rev) { Reverse-Order $child }
}
}
Reverse-Order $root
```
9. Serialize the final tree as JSON with ConvertTo-Json -Depth 20. If $OutputPath is provided, write to that file (UTF8); otherwise Write-Output the JSON.
10. All error messages must use plain ASCII and Write-Error or throw appropriately.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment