Collaborate, Innovate, Automate

Folder Path Length

This PnP PowerShell script analyses the folder and page structure of a SharePoint Online Site Pages library, calculating the character length of every path in the hierarchy. It exports a full report to CSV, sorted by path length, so you can quickly identify items that are approaching or exceeding SharePoint's URL limits.

Purpose

SharePoint enforces a 400-character limit on encoded paths (starting from sites/...). Deeply nested folder structures with long names can silently cause issues — pages may fail to load, move, or be indexed correctly. This script helps you:

What the script reports

The exported CSV includes the following columns for every folder and page found:

Results are sorted with the longest paths first, making it easy to focus on the items most at risk.

Prerequisites

PowerShell Script

param(
    [Parameter(Mandatory=$false)]
    [string]$ClientId = "",

    [Parameter(Mandatory=$false)]
    [string]$SiteUrl = "https://tenantName.sharepoint.com/sites/siteName",

    [Parameter(Mandatory=$false)]
    [string]$OutputPath = ".\SharePoint_FolderStructure_Report.csv"
)

# Function to get all items recursively
function Get-AllItemsRecursive {
    param(
        [string]$FolderUrl = "",
        [string]$LibraryName = "Site Pages",
        [hashtable]$ProcessedPaths = @{}
    )

    $items = @()

    try {
        if ([string]::IsNullOrEmpty($FolderUrl)) {
            # Get root level items
            $listItems = Get-PnPListItem -List $LibraryName -PageSize 5000
        }
        else {
            # Get items in specific folder
            $listItems = Get-PnPListItem -List $LibraryName -FolderServerRelativeUrl $FolderUrl -PageSize 5000
        }

        foreach ($item in $listItems) {
            $fileRef = $item.FieldValues["FileRef"]
            $fileType = $item.FieldValues["FSObjType"]
            $fileName = $item.FieldValues["FileLeafRef"]

            # Skip if we've already processed this path
            if ($ProcessedPaths.ContainsKey($fileRef)) {
                continue
            }

            # Mark this path as processed
            $ProcessedPaths[$fileRef] = $true

            # Get the web's server-relative URL to avoid duplication
            $webServerRelativeUrl = $web.ServerRelativeUrl

            # Construct the full URL properly
            # $fileRef is the full server-relative path (e.g., /sites/intranet/SitePages/Page.aspx)
            # We need to combine domain + fileRef, NOT web.Url + fileRef
            $domain = $web.Url.Replace($webServerRelativeUrl, "")
            $fullUrl = $domain + $fileRef

            # Per Microsoft documentation, the 400-char limit applies to the path starting from /sites/
            # NOT including the domain (https://domain.sharepoint.com)
            # Example: "sites/marketing/documents/Shared Documents/Promotion/Some File.xlsx"

            # Remove leading slash from fileRef to get the path for length calculation
            $pathForLimit = $fileRef.TrimStart('/')

            # URL-encode the path to get the ACTUAL length as it appears in SharePoint
            # This converts: spaces to %20, # to %23, & to %26, ( to %28, ) to %29, etc.
            $encodedPath = [System.Uri]::EscapeDataString($pathForLimit)
            $encodedUrl = [System.Uri]::EscapeUriString($fullUrl)

            # FSObjType: 0 = File, 1 = Folder
            if ($fileType -eq 1) {
                # It's a folder
                $items += [PSCustomObject]@{
                    Type = "Folder"
                    Name = $fileName
                    ServerRelativeUrl = $fileRef
                    FullUrl = $fullUrl
                    EncodedUrl = $encodedUrl
                    PathForLimit = $pathForLimit
                    EncodedPathForLimit = $encodedPath
                    ActualPathLength = $encodedPath.Length
                    DecodedPathLength = $pathForLimit.Length
                    FullUrlLength = $encodedUrl.Length
                    Level = ($fileRef -split '/').Count - 1
                }

                # Recursively get items in subfolder
                $items += Get-AllItemsRecursive -FolderUrl $fileRef -LibraryName $LibraryName -ProcessedPaths $ProcessedPaths
            }
            else {
                # It's a file/page
                $items += [PSCustomObject]@{
                    Type = "Page"
                    Name = $fileName
                    ServerRelativeUrl = $fileRef
                    FullUrl = $fullUrl
                    EncodedUrl = $encodedUrl
                    PathForLimit = $pathForLimit
                    EncodedPathForLimit = $encodedPath
                    ActualPathLength = $encodedPath.Length
                    DecodedPathLength = $pathForLimit.Length
                    FullUrlLength = $encodedUrl.Length
                    Level = ($fileRef -split '/').Count - 1
                }
            }
        }
    }
    catch {
        Write-Error "Error processing folder '$FolderUrl': $_"
    }

    return $items
}

# Main script execution
try {
    Write-Host "Connecting to SharePoint site: $SiteUrl" -ForegroundColor Cyan

    # Connect to SharePoint (will prompt for authentication)
    Connect-PnPOnline -Url $SiteUrl -Interactive -ClientId $ClientId

    # Get web details for constructing full URLs
    $web = Get-PnPWeb

    Write-Host "Connected successfully. Retrieving folder structure and pages..." -ForegroundColor Green

    # Create hashtable to track processed items and prevent duplicates
    $processedPaths = @{}

    # Get all items from Site Pages library
    $allItems = Get-AllItemsRecursive -LibraryName "Site Pages" -ProcessedPaths $processedPaths

    Write-Host "`nTotal items found: $($allItems.Count)" -ForegroundColor Yellow
    Write-Host "  - Folders: $(($allItems | Where-Object {$_.Type -eq 'Folder'}).Count)" -ForegroundColor Yellow
    Write-Host "  - Pages: $(($allItems | Where-Object {$_.Type -eq 'Page'}).Count)" -ForegroundColor Yellow

    # Display statistics
    $maxPathLength = ($allItems | Measure-Object -Property ActualPathLength -Maximum).Maximum
    $avgPathLength = ($allItems | Measure-Object -Property ActualPathLength -Average).Average

    Write-Host "`nPath Length Statistics (from /sites/ onwards, ENCODED):" -ForegroundColor Cyan
    Write-Host "  - Maximum path length: $maxPathLength characters" -ForegroundColor $(if($maxPathLength -gt 400){"Red"}else{"Green"})
    Write-Host "  - Average path length: $([math]::Round($avgPathLength, 2)) characters" -ForegroundColor White
    Write-Host "  - SharePoint limit: 400 characters" -ForegroundColor Yellow

    # Show items with longest paths
    Write-Host "`nTop 10 longest paths:" -ForegroundColor Cyan
    $allItems | Sort-Object ActualPathLength -Descending | Select-Object -First 10 |
        Format-Table Type, Name, ActualPathLength, DecodedPathLength, @{Label="PathFromSites";Expression={$_.PathForLimit};Width=60} -AutoSize

    # Export to CSV - sorted by ActualPathLength (longest first)
    Write-Host "`nExporting to CSV..." -ForegroundColor Cyan
    $allItems | Sort-Object ActualPathLength -Descending |
        Select-Object Type, Name, PathForLimit, EncodedPathForLimit, ActualPathLength, DecodedPathLength, ServerRelativeUrl, FullUrl, FullUrlLength, Level |
        Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8

    Write-Host "`nCSV report exported to: $OutputPath" -ForegroundColor Green
    Write-Host "Columns included:" -ForegroundColor Cyan
    Write-Host "  - Type: Folder or Page" -ForegroundColor White
    Write-Host "  - Name: Item name" -ForegroundColor White
    Write-Host "  - PathForLimit: Path from /sites/ onwards (decoded)" -ForegroundColor White
    Write-Host "  - EncodedPathForLimit: Path from /sites/ onwards (with %20, %28, etc.)" -ForegroundColor White
    Write-Host "  - ActualPathLength: Encoded path length - THE LIMIT APPLIES TO THIS!" -ForegroundColor Yellow
    Write-Host "  - DecodedPathLength: Decoded path length (for comparison)" -ForegroundColor White
    Write-Host "  - ServerRelativeUrl: Full relative path including leading /" -ForegroundColor White
    Write-Host "  - FullUrl: Complete URL with domain" -ForegroundColor White
    Write-Host "  - FullUrlLength: Full encoded URL length (for reference)" -ForegroundColor White
    Write-Host "  - Level: Folder depth" -ForegroundColor White

    Write-Host "`nNote: Sorted by ActualPathLength (longest first)" -ForegroundColor Yellow
    Write-Host "SharePoint limit: 400 characters for ENCODED path (starting from sites/...)" -ForegroundColor Yellow

    # Disconnect
    Disconnect-PnPOnline

    Write-Host "`nScript completed successfully!" -ForegroundColor Green
}
catch {
    Write-Error "An error occurred: $_"
    Write-Error $_.Exception.StackTrace
}
finally {
    # Ensure disconnection
    try { Disconnect-PnPOnline -ErrorAction SilentlyContinue } catch {}
}

Usage Notes