Find SharePoint pages Path lengths

Ever suspected some of the paths are bumping up against the 400 character limit? This PnP PowerShell script will help you confirm that suspicion. A client of mine took the idea of a folder structure and got a little carried away. Nested folder after nested folder. Often SharePoint itself doesn´t respect it´s own characater limits. But if you are working with another tool, such as Power Automate, that does respect SharePoint´s limits, then it quickly becomes important. Plus, Micrsoft itself recommends using metadata over folders, and folder use should be minimal where possible.

Purpose

This script helps you:

  • Output the path and encoded path
  • Count the number of characters in the path
  • Order the pages and folders according to length
  • Provide the business with evidence for decision making

Prerequisites

  • PnP PowerShell module installed
  • Site owner permissions
  • Connection to your SharePoint Online site

PowerShell Script


            param(
                [Parameter(Mandatory=$false)]
                [string]$ClientId = "",
            
                [Parameter(Mandatory=$false)]
                [string]$SiteUrl = "https://tenantName.sharepoint.com/sites/siteName",
                
                [Parameter(Mandatory=$false)]
                [string]$OutputPath = ".\SitePages_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

  • Update the Tenant name, site name, client id for your tenant
  • The csv will be output to the same directory used to run the
  • Check the FullUrlLength column for pth length
  • This script shouldn´t impact site performance

Important Considerations

  • Spaces add length to paths
  • Restrict the length of page titles if you can
  • Folder should not be nested