Get (or export) an existing site structure in a SharePoint Online tenant
Summary
The Get-SiteStructure function is a PowerShell function that could be part of a larger script or module.
Its purpose is to provide a function that retrieves an existing structure of a SharePoint Online (SPO) tenant
and its content for further processing.
The function takes in several parameters:
$RootSiteUrl: The URL of the root site to start with.$WithSiteContent: A switch parameter that determines whether the site content (libraries) should be included.$AsObject: A switch parameter that determines whether the result will be returned as an object to be processed later.
The main function consists of several sub-functions define the analysis or export process:
- initialize some variables,
 - analyze whether to start with a home site or hub site,
 - get informations about the selected root site,
 - get all assigned sites (and hubs),
 - and optionally retrieve also site content (document libraries and lists).
 
Finally, Get-SiteStructure returns the site structure, optionally as an object that can be processed later:
Tenant: The tenant's nameVersion: Timestamp of the executionSharePoint: High-level inforamtion about the tenant (for now, only theTenantId)Structure: The retrieved structure, consisting of sites and site content (optional)
Note
The function needs an exisiting connection to the SPO admin center; at least SharePoint administrator rights are needed.
Usage
First of all, connect to the SPO admin center:
$adminUrl = "your SPO tenant admin url"
Connect-PnPOnline -Url $adminUrl -Interactive
Then, use the function Get-SiteStructure:
Get-SiteStructure -RootSiteUrl "https://<yourtenant>.sharepoint.com/sites/<your(hub)site>" -WithSiteContent
# store the output as a structured object in a variable
$result = Get-SiteStructure -RootSiteUrl "https://<yourtenant>.sharepoint.com/sites/<your(hub)site>" -WithSiteContent -AsObject
Further processing can be done on the $result object along your needs, e.g.:
$result | ConvertTo-Json -Depth 4 > hub-structure.json
# Initial parameters to start (connect to SPO admin center)
$adminUrl = "your SPO tenant admin url"
Connect-PnPOnline -Url $adminUrl -Interactive
Function Get-SiteStructure {
  param (
    [Parameter(HelpMessage = "The root site url to start with", Mandatory = $true)]
    [string] $RootSiteUrl,
    [Parameter(HelpMessage = "defines whether the site content (libraries) should be included", Mandatory = $false)]
    [switch] $WithSiteContent,
    [Parameter(HelpMessage = "defines whether the result will be returned as an object to be processed later", Mandatory = $false)]
    [switch] $AsObject
  )
  Function Initialize-Routine {
    $Script:RootSiteUrl = $RootSiteUrl
    $Script:Structure = @()
    $Script:TenantInfo = Get-PnPTenantInfo
    $Script:RetrieveSiteContent = $WithSiteContent
    
    Clear-Host
  }
  Function Get-SiteInfo([string] $SiteUrl) {
    $siteInfo = Get-PnPTenantSite -Identity $SiteUrl
    return @{
      Id        = $siteInfo.IsHubSite ? $siteInfo.HubSiteId : $siteInfo.Id
      Title     = $siteInfo.Title
      Url       = $siteInfo.Url
      Type      = $siteInfo.Template -eq "SITEPAGEPUBLISHING#0" ? "Communication" 
      : $siteInfo.Template -eq "GROUP#0" ? "Team" 
      : $siteInfo.Template -eq "STS#3" ? "SPOTeam" 
      : "Other"
      IsHubSite = $siteInfo.IsHubSite
    }
  }
  
  Function Get-StartSite {
    Function Get-HomeSite() {
      try {
        Write-Host "Trying to get the home site in your tenant and check whether it matches the root site"
        $homeSite = Get-PnPHomeSite -Detailed
        if (!$homeSite) { throw "Home Site not found or not set" }
        if ($homeSite.Url -ne $RootSiteUrl) { throw "Home Site does not match root site" }
        $siteInfo = Get-SiteInfo -SiteUrl $hubSite.SiteUrl
        $siteObject = ([Ordered]@{Hub = $siteInfo.Title; Url = $siteInfo.Url; Type = $siteInfo.Type })
        
        if ($Script:RetrieveSiteContent) { 
          $content = Get-SiteContent -RootSite $siteInfo 
          if ($content) { $siteObject.Content = $content }
        }
        $Script:Structure += $siteObject
        return $siteInfo
      }
      catch {
        throw $_
      }
    }
    
    Function Get-HubSite([string] $SiteUrl) {
      try {
        Write-Host "Trying to get the according hub site: " -NoNewline
        $hubSite = Get-PnPHubSite -Identity $RootSiteUrl
        if (!$hubSite) { throw "Hub Site not found or not set" }
        
        Write-Host -ForegroundColor DarkGreen "✔︎ Starting with $($hubSite.SiteUrl)"
        $siteInfo = Get-SiteInfo -SiteUrl $hubSite.SiteUrl
        $siteObject = ([Ordered]@{Hub = $siteInfo.Title; Url = $siteInfo.Url; Type = $siteInfo.Type })
        
        if ($Script:RetrieveSiteContent) { 
          $content = Get-SiteContent -RootSite $siteInfo 
          if ($content) { $siteObject.Content = $content }
        }
        $Script:Structure += $siteObject
        return $siteInfo
      }
      catch {
        throw $_
      }
    }
    
    # Run the start site routine
    try {
      return Get-HomeSite
    }
    catch {
      Write-Host -ForegroundColor DarkYellow $_
      return Get-HubSite -SiteUrl $RootSiteUrl
    }
  }
  Function Get-AssignedSites {
    param (
      [Parameter(HelpMessage = "the site from where the assigned sites will be retrieved", Mandatory = $true)]
      [object] $RootSite
    )
    
    # Get all hubs that are assigned to this site site;
    # either directly connected sites (first test) or connected hubs (second test)
    $children = (Get-PnPHubSiteChild -Identity $RootSite.Url) ?? (Get-PnPHubSite | ? { $_.ParentHubSiteId -eq $RootSite.Id })
    foreach ($site in $children) {
      $siteInfo = switch ($site.GetType().FullName) {
        "Microsoft.Online.SharePoint.TenantAdministration.SiteProperties" { Get-SiteInfo -SiteUrl $site.SiteUrl }
        "System.String" { Get-SiteInfo -SiteUrl $site }
        "Default" { Get-SiteInfo -SiteUrl $site }
      }
      Write-Host "👉 $($siteInfo.Url)"
      
      # Get all assigned sites in case of current site is a hub site
      if ($siteInfo.IsHubSite) {
        $siteObject = ([Ordered]@{Hub = $siteInfo.Title; Url = $siteInfo.Url; Type = $siteInfo.Type; ConnectedHubsite = $RootSite.Url })
        if ($Script:RetrieveSiteContent) { 
          $content = Get-SiteContent -RootSite $siteInfo 
          if ($content) { $siteObject.Content = $content }
        }
        
        $Script:Structure += $siteObject
        Get-AssignedSites -RootSite $siteInfo
      }
      else {
        $siteObject = ([Ordered]@{Site = $siteInfo.Title; Url = $siteInfo.Url; Type = $siteInfo.Type; ConnectedHubsite = $RootSite.Url })
        if ($Script:RetrieveSiteContent) { 
          $content = Get-SiteContent -RootSite $siteInfo 
          if ($content) { $siteObject.Content = $content }
        }
        
        $Script:Structure += $siteObject
      }
    }
  }
  Function Get-SiteContent {
    param (
      [Parameter(HelpMessage = "the site from where the content will be retrieved", Mandatory = $true)]
      [object] $RootSite
    )
    $output = @()
    $connSite = Connect-PnPOnline -Url $RootSite.Url -Interactive -ReturnConnection
    
    # Get the document libraries & lists
    $objects = Get-PnPList -Connection $connSite | `
      Where-Object { $_.BaseType -in @("DocumentLibrary", "GenericList", "Events") -and $_.Hidden -eq $false -and $_.EntityTypeName -notin @("Style_x0020_Library", "FormServerTemplates", "SiteAssets", "SitePages") }
    if ($objects) {
      foreach ($object in $objects) {
        $output += switch ($object.BaseType) {
          "DocumentLibrary" { [Ordered]@{ DocumentLibrary = $object.Title; Url = "/" + $object.RootFolder.ServerRelativeUrl.Split("/")[-1]; } }
          "GenericList" { [Ordered]@{ List = $object.Title; Url = "/" + $object.RootFolder.ServerRelativeUrl.Split("/")[-1]; } }
          "Default" { [Ordered]@{ List = $object.Title; Url = "/" + $object.RootFolder.ServerRelativeUrl.Split("/")[-1]; } }
        }
      }
    }
    $connSite = $null;
    return $output
  }
  
  #######################################
  # START Main Routine
  try {
    Initialize-Routine
    $startSite = Get-StartSite
    Get-AssignedSites -RootSite $startSite
    
    $result = [Ordered]@{
      Tenant     = $Script:TenantInfo.DisplayName
      Version    = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
      SharePoint = @{TenantId = $Script:TenantInfo.TenantId.ToString() }
      Structure  = $Script:Structure
    }
    if ($AsObject.IsPresent) {
      return $result
    }
    else {
      $result.Structure
    }
  }
  catch {
    Write-Error $_
  }
}
Check out the PnP PowerShell to learn more at: https://aka.ms/pnp/powershell
Source Credit
Sample taken from https://github.com/tmaestrini/easyProvisioning/
Contributors
| Author(s) | 
|---|
| Tobias Maestrini | 
Disclaimer
THESE SAMPLES ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.