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


42 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
  4. Zach Welding

    August 13, 2018

    Any idea why i would be getting this?
    You cannot call a method on a null-valued expression.
    At C:\Users\zwelding\OneDrive – BlueNet Inc\Searchwide\template.ps1:58 char:1
    + $clientSidePages.RemoveChild($mainFile.Provisioning.Templates.Provisi …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.
    At C:\Users\zwelding\OneDrive – BlueNet Inc\Searchwide\template.ps1:67 char:3
    + $importNode = $clientSidePages.OwnerDocument.ImportNode($node …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.
    At C:\Users\zwelding\OneDrive – BlueNet Inc\Searchwide\template.ps1:68 char:3
    + $clientSidePages.AppendChild($importNode) | Out-Null
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    Reply
    • Zach Welding

      August 13, 2018

      i ran it again on the root site and it worked. Our structure is /sites/clientportal (this is the main modern site) we then have subsites under that for each client(sites/clientportal/client1). That initial subsite is what i would like to save as a template so that for each client we can create a new site. Will this work for that?

      Reply
      • Zach Welding

        August 13, 2018

        Final question, how do you actually apply the template once its been saved?

        Reply
        • João Ferreira

          August 13, 2018

          Zach,

          Once saved you can apply the template following the instructions on the Set Template section of this article

          Reply
      • João Ferreira

        August 13, 2018

        Hi Zach,

        I’m assuming the sub sites are classic sites, if you are using classic make sure you are using Collaboration Sites otherwise you will not have the library Site Pages.
        If you are using Publishing you need to modify the script to reference Pages Library instead of Site Pages.

        Reply
        • Zach Welding

          August 16, 2018

          I have a site pages library, no publishing enabled. So i dont think i need to modify the script?

          I created a communication site first, then created classic subsites underneath that with modern pages.

          Reply
          • João Ferreira

            August 26, 2018

            Hi Zach,

            This script was made to save the root modern site and the pages that belong to it. To save classic sub sites with pages it might need to be adjusted.

  5. Pappu Kumar Singh

    September 19, 2018

    You are awesome. You script works like a charm.

    Reply
  6. Pappu Kumar Singh

    September 19, 2018

    I am able to export the pages as well as template xml file. When i use the following pnp command i get “File Not Found”. I want only pages to be created in the new site. Is it possible ?
    $url = “https://XXXXXXX.sharepoint.com/sites/NewSiteUsingTemplate1”
    $cred = Get-Credential

    Connect-PnPOnline $url -Credential $cred
    Write-Host “Adding Template…” -ForegroundColor Yellow
    Apply-PnPProvisioningTemplate -Path ‘.\Template.xml’
    Write-Host “Template Added” -ForegroundColor Green

    Reply
    • João Ferreira

      November 25, 2018

      Hi Pappu,

      Please make sure the path to the file is correct.
      Apply-PnPProvisioningTemplate -Path ‘.\Template.xml’
      Tipically this happens when the command does not find the xml file.

      Thanks,

      João

      Reply
  7. […] is possible to get all of the pages for  a site using a bit of cunning.  João Ferreira has written a script which cycles through all of the pages in a site, sets them as the home page and then exports the […]

    Reply
  8. Chris

    October 4, 2018

    Hello I used this script thank you by the way. But I used it to help a company out with creating a template for a final project group and I get and error on line 54:
    Exception calling “ReadAllText” with “1” argument(s): “Access to the path ‘C:\junk\Template.xml’ is denied.”
    At C:\junk\DaytonRogers_PSClone.ps1:54 char:65
    + … = [xml][io.File]::ReadAllText($($saveDir.Path + $(“\Template.xml”)))
    + ~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : UnauthorizedAccessException

    You cannot call a method on a null-valued expression.
    At C:\junk\DaytonRogers_PSClone.ps1:58 char:5
    + $clientSidePages.RemoveChild($mainFile.Provisioning.Templates.Pro …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    Is there any help I can get please.

    Reply
    • João Ferreira

      October 4, 2018

      Hi Chris,

      The path to the file doesn’t seem correct. Not sure if you are trying to save it on the root of your disk, if you are try to save the template on your desktop. Also check if you user folder has any special character if it has powershell might fail saving the file.

      Reply
  9. Sam Foster

    October 18, 2018

    João Ferreira,

    First of all want to say thanks for you blog posts. Really useful to folks getting off the ground with stuff like this.

    I have a modern collaboration site which I have created and added a few test pages to. The URL to the site is

    https:///sites/provisioning101/

    where is my dev tenant URL. When I run your script it starts off OK but then after it finishes the last single page it starts spitting out the following

    ———————–

    Connected!
    Saving page #1 – Home.xml
    Saving page #2 – Sam-was-here.xml
    Saving page #3 – Page-2.xml
    Saving page #4 – Page-3.xml
    Home.xml
    Sam-was-here.xml
    Page-2.xml
    Page-3.xml

    You cannot call a method on a null-valued expression.
    At D:\export-all-pages-from-SPO-site.ps1:59 char:1
    + $clientSidePages.RemoveChild($mainFile.Provisioning.Templates.Provisi
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.
    At D:\export-all-pages-from-SPO-site.ps1:68 char:3
    + $importNode = $clientSidePages.OwnerDocument.ImportNode($node
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.
    At D:\export-all-pages-from-SPO-site.ps1:69 char:3
    + $clientSidePages.AppendChild($importNode) | Out-Null
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.
    At D:\export-all-pages-from-SPO-site.ps1:68 char:3
    + $importNode = $clientSidePages.OwnerDocument.ImportNode($node
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.
    At D:\export-all-pages-from-SPO-site.ps1:69 char:3
    + $clientSidePages.AppendChild($importNode) | Out-Null
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.
    At D:\export-all-pages-from-SPO-site.ps1:68 char:3
    + $importNode = $clientSidePages.OwnerDocument.ImportNode($node
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.
    At D:\export-all-pages-from-SPO-site.ps1:69 char:3
    + $clientSidePages.AppendChild($importNode) | Out-Null
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    ———

    Can you please give me any pointers?

    What I want to do is be able to point this script at any modern site I choose on my DEV tenant, and then use that package to populate with pages etc a site on my PROD Tenant.

    As an aside – are you aware of any other ways of doing this now? Perhaps one of the MS SPO engineers has created something too?

    Kindest regards

    Sam

    Reply
    • Sam Foster

      October 18, 2018

      Hello,

      I worked out what was wrong. I’ve been playing around so much I had created my test site and I had added pages, but what I had NOT done was edit and then publish the home page. It turns out I didn’t have to make any edits – didn’t need to add any text etc – but simply opening in edit mode, and then publishing is enough to make your script work fine!

      Always expect the unexpected!

      Does that compute with regards why it would be failing?

      Kind regards

      Sam

      Reply
  10. Sam Foster

    October 18, 2018

    Me again!

    I’m loving this script of yours. I’ve now had a fiddle with the XML and have added new pages programmatically by editing it. I have also seen that I can add News items.

    How though can I export Events that I have added to a modern comms site?

    Any pointers?

    Kind regards

    Reply
  11. Fernando

    November 5, 2018

    Hello João, greetings from Portugal.
    First of all, I want to thank you for sharing your knowledge in the Sharepoint world with the entire Web community.
    I have some knowledge of Sharepoint 2010 and have now started working with Sharepoint Online. I needed to create a site template, and your solution seemed remarkably useful to me. I tried it and it worked very well. However there is a problem that I still can not figure out how to solve and for this reason I am exposing the same to see if anyone can help me.
    The situation:
    1- My original site has a library of site pages and I created in this library 2 new columns (Choice type), for example: Category, Tags.
    2- I created 3 new pages and in the details of the page the fields “Category” and “Tags” have been filled.
    3- When accessing the Library Settings, I see that the created columns have the following content types assigned in “Used in”: Site Page, Republishing Page, Wiki Page, Web Page Page.
    4- On the Home Page, I added 2 “Highlighted Content” Web Parts to filter pages from the Site Pages library by Property.
    5- I created the template.xml using your great script.
    6- I created a new site and applied the template.xml.
    The Problem:
    7- All Pages have been created, but when I access the Site Pages library, I see that the “Category” and “Tags” columns are empty. By accessing the Library Settings, I see that the Category and Tags columns don`t have any content types assigned in “Used in”. Can the problem be related to this?
    8- On the Home page, the “Enhanced Content” Web Parts appear unconfigured because the “Categories” and “Tags” columns are empty. Question: Is there a way to apply the template to a new site and the columns created in the Site Page Library are filled out?

    Reply
    • João Ferreira

      November 25, 2018

      Hello Fernando,

      PnP provisioning engine will not get the content you create with the pages automatically.
      It get the definitions but the content is not added, this is a limitation added by design to avoid this tool being used as migration tool.
      Although it does not get the content by default you can add it manually to your xml file, in most cases the schema has support for it, you can check the provisioning schema in the page below.
      https://github.com/SharePoint/PnP-Provisioning-Schema

      My best,

      João Ferreira

      Reply
  12. Nick

    December 14, 2018

    Hi João – many thanks for this, a really useful script. Your blog posts are excellent as well.

    Reply
  13. Sam

    January 15, 2019

    Hi João,
    is there any way to hire you to fix the small issues I am getting with the template?
    Regards
    Sam

    Reply
    • João Ferreira

      March 26, 2019

      Hi Sam,

      Can you please let me know what are the issues you are having with the template?

      Reply
  14. Nick Brattoli

    February 6, 2019

    Hello,

    I am trying to use this script today, and it doesn’t seem to work on any new sites. As soon as it gets to each page, I get an error like so:
    “Get-PnPProvisioningTemplate : Object reference not set to an instance of an object.
    At c:\path\GetTemplate.ps1:38 char:4
    + Get-PnPProvisioningTemplate -Out $($saveDir.Path + “\” + …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : WriteError: (:) [Get-PnPProvisioningTemplate], NullReferenceException
    + FullyQualifiedErrorId : EXCEPTION,SharePointPnP.PowerShell.Commands.Provisioning.GetProvisioningTemplate”

    This seems to be an issue with the get-pnpprovisioningtemplate command when it hits the modern pages. I can replicate the issue with that single command, but I can make it go away by deleting pages.

    This script hasn’t worked on two separate sites I’ve made today. However, it works fine when I test it on older sites that were already created. New pages or not. It’s a fun issue.

    Reply
    • João Ferreira

      March 7, 2019

      Hi Nick,

      I wasn’t able to reproduce this issue locally.
      Were you able to deploy the sites?

      My best,

      João Ferreira

      Reply

Leave a Reply


I've been working with Microsoft Technologies over the last ten years, mainly focused on creating collaboration and productivity solutions that drive the adoption of Microsoft Modern Workplace.

%d bloggers like this: