Collaborate, Innovate, Automate

Add HTML to PnP Modern Search Templates

These two scripts work together to inject new HTML into multiple PnP Modern Search Handlebars result templates stored in a SharePoint Site Assets library. The first is a diagnostic tool to confirm your anchor string before making any changes; the second performs the injection across all templates in a folder.

The challenge with any find-and-replace approach on HTML files is that the anchor string must match the file content exactly — including indentation, trailing spaces, and line endings. Before running the main update script, use the diagnostic script to inspect the exact characters around your intended insertion point. It downloads the template and prints a numbered, bracket-wrapped view of the relevant lines, making trailing spaces and CRLF line endings visible.

Once you have confirmed the exact anchor string, the main script connects to the target site, downloads each .html template from the specified Site Assets folder, performs the string replacement in memory, and uploads the modified file back — no temp files on disk.

Prerequisites

Handlebars Template Context — Before

This is the section of the Handlebars template where the CTA button is injected — immediately after the closing </div> of the tags block:

<div class="tags"><!-- Tag - fixed styling -->
                    {{#if item.RefinableString05}}
                        <span class="card-tag">{{trim item.RefinableString05}}</span>
                    {{/if}}
                    {{#if item.RefinableString04}}
                        <span class="card-tag">{{trim item.RefinableString04}}</span>
                    {{/if}}
                </div>
                </div>

Script 1: Find-TemplateAnchor.ps1

Run this diagnostic script first. It downloads a single template and prints lines 160–175 with bracket wrapping so you can see trailing spaces and CRLF endings. Use the output to confirm the exact anchor string before running the update script.

# ============================================================
# Find-TemplateAnchor.ps1
# Downloads NewsCards.html and shows the closing tags section
# so we can identify the exact anchor string for replacement
# ============================================================

$siteUrl   = "https://tenantName.sharepoint.com/sites/siteName"
$folderUrl = "/sites/siteName/SiteAssets/pnpCustomerTemplates"
$clientId  = ""

Connect-PnPOnline -Url $siteUrl -Interactive -ClientId $clientId

$content = Get-PnPFile -Url "$folderUrl/NewsCards.html" -AsString

Write-Host "File downloaded — $($content.Length) characters`n"

$lines = $content -split "`n"

Write-Host "Total lines: $($lines.Count)`n"
Write-Host "--- Lines 160 to 175 ---"

for ($i = 160; $i -le 175; $i++) {
    if ($i -lt $lines.Count) {
        Write-Host "$($i.ToString().PadLeft(3)): [$($lines[$i])]"
    }
}

Script 2: UpdateHTMLToPnPTemplate.ps1

Once you have confirmed the anchor string using the diagnostic script above, run this script to inject the CTA button HTML and supporting CSS into every .html template in the target folder. Files where the anchor is not found are skipped with a warning rather than modified.

# ============================================================
# UpdateHTMLToPnPTemplate.ps1
# Injects a CTA button into all PnP Handlebars result templates
# stored in a SharePoint Site Assets folder
# ============================================================

$siteUrl    = "https://tenantName.sharepoint.com/sites/siteName"
$folderPath = "SiteAssets/pnpCustomerTemplates"
$clientId   = ""      # <-- replace with your Entra app registration client ID
$buttonUrl  = "https://www.google.com"   # <-- replace with real URL per deployment
$buttonLabel = "View More"               # <-- replace with real label

# Derived server-relative folder URL — used for Get-PnPFile and Add-PnPFile
$folderUrl  = "/sites/siteName/$folderPath"

# The button HTML to inject — sits inside card-content, after the tags div
$buttonHtml = @"
                <div class="card-actions">
                    <a href="$buttonUrl" target="_blank" rel="noopener noreferrer" class="card-cta-btn">$buttonLabel</a>
                </div>
"@

# CSS to inject into the <style> block of each template
$buttonCss = @"

    /* CTA button */
    .card-actions {
        margin-top: 8px;
    }

    .card-cta-btn {
        display: inline-block;
        font-size: 11px;
        font-weight: 600;
        color: #ffffff;
        background-color: #0f6cbd;
        border: 1px solid #0f6cbd;
        border-radius: 4px;
        padding: 4px 12px;
        text-decoration: none;
        letter-spacing: 0.02em;
        transition: background-color 0.2s ease;
        white-space: nowrap;
    }

    .card-cta-btn:hover {
        background-color: #115ea3;
        text-decoration: none;
    }
"@

# ============================================================

function Update-Template {
    param(
        [string]$FileName,
        [string]$Content
    )

    $updated    = $false
    $newContent = $Content

    # 1 — Inject button HTML after tags block
    if ($newContent.Contains("                </div> `r`n                </div>")) {
        $newContent = $newContent.Replace(
            "                </div> `r`n                </div>",
            "                </div>`r`n$buttonHtml`r`n                </div>"
        )
        $updated = $true
    } else {
        Write-Warning "ANCHOR NOT FOUND (HTML): $FileName — skipping button injection"
    }

    # 2 — Inject CSS before closing </style>
    if ($newContent.Contains("</style>")) {
        $newContent = $newContent.Replace("</style>", "$buttonCss`n</style>")
    } else {
        Write-Warning "NO </style> TAG FOUND: $FileName — CSS not injected"
    }

    if ($updated) { return $newContent }
    return $null
}

# ============================================================

try {
    Connect-PnPOnline -Url $siteUrl -Interactive -ClientId $clientId

    # Get all .html files in the templates folder
    $files = Get-PnPFolderItem -FolderSiteRelativeUrl $folderPath -ItemType File |
             Where-Object { $_.Name -like "*.html" }

    if (-not $files) {
        Write-Warning "No .html files found in $folderPath"
        return
    }

    Write-Host "Found $($files.Count) template(s)`n"

    foreach ($file in $files) {
        $filePath = "$folderUrl/$($file.Name)"
        Write-Host "Processing: $($file.Name)"

        # Download file content
        $content = Get-PnPFile -Url $filePath -AsString

        # Apply updates
        $newContent = Update-Template -FileName $file.Name -Content $content

        if ($newContent) {
            # Upload updated content back
            $bytes  = [System.Text.Encoding]::UTF8.GetBytes($newContent)
            $stream = New-Object System.IO.MemoryStream(, $bytes)
            Add-PnPFile -FileName $file.Name -Folder $folderUrl -Stream $stream | Out-Null
            $stream.Dispose()
            Write-Host "  OK  $($file.Name)" -ForegroundColor Green
        } else {
            Write-Host "  SKIPPED  $($file.Name)" -ForegroundColor Yellow
        }
    }

    Write-Host "`nDone."
}
catch {
    Write-Error "Error: $_"
}

Handlebars Template Context — After

This is how the same section looks once the script has run — the card-actions div with the CTA button has been inserted between the tags block and the closing card div:

<div class="tags"><!-- Tag - fixed styling -->
                    {{#if item.RefinableString05}}
                        <span class="card-tag">{{trim item.RefinableString05}}</span>
                    {{/if}}
                    {{#if item.RefinableString04}}
                        <span class="card-tag">{{trim item.RefinableString04}}</span>
                    {{/if}}
                </div>
                <div class="card-actions">
                    <a href="https://www.google.com" target="_blank" rel="noopener noreferrer" class="card-cta-btn">View More</a>
                </div>
                </div>

Usage Notes