The out of the box functionality of SharePoint Online to organize your intranet is getting better and better and covers a lot of standard use cases. You have the possibilities to organize it by Hub-Sites, Highlighted Content-, News, Sites- and many more Webparts. But in real life they often do not fulfill all requirements. Also, you are restricted when it comes to customizing the layout and the look & feel. Recently, a customer of mine needed an overview page for different project types. My idea to solve the problem was to use PnP Provisioning Templates to provision and tag different project types/sites and then use the SharePoint Online search to display them in the required overview page.
The ingredients to implement this solutions are:
- PnP Provisioning Templates
- PnP PowerShell
- SharePoint Online PowerShell
- SharePoint Online Search
- SharePoint Framework modern search Web Parts
Check out the whole post to see how I implemented it…
Setup the PnP Templates
For each project type I created a separate PnP Template xml file. In these templates, you can let your creativity off. Important for this solution is that you name the templates in a logic manner.
1 2 3 4 5 6 7 8 9 10 11 |
<?xml version="1.0"?> <pnp:Provisioning xmlns:pnp="http://schemas.dev.office.com/PnP/2018/05/ProvisioningSchema"> <pnp:Preferences Author="Jens Haile, IF-Blueprint AG" Generator="OfficeDevPnP.Core, Version=2.27.1806.0, Culture=neutral, PublicKeyToken=5e633289e95c321a" /> <pnp:Templates ID="CONTOSO.TEMPLATES"> <pnp:ProvisioningTemplate ID="CONTOSO.HR.PROJECT" Version="1" BaseSiteTemplate="GROUP#0" Scope="RootSite"> <!-- ADD TEMPLATE CONTENT HERE --> </pnp:ProvisioningTemplate> </pnp:Templates> </pnp:Provisioning> |
The important XML tag / line is
<pnp:ProvisioningTemplate ID=”CONTOSO.HR.PROJECT” Version=”1″ BaseSiteTemplate=”GROUP#0″ Scope=”RootSite”>
Later on, this ID will be searchable and will help to differentiate single project templates in the search webpart. In my demo example I create a HR (CONTOSO.HR.PROJECT) and a SALES (CONTOSO.SALES.PROJECT) project template. Each template creates its own folder structure in the default document library, but as I said before, you have nearly endless possibilities to create customized Team- and Communication Sites with PnP Provisioning Templates.
You can get more information about PnP Provisioning Templates here Remote Provisioning Schema
Create modern TeamSites and apply the templates
After you have created awesome templates you can now create some modern SharePoint TeamSites and apply the templates to them. In my example I do this with some simple PowerShell commands, but it is up to you to do this with the magic of SiteDesigns, SiteScripts, Flow and AzureFunctions (you can find a good overview here).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# Connect to SharePint Connect-PnPOnline "https://[YOUR_TENANT].sharepoint.com" -Credentials $credential # Create an HR project $hrProject1 = New-PnPSite -Title "HR project 1" -Alias "prj_0001" -Type TeamSite # Enable scripting Set-SPOsite $hrProject1 -DenyAddAndCustomizePages 0 # Close previous connection Disconnect-PnPOnline # Connect to the new created site Connect-PnPOnline $hrProject1 -Credentials $credential # Set a SiteCollection admin Add-PnPSiteCollectionAdmin -Owner "yourAdmin@contoso.com" # Apply the HR project template Apply-PnPProvisioningTemplate .\Contoso_HR_Project.xml #Create a Sales project $salesProject1 = New-PnPSite -Title "Sales project 1" -Alias "prj_0002" -Type TeamSite # Enable scripting Set-SPOsite $salesProject1 -DenyAddAndCustomizePages 0 # Close previous connection Disconnect-PnPOnline # Connect to the new created site Connect-PnPOnline $salesProject1 -Credentials $credential # Set a SiteCollection admin Add-PnPSiteCollectionAdmin -Owners "yourAdmin@contoso.com" # Apply the Sales project template Apply-PnPProvisioningTemplate .\Contoso_Sales_Project.xml |
There a two important steps in this script:
- Set-SPOsite $hrProject1 -DenyAddAndCustomizePages 0 ⇒ By default scripting is disabled by modern Team- and CommunicationSites. You must allow scripting for your site. If you don’t do that the necessary property will not be written into the site properties! In my little script I did not disable scripting again. But to be save you can of course do this with the command Set-SPOsite $hrProject1 -DenyAddAndCustomizePages 1 . (Read more about this topic here)
- Add-PnPSiteCollectionAdmin -Owners “yourAdmin@contoso.com” ⇒ When creating a modern Team- or Communication site, the related AD-Group will be set as SiteCollection admin. And for some reasons, the search crawler will not index the site properties although they are correctly registered in the vti_indexedpropertykeys property. But after setting an additional SiteCollection admin the properties will be indexed (I will explain which property matter to us latter). In my case this account is my global admin, and because I was so annoyed about the fact that it is not working when only the AD-Group is the SiteCollection admin, I stopped my investigations here. (Please let me know if you have more information about this behavior! For example, is it necessary to and a second SiteCollection admin or is it ok to add an user to the owner group, or is it required that the user has Office 365 SharePoint admin right?). Thanks to Jonne Klein her article led me into the right direction!
The crawled property relevant to us is the _PnP_ProvisioningTemplateId. This property stores information about which PnP template was applied to the site (CONTOSO.HR.PROJECT or CONTOSO.SALES.PROJECT). When you check the properties of one of this new created site (e.g. my HR project 1 site) with the following REST call:
1 |
https://[YOUR_TENANT].sharepoint.com/sites/prj_0001/_api/web/allproperties |
You will get a result like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:georss="http://www.georss.org/georss" xmlns:gml="http://www.opengis.net/gml" xml:base="https://m365x920452.sharepoint.com/sites/prj_hr_0001/_api/"> <id>https://m365x920452.sharepoint.com/sites/prj_hr_0001/_api/web/allproperties</id> <category term="SP.PropertyValues" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> <link rel="edit" href="web/allproperties" /> <title /> <updated>2019-03-10T22:27:25Z</updated> <author> <name /> </author> <content type="application/xml"> <m:properties> <d:FollowLinkEnabled>TRUE</d:FollowLinkEnabled> <d:SiteCollectionGroupId5fd72cd0_x002d_3a5b_x002d_4092_x002d_b7c1_x002d_91befe788312>345035e8-1f4e-4cee-b662-cf8796e798cc</d:SiteCollectionGroupId5fd72cd0_x002d_3a5b_x002d_4092_x002d_b7c1_x002d_91befe788312> <d:vti_x005f_approvallevels>Approved Rejected Pending\ Review</d:vti_x005f_approvallevels> <d:dlc_x005f_expirationlastrun>3/10/2019 12:46:20 AM</d:dlc_x005f_expirationlastrun> <d:NoCrawl>false</d:NoCrawl> <d:disabledhelpcollections /> <d:vti_x005f_defaultlanguage>en-us</d:vti_x005f_defaultlanguage> <d:GroupDocumentsListId>ae9195d9-14dc-4701-857f-0eea069b4edf</d:GroupDocumentsListId> <d:GroupDocumentsUrl>Shared Documents</d:GroupDocumentsUrl> <d:taxonomyhiddenlist>212dc1b6-d1aa-488a-a097-38c97b3feba4</d:taxonomyhiddenlist> <d:OData__x005f_PnP_x005f_ProvisioningTemplateInfo> {"TemplateId":"CONTOSO.HR.PROJECT","TemplateVersion":1.0,"TemplateSitePolicy":null,"ProvisioningTime":"2019-03-09T16:05:27.7911755+01:00","Result":true} </d:OData__x005f_PnP_x005f_ProvisioningTemplateInfo> <strong><d:OData__x005f_PnP_x005f_ProvisioningTemplateId>CONTOSO.HR.PROJECT</d:OData__x005f_PnP_x005f_ProvisioningTemplateId> </strong><d:vti_x005f_categories> Travel Expense\ Report Business Competition Goals/Objectives Ideas Miscellaneous Waiting VIP In\ Process Planning Schedule </d:vti_x005f_categories> <d:enabledhelpcollections>VGSEndUser</d:enabledhelpcollections> <d:GroupType>Private</d:GroupType> <d:vti_x005f_sitemasterid>34fc3ca8-da35-4287-a8db-9f76f7837e59</d:vti_x005f_sitemasterid> <d:vti_x005f_searchversion m:type="Edm.Int32">4</d:vti_x005f_searchversion> <d:vti_x005f_associategroups>5;4;3</d:vti_x005f_associategroups> <d:GroupId>10888cf3-5233-4533-b85a-3923fa7f54f5</d:GroupId> <d:ProvCorrelationId>434ec79e-203c-0000-5b37-94dc9209fa11</d:ProvCorrelationId> <d:dlc_x005f_policyupdatelastrun>3/9/2019 11:34:24 PM</d:dlc_x005f_policyupdatelastrun> <d:vti_x005f_createdassociategroups>3;4;5</d:vti_x005f_createdassociategroups> <d:GroupAlias>prj_hr_0001</d:GroupAlias> <d:vti_x005f_extenderversion>16.0.0.8620</d:vti_x005f_extenderversion> <d:profileschemaversion>1</d:profileschemaversion> <d:RelatedGroupId>10888cf3-5233-4533-b85a-3923fa7f54f5</d:RelatedGroupId> <d:vti_x005f_associatevisitorgroup>4</d:vti_x005f_associatevisitorgroup> <d:vti_x005f_associateownergroup>3</d:vti_x005f_associateownergroup> <strong><d:vti_x005f_indexedpropertykeys>XwBQAG4AUABfAFAAcgBvAHYAaQBzAGkAbwBuAGkAbgBnAFQAZQBtAHAAbABhAHQAZQBJAGQA|</d:vti_x005f_indexedpropertykeys></strong> <d:HomepageProvisioned>1</d:HomepageProvisioned> <d:vti_x005f_associatemembergroup>5</d:vti_x005f_associatemembergroup> <d:LastGroupSitePrivacyUpdated>636877704751662193</d:LastGroupSitePrivacyUpdated> </m:properties> </content> </entry> |
The interesting values are these two (NOTE if scripting had not been enabled for the sites, these entries would not have been written into the site properties!)
1 2 |
<d:OData__x005f_PnP_x005f_ProvisioningTemplateId>CONTOSO.HR.PROJECT</d:OData__x005f_PnP_x005f_ProvisioningTemplateId> <d:vti_x005f_indexedpropertykeys>XwBQAG4AUABfAFAAcgBvAHYAaQBzAGkAbwBuAGkAbgBnAFQAZQBtAHAAbABhAHQAZQBJAGQA|</d:vti_x005f_indexedpropertykeys> <d:HomepageProvision |
But as we enabled it, the _PnP_ProvisioningTemplateId property is registered in the vti_indexpropertykeys (to decode the base64 coded values, you can use this tool).
XwBQAG4AUABfAFAAcgBvAHYAaQBzAGkAbwBuAGkAbgBnAFQAZQBtAHAAbABhAHQAZQBJAGQA ⇒ _PnP_ProvisioningTemplateId
So, if everything works fine, the crawled property will appear sooner or later in your SharePoint Online search schema (NOTE: only when you added a second SiteCollection admin!)
Setup the SharePoint Online Search
When the crawled property _PnP_ProvisioningTemplateId appears as crawled property you must map it to a managed property. In my example I used the RefinableString00 managed property. I did this on the global tenant search level.
https://[YOUR_TENANT]-admin.sharepoint.com/_layouts/15/searchadmin/ta_listcrawledproperties.aspx?level=tenant
After that step you can use the RefinableString00 to search and filter for it in the search webpart.
Deploy the SPFx Search
With the awesome and super configurable SPFx Search WebPart you can search and filter by your previously created ManagedProperty RefinableString00. But first, you must upload it to your App-Cataloge (this article describes how you can setup a SharePoint Online App-Catalog). If your App-Cataloge is available you can upload the pnp-react-search-refiners.sppkg .If you have not chosen a tenant wide deployment, then you must add the SPFx WebPart on the site on which you want to use it.
Add the WebPart to you page
After this, you can place the Search WebPart on a page. In my example I have placed two Search WebPart with different configurations.
Configure the WebPart
Now it is time to demonstrate the power of this awesome WebPart. What we want to do is, adding a search query to get the sites filtered by the _PnP_ProvisioningTemplateId (exactly by the mapped ManagedProperty RefinableString00). Afterwards, we will pimp the layout with a handlebar template.
- Configure the Search query keywords
- Add the following Query template:
ContentClass=sts_site RefinableString00:”CONTOSO.HR*”
The template returns only elements which are of type sts_site and whose RefinableString00 attribute starts with “CONTOSO.HR*”. Remember this is the PnP Template ID which we have defined in our PnP template. In my example I removed the Sort order, Sortable properties and the Refiners.As you see in the image above, the search returns only site on which the PnP ProvisioningTemplate ID=”CONTOSO.HR.PROJECT”. Nice! But the look is a bit old school, so it is time to pimp it up.
- To change the look we can use the integrated handlebars templating engine.
This is my simple code:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849<style>.singleCard{box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);}.template_root {margin: 30px;}.header{margin-left:17px;margin-bottom: 5px;border-bottom: lightblue;border-bottom-style: solid;border-bottom-width: 3px;display:inline;padding-bottom: 5px;}</style><div class="template_root ms-slideRightIn10 "><div class="header"><i style="font-size: 24px;" class="ms-Icon ms-Icon--Feedback" aria-hidden="true"></i><span class="ms-font-xxl">HR Projects</span></div><div class="template_defaultCard" style="margin-top:10px"><div class="ms-Grid"><div class="ms-Grid-row">{{#each items as |item|}}<div class="ms-Grid-col ms-sm12 ms-md6 ms-lg4"><div class="singleCard "><div class="previewImg ms-bgColor-themePrimary" style="background-image: url("{{getPreviewSrc item}}")"></div><li class="ms-ListItem ms-ListItem--document" tabindex="0"><div class="cardInfo"><span class="ms-ListItem-primaryText "><a href="{{getUrl item}}">{{Title}}</a></span><span class="ms-ListItem-secondaryText">{{Description}}</span></div></li></div></div>{{/each}}</div></div></div></div>
VOILA! If you add a second search WebPart with a different Query template, you can create a nice landing page like this: