Create a modern SharePoint site template with multiple pages using PnP provisioning engine

The PnP provisioning engine is a framework that allows you to create templates based on pre-built SharePoint sites. The framework was not built to be a migration tool and when a template is generated it only saves the home page, although the schema supports multiple pages.

In this article you can find a PowerShell script that saves a modern SharePoint site as a template with all the included pages.

The algorithm to save all the pages is quite simple and all the commands used are available in the PnP PowerShell.

It starts by saving the site as a template with all properties and definitions included, then it loops through all the pages in the Site Pages library, set each page as home page of the site and saves a new template with the name of the page.

Since all the site properties and definitions were saved before each subsequent save only gets the page using the -Handlers PageContents option, when the last page is saved the original home page is applied to the site.

The script ends by copying the first template saved and by importing all the page nodes from the smaller templates, generating then a full template with all the pages.

The full script is available below.

pushd (Split-Path -Path $MyInvocation.MyCommand.Definition -Parent)
$saveDir = (Resolve-path ".\")
$siteURL = Read-Host "Please enter your site URL"

Write-Host "Connecting to: $siteURL"
Connect-PnPOnline -Url $siteURL; 
Write-Host "Connected!"

$web = Get-PnPWeb
$sourceSite = $web.ServerRelativeUrl
$library = "Site Pages"

#get all pages in the site pages library
$pages = Get-PnPListItem -List $library

#save current homepage
$currentHomePage = Get-PnPHomePage

$pagesList = New-Object System.Collections.Generic.List[System.Object]

$pageNumber = 1

foreach($page in $pages){
	if($page.FileSystemObjectType -eq "File"){
		$pagePath = $page.FieldValues["FileRef"]
	    $pageFile = $pagePath -replace $sourceSite, ""
		$pageTemplate = $pageFile -replace "/SitePages/","" -replace ".aspx",".xml"
	    $pagesList.Add($($pageTemplate -replace "./TemplateWithPages",""))	
		
		#set current page as home page
		Set-PnPHomePage -RootFolderRelativeUrl ($pagePath -replace ($sourceSite+"/"), "")
		
		Write-Host ("Saving page #" + $pageNumber + " - " + $pageTemplate)
		
		if($pageNumber -eq 1){
			Get-PnPProvisioningTemplate -Out $($saveDir.Path + "\" + $pageTemplate)
		}else{
			Get-PnPProvisioningTemplate -Out $($saveDir.Path + "\" + $pageTemplate) -Handlers PageContents
		}
		
		$pageNumber++
	}
}

$pagesList.ToArray()

#apply default default homepage
Set-PnPHomePage -RootFolderRelativeUrl $currentHomePage

#copy base template 
Copy-item -path ($saveDir.Path + "\" + $pagesList[0]) -destination ($saveDir.Path + "\Template.xml")

#open main xml
$mainFile = [xml][io.File]::ReadAllText($($saveDir.Path + $("\Template.xml")))
$clientSidePages = $mainFile.Provisioning.Templates.ProvisioningTemplate.ClientSidePages

#remove pages to avoid duplicates 
$clientSidePages.RemoveChild($mainFile.Provisioning.Templates.ProvisioningTemplate.ClientSidePages.ClientSidePage)

foreach( $page in $pagesList ){
	#open page template xml
	$xmlContents = [xml][io.File]::ReadAllText($saveDir.Path + "\" + $page)
	foreach($node in $xmlContents.Provisioning.Templates.ProvisioningTemplate.ClientSidePages.ClientSidePage)
	{
			
		#copy nodes from page xml
		$importNode = $clientSidePages.OwnerDocument.ImportNode($node, $true);
		$clientSidePages.AppendChild($importNode) | Out-Null
		
		#save final tempate
		$mainFile.Save($($saveDir.Path + $("\Template.xml")))
	
	}
}

popd

Designed by Freepik


17 Responses to “Create a modern SharePoint site template with multiple pages using PnP provisioning engine”

  1. Simon Hudson

    February 22, 2018

    Thanks for this João, very useful

    For my clarity, as I’m not a PS expert, what are the variables in here that we need to modify / configure when readying the script for our own sites?

    Reply
    • João Ferreira

      February 23, 2018

      Hi Simon, you just need to save the script in a ps1 file and execute it. The script will request the url to your own modern site

      Reply
  2. Thomas

    April 5, 2018

    Hello there.

    Thank you for this nice example, it looks very promising.
    I am a beginner with this and run into one problem though so far: In line 55, the $clientSidePages are null, so all the consecutive calls to this variable fail.

    Would you know why this variable is empty, how to avoid it or how to add this node into the XML? Since it seems that i really need it right?

    Thank you very much.

    Reply
    • Thomas

      April 5, 2018

      One more Information: Also the ClientSidePages of each $xmlContents in line 63 are null for me. Do you have any suggestion? Thank you in advance.

      Reply
    • João Ferreira

      April 5, 2018

      Hi Thomas,

      Please make sure you are using this script with modern SharePoint Team Sites or Communication Sites.
      This node is generated by the PnP Provisioning and it generates a node for each site page in the site.
      Can you add a break point to line 14 and let me know what is the result of the $pages variable?

      Reply
      • Thomas

        April 5, 2018

        Thank you for your reply.
        I am not realy sure about what is a “modern” TeamSite, but I am confident that I am using this.

        The content of the $pages variable is: Microsoft.SharePoint.Client.ListItem Microsoft.SharePoint.Client.ListItem Microsoft.SharePoint.Client.ListItem

        So there are three pages inside there. Namely they are “Home.aspx”, “How to use this library.aspx” and “Core-Data.aspx”. The first two seem to be there by default, the last one is the only one I am interested in actually.

        I double-checked the exported XML files for these three pages, and actually the last one contains the node “ClientSidePages”. This is good.

        The other two pages do not have this node though.

        And also the $mainFile does not have this node..

        I could try to just add a new node there with this name but I am unable to create a node called , instead I am only able to create a node called (without the pnp prefix). I tried it using this Approach:

        $child = $mainFile.CreateElement(“pnp:ClientSidePages”)
        $mainFile.Provisioning.Templates.ProvisioningTemplate.AppendChild($child)

        I hope you can follow my thoughts here. Thank you very much for any Input.

        Reply
        • João Ferreira

          April 17, 2018

          Thomas,

          According to your description and after debugging it seems that your site is not a modern team site.
          Let me know if it looks like the site in the image below:

          Reply
          • Thomas

            April 18, 2018

            Hello Joao.

            Thanks for your reply. My site looks like this. At least the navigation on the left and the top and header of the page. The actual contents in the middle of the page are not the same, but i guess that does not matter?

            Anyway, I was now able to make your example work for me with a little extra xml modification in my powershell script. 😉 Thank you for your kind help!

            By the way, since you seem to be an expert with PnP and Sharepoint Online:

            Do you know if a similar way is possible for adding subsites (not pages) to the xml template?

            Thank you!

          • João Ferreira

            April 18, 2018

            Hi Thomas,

            I’m glad it worked for you.
            The creation of subsites is not part of the PnP Schema, a possible workarround is to create the templates for each site and then create a PowerShell script to execute the templates creation in the desired order.
            You just need to change the site context after each site creation to deploy a template as a subsite of the new provisioned site.

      • Thomas

        April 5, 2018

        In my previous reply it seems that the names of the node I am trying to create got missing.

        I am trying to create a node called pnp:ClientSidePages but i always end up with a node just called ClientSidePages (without the pnp prefix).

        Reply
        • João Ferreira

          April 17, 2018

          Hi Thomas,

          The node can not be created manually, if you end up with a node called ClientSidePages you are not using a modern site, instead you are using a classic Publishing site or classic team site with the publishing features enabled.

          Reply
  3. kelvin

    May 24, 2018

    after I apply, how can I undo? if I want back to default setting.

    Reply
    • João Ferreira

      May 28, 2018

      Hi Kelvin,

      After applying the template is not possible to revert and remove all the assets applied with it.

      Reply
      • kelvin

        May 31, 2018

        this can support sharepoint 2016 on premise? or just can apply on sharepoint online?

        Reply
        • kelvin

          May 31, 2018

          I already run the script and after run i get the sitepages.xml on my desktop?

          How I can insert the xml file to my sharepoint 2016 on premise?

          Reply
        • João Ferreira

          May 31, 2018

          Hi Kelvin,

          This script was made specifically for SharePoint online but since it uses PnP you will be able to adapt it to SharePoint on Prem 2016.
          If you are using a Publishing site change the library name from Site Pages to Pages and you should get all the pages of the site in your template.

          Reply

Leave a Reply


Web developer focused on SharePoint branding, blogger, tech enthusiast. Travelling and sports are my addictions, knowledge and success are my daily motivations.