Web/Site Templates in SharePoint Online Modern Pages? No problem.

Let’s imagine that we want to have site template with predefined design, sections, web parts on it, lists with additional fields, custom page settings etc. etc.

If you are familiar with SharePoint On-Prem versions there we have Site Definitions and Web Templates for stuff like this. But what can we do now in SharePoint Modern Sites times in a combination with SharePoint Online?

We have to learn something new – there we have Site Scripts and Site Designs for some specific common steps in creation process of our Web/Site Templates. What you could do with Site Scripts you could see on that link.
But scope of modifying with Site Scripts is a lil’bit small, so we have to take a look into alternatives.

In SharePoint world we have SharePoint PowerShell modules – so we could use PowerShell to modify our Modern Pages in SharePoint Online. But we want to make this happen automatically when new Web/Site was created -> so we want to make this happen with Web/Site Template.
That is possible only with Site Scripts and Site Designs. Fortunately we could connect our PowerShell scripts with Site Scripts and Site Designs via Microsoft Flow, which could call Azure Function App which contains PowerShell script.

But PnP team go further with PnP Provisioning Engine in their PnP PowerShell modules where you could easly specify Web/Site Template in one XML file and put it into your PowerShell script which is called from Site Scripts via Flow.

Let’s take a look into a simple example.

We will work with SharePoint Online so you have to install PnP PowerShell module for SharePoint Online:

Install-Module SharePointPnPPowerShellOnline

Or you have to update it to latest version if you have already installed it:

Update-Module SharePointPnPPowerShellOnline

Then you have to connect to one of your Modern Site on your SharePoint Online which could be Modern Team Site or Modern Communication Site. In my example I will work with Modern Communication Site.

Connect-PnPOnline -Url "https://rr87.sharepoint.com/sites/TestCMS4" -Credentials (Get-Credential)

After that you could simply add a new field into existing SitePages library with next command:

Add-PnPField -List "SitePages" -DisplayName "Tip" -InternalName "NewsType" -Type Choice -Required -Group "News Site Columns" -AddToDefaultView -Choices "Novosti", "Dogajanja", "Dnevne novice"

And this is for example our PowerShell script for our Web/Site template in SharePoint Online which could be called from Site Scripts via Flow. But we want more so we will use PnP Provisioning Engine.

We want to create Site Template (like on image below) where we want to have two column section.
In left section we want to have main News Web Part. In right we want to have custom Tiles Web Part, News Web Part for specific news category and Event Web Part.

For that reason we have to create one XML file which could be named CMS.xml.

<pnp:Provisioning xmlns:pnp="http://schemas.dev.office.com/PnP/2018/01/ProvisioningSchema">
  <pnp:Preferences Generator="OfficeDevPnP.Core, Version=2.21.1712.2, Culture=neutral, PublicKeyToken=5e633289e95c321a">
  </pnp:Preferences>
  <pnp:Localizations>
    <!-- Resources Files -->
    <pnp:Localization LCID="1033" Name="cms" ResourceFile="resources\cms.en-us.resx" />
    <pnp:Localization LCID="1050" Name="cms" ResourceFile="resources\cms.hr.resx" />
    <pnp:Localization LCID="1060" Name="cms" ResourceFile="resources\cms.sl-si.resx" />
    <pnp:Localization LCID="1071" Name="cms" ResourceFile="resources\cms.mk.resx" />
    <pnp:Localization LCID="3098" Name="cms" ResourceFile="resources\cms.sr-sp.resx" />
  </pnp:Localizations>
  <pnp:Templates ID="PORTAL-SHOWCASE-TEMPLATES">
    <pnp:ProvisioningTemplate ID="PORTAL-TEMPLATE" Version="1.0" BaseSiteTemplate="SITEPAGEPUBLISHING#0" ImagePreviewUrl="https://preview.png" Description="" Scope="RootSite" TemplateCultureInfo="1033">
      <!-- Welcome page -->
      <pnp:WebSettings WelcomePage="SitePages/Home.aspx" />
      <pnp:Security>
        <!-- Permissions -->
        <pnp:AdditionalAdministrators>
          <pnp:User Name="rasper87@rr87.onmicrosoft.com"/>
        </pnp:AdditionalAdministrators>
      </pnp:Security>
      <pnp:ClientSidePages>
        <!-- Modern Pages -->
        <pnp:ClientSidePage PageName="Home.aspx" PromoteAsNewsArticle="false" Overwrite="true" EnableComments="false" Publish="true">
          <pnp:Sections>
            <pnp:Section Order="1" Type="TwoColumnLeft">
			        <pnp:Controls>
                <!-- News WebPart -->
                <pnp:CanvasControl WebPartType="NewsReel" JsonControlData="{"dataVersion": "1.5", "serverProcessedContent": {"htmlStrings":{},"searchablePlainTexts":{"title":"{resource:NewsWebPartTitle}"},"imageSources":{},"links":{"baseUrl":"{hosturl}{site}"},"componentDependencies":{"layoutComponentId":"a2752e70-c076-41bf-a42e-1d955b449fbc"}}, "properties": {"layoutId":"ListNews","filters":[{"filterType":6,"values":["Dogajanja","Novosti",""],"fieldname":"SavNewsType","op":1}],"newsDataSourceProp":1,"dataProviderId":"viewCounts","newsSiteList":[],"renderItemsSliderValue":3,"webId":"2a0e6b11-a06b-41dd-a7cd-304cfb4a16c8","siteId":"22cd25e2-11a9-4edf-9e3d-046b8df8da4c","templateId":"ListNews","propsLastEdited":"2019-03-12T13:06:55.11Z","showChrome":true,"prefetchCount":3,"compactMode":false,"carouselSettings":{"autoplay":false,"autoplaySpeed":5,"dots":true,"lazyLoad":true,"metadata":true,"swipe":true},"pinnedItems":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"serializedFilterQuery":"<Where><And><Or><Or><Eq><FieldRef Name=\"SavNewsType\"/><Value Type=\"Choice\">Dogajanja</Value></Eq><Eq><FieldRef Name=\"SavNewsType\"/><Value Type=\"Choice\">Novosti</Value></Eq></Or><IsNull><FieldRef Name=\"SavNewsType\"/></IsNull></Or><Eq><FieldRef Name=\"FSObjType\" /><Value Type=\"Integer\">0</Value></Eq></And></Where>"}}" ControlId="a5df8fdf-b508-4b66-98a6-d83bc2597f63" Order="1" Column="1" />

                <!-- Tiles WebPart -->
                <pnp:CanvasControl WebPartType="Custom" JsonControlData="" ControlId="81cd8d91-f5b5-4ee1-b9cd-cb5b2155837f" Order="1" Column="2" />

                <!-- Dnevne Novice WebPart -->
                <pnp:CanvasControl WebPartType="NewsReel" JsonControlData="{"dataVersion": "1.5", "serverProcessedContent": {"htmlStrings":{},"searchablePlainTexts":{"title":"{resource:DnevneNoviceWebPartTitle}"},"imageSources":{},"links":{"baseUrl":"{hosturl}{site}"},"componentDependencies":{"layoutComponentId":"a2752e70-c076-41bf-a42e-1d955b449fbc"}}, "properties":{"carouselSettings":{"autoplay":false,"autoplaySpeed":5,"dots":true,"lazyLoad":true,"metadata":true,"swipe":true},"showChrome":true,"layoutId":"ListNews","prefetchCount":4,"filters":[{"filterType":6,"values":["Dnevne novice"],"fieldname":"SavNewsType","op":1}],"newsDataSourceProp":1,"dataProviderId":"viewCounts","newsSiteList":[],"renderItemsSliderValue":4,"webId":"2a0e6b11-a06b-41dd-a7cd-304cfb4a16c8","siteId":"22cd25e2-11a9-4edf-9e3d-046b8df8da4c","pinnedItems":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"serializedFilterQuery":"<Where><And><Eq><FieldRef Name=\"SavNewsType\"/><Value Type=\"Choice\">Dnevne novice</Value></Eq><Eq><FieldRef Name=\"FSObjType\" /><Value Type=\"Integer\">0</Value></Eq></And></Where>","templateId":"ListNews","propsLastEdited":"2019-03-12T13:05:17.022Z","compactMode":false}}" ControlId="a5df8fdf-b508-4b66-98a6-d83bc2597f63" Order="2" Column="2" />

                <!-- Calendar WebPart -->
                <pnp:CanvasControl WebPartType="Events" JsonControlData="{ "dataVersion": "1.2", "serverProcessedContent": {"htmlStrings":{},"searchablePlainTexts":{"title":"{resource:CalendarWebPartTitle}"},"imageSources":{},"links":{"baseUrl":"{hosturl}{site}"},"componentDependencies":{"layoutComponentId":"0447e11d-bed9-4898-b600-8dbcd95e9cc2"}}, "properties": {"selectedListId":"e116daf4-b706-43ae-9d61-160240b15ae0","selectedCategory":"","dateRangeOption":0,"startDate":"","endDate":"","isOnSeeAllPage":false,"layoutId":"Flex","dataProviderId":"Event","webId":"2a0e6b11-a06b-41dd-a7cd-304cfb4a16c8","siteId":"22cd25e2-11a9-4edf-9e3d-046b8df8da4c","layout":"Compact","dataSource":7,"sites":[],"maxItemsPerPage":20}}" ControlId="20745d7d-8581-4a6c-bf26-68279bc123fc" Order="3" Column="2" />
              </pnp:Controls>
            </pnp:Section>
          </pnp:Sections>
        </pnp:ClientSidePage>
      </pnp:ClientSidePages>
    </pnp:ProvisioningTemplate>
  </pnp:Templates>
</pnp:Provisioning>

In XML above you could see resources section, where we define resource files for some languages.
After that we define Welcome page and special Permissions section.
The main thing is ClientSidePages section where we could define Modern Pages customizations.

As you could see I want to modify existing Home.aspx modern page where I want clear all existing content from it and add one Section which is TwoColumnLeft. This mean that we will have two columns where left column is main column.

In left column we add News Web Part (WebPartType = NewsReel) with specific settings (JsonControlData). In right column we add custom Tiles Web Part (WebPartType = Custom), another News Web Part with different settings (JsonControlData) and last is Calendar Web Part (WebPartType = Events).

You could get information for JsonControlData field if you manually add Web Parts to page and manually fill their property pane. Then you could get properties for all Web Parts on specific Modern Page with that PowerShell script:

$list = Get-PnPList -Identity SitePages
$li = Get-PnPListItem -List $list -Id 1
$page = Get-PnPClientSidePage $li["FileLeafRef"]

# go throught all sections
foreach ($section in $page.Sections) {
    # go throught all controls
    foreach ($control in $section.Controls) {
        #Write-Host $control.JsonControlData
        write-host $control.PropertiesJson
        Write-Host ""
    }
}

As you could see we could use resource string from resource file with command like that:

{resource:NewsWebPartTitle}

In resource file (for example cms.en-us.resx) we could define our resources strings (NewsWebPartTitle, DnevneNoviceWebPartTitle, CalendarWebPartTitle) in a way like that:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="NewsWebPartTitle" xml:space="preserve">
    <value>News</value>
  </data>
  <data name="DnevneNoviceWebPartTitle" xml:space="preserve">
    <value>Daily News</value>
  </data>
  <data name="CalendarWebPartTitle" xml:space="preserve">
    <value>Events</value>
  </data>
</root>

This is all about out PnP Provisioning Template for PnP Provisioning Engine. We just have to call command from PnP PowerShell to apply our provisioning template to connected SharePoint Online site:

Apply-PnPProvisioningTemplate -Path "D:\Temp\Provisioning\CMS.xml"

After that you could see your Site modified like this on image above.
But as I mentioned before I want to have this Site modifications / Site Provisioning made automatically when user create new SharePoint Online Modern Site.

You could see complete process for calling the PnP provisioning engine from a site script on that link.
You just have to change PnP provisioning template named FlowDemoTemplate.xml with our CMS.xml and copy all resources files to Azure Function too.

That’s all folks!

Cheers!
Gašper Rupnik

{End.}

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Powered by WordPress.com.

Up ↑

%d bloggers like this: