An awesome part of SPFx is the ability to create SharePoint Framework Extensions. At the time of this writing, you can write three types of SPFx extensions:
This articles doesn’t try to explain how to create extensions — there are many great examples on the SharePoint Framework Extensions Samples & Tutorial Materials GitHub repo, and the Overview of SharePoint Framework Extensions tutorial is a pretty place to start if you haven’t played with extensions.
In this article, I’ll share a PowerShell script I use to deploy to many sites at once.
But first, a few things you need to know:
When you have a SharePoint tenant with lots and lots of sites, and you need to provide different configuration properties for each site, it can become painful to deploy an extension everywhere.
Sure, the solution deployment step is easy, just make sure that your solution-package.json has "skipFeatureDeployment": true,
and SharePoint will kindly offer to automatically deploy your solution to every site for you.
But to add an extension as a custom user action and provide configuration properties, you need to call a command or use some scripts:
When I need to do just one site, I’ll often use the SPFx-extensions-cli, but when I need to do a whole bunch of sites, I like to use the PnP PowerShell cmdlets and PowerShell.
The idea came from the RegionsFooterProvisionCustomizer.ps1 script on Paolo Pialorsi’s awesome Regions Footer Application Customizer example, which goes like this:
$credentials = Get-Credential
Connect-PnPOnline "https://.sharepoint.com/sites/" -Credentials $credentials
$context = Get-PnPContext
$web = Get-PnPWeb
$context.Load($web)
Execute-PnPQuery
$ca = $web.UserCustomActions.Add()
$ca.ClientSideComponentId = "67fd1d01-84e8-4fbf-85bd-4b80768c6080"
$ca.ClientSideComponentProperties = "{""SourceTermSetName"":""Regions""}"
$ca.Location = "ClientSideExtension.ApplicationCustomizer"
$ca.Name = "RegionsFooterCustomAction"
$ca.Title = "RegionsFooterCustomizer"
$ca.Description = "Custom action for Regions Footer Application Customizer"
$ca.Update()
$context.Load($web.UserCustomActions)
Execute-PnPQuery
Now Paolo’s script will only work for his extension, but you can easily go in and change the ClientSideComponentId, ClientSideComponentProperties, Name, Title and Description and make it your own. And if you mistakenly re-run the script for the same site twice, the extension will appear twice.
But I wanted to repeat this for each site on one of my tenant’s bazillion sites, and provide different configuration properties — if necessary. I also wanted to be able to re-run the script as many times as I wanted. Finally, I wanted the customer to be able to simply provide a CSV with a list of sites where they wanted the extensions applied.
So I made tweaked Paolo’s code to read the list of sites from aCSV file and apply the extension to each site. I borrowed a lot of this script from another example on the SharePoint Framework Extensions Samples & Tutorial Materials GitHub repo, but I can’t find it anymore, so I can’t tell who I should give the credit to. Let me know in the comments if you know who deserves the credits. I’m lazy, but I’m not a thief 🙂
First, make sure that you install the PnP PowerShell cmdlets on your workstation.
Then create a new PowerShell file and copy this code into it:
$credentials = Get-Credential
# Import the list of sites where we want to apply
$sitesToProcess = import-csv "sites.csv"
# details of custom action/SPFx extension
[guid]$spfxExtId = "[extension id goes here]"
[string]$spfxExtName = "[extension name goes here]"
[string]$spfxExtTitle = "[extension title goes here]"
[string]$spfxExtGroup = "[extension group goes here]"
[string]$spfxExtDescription = "[extension description goes here]"
[string]$spfxExtLocation = "ClientSideExtension.ApplicationCustomizer"
[string]$spfxExtension_Properties = "[properties JSON goes here]"
function Add-CustomActionForSPFxExt ([string]$url, $clientContext) {
Write-Output "-- About to add custom action to: $url"
$rootWeb = $clientContext.Web
$clientContext.ExecuteQuery()
$customActions = $rootWeb.UserCustomActions
$clientContext.Load($customActions)
$clientContext.ExecuteQuery()
$custAction = $customActions.Add()
$custAction.Name = $spfxExtName
$custAction.Title = $spfxExtTitle
$custAction.Description = $spfxExtDescription
$custAction.Location = $spfxExtLocation
$custAction.ClientSideComponentId = $spfxExtId
$custAction.ClientSideComponentProperties = $spfxExtension_Properties
$custAction.Update()
$clientContext.ExecuteQuery()
Write-Output "-- Successfully added extension"
Write-Output "Processed: $url"
}
function Remove-CustomActionForSPFxExt ([string]$extensionName, [string]$url, $clientContext) {
Write-Output "-- About to remove custom action with name '$($extensionName)' from: $url"
$actionsToRemove = Get-PnPCustomAction -Web $clientContext.Web | Where-Object {$_.Location -eq $spfxExtLocation -and $_.Name -eq $extensionName }
Write-Output "-- Found $($actionsToRemove.Count) extensions with name $extensionName on this web."
foreach ($action in $actionsToRemove) {
Remove-PnPCustomAction -Identity $action.Id
Write-Output "-- Successfully removed extension $extensionName from web $url."
}
Write-Output "Processed: $url"
}
# -- end functions --
foreach ($site in $sitesToProcess) {
$ctx = $null
$url = $site.Url
try {
Connect-PnPOnline -Url $url -Credentials $credentials
Write-Output ""
Write-Output "Authenticated to: $url"
$ctx = Get-PnPContext
}
catch {
Write-Error "Failed to authenticate to $url"
Write-Error $_.Exception
}
# Make sure have a context before continuing
if ($ctx) {
# Find out if the extension is already added
$existingActions = Get-PnPCustomAction -Web $ctx.Web | Where-Object {$_.Location -eq $spfxExtLocation -and $_.Name -eq $spfxExtName }
# Count how many existing extensions we found
$count = $($existingActions.Count)
# Don't re-install extension if it is already there
if ($count -ge 1) {
#This assumes that you don't want to duplicate extensions. If you do, feel free to change the logic below
if ($count -eq 1) {
Write-Output "Extension is already applied"
}
else {
Write-Warning "Extension is duplicated!"
}
}
else {
# Add the extension
Add-CustomActionForSPFxExt $url $ctx
Write-Output "-- Successfully added extension $spfxExtName to web $url."
}
#Add-CustomActionForSPFxExt $url $ctx
#Remove-CustomActionForSPFxExt $spfxExtName $site $ctx
#Get-PnPCustomAction -Web $ctx.Web | Where-Object {$_.Location -eq "ClientSideExtension.ApplicationCustomizer" }
}
}
Making sure to replace all the [sections in bold] with your own information. I get the name and id from the extension’s manifest.json file.
Then, create a CSV file containing all the sites you want to get the extension. It should look like this:
Url
https://yourtenantgoeshere.sharepoint.com/sites/Employee
https://yourtenantgoeshere.sharepoint.com/sites/Employee/About
https://yourtenantgoeshere.sharepoint.com/sites/Employee/Calendars
https://yourtenantgoeshere.sharepoint.com/sites/Employee/Learning
https://yourtenantgoeshere.sharepoint.com/sites/Employee/FAQs
https://yourtenantgoeshere.sharepoint.com/sites/Employee/Learning
https://yourtenantgoeshere.sharepoint.com/sites/Employee/News
https://yourtenantgoeshere.sharepoint.com/sites/Employee/InformationTechnology
https://yourtenantgoeshere.sharepoint.com/sites/Employee/MarketingAndCommunications
https://yourtenantgoeshere.sharepoint.com/sites/Employee/Security
https://yourtenantgoeshere.sharepoint.com/sites/Employee/EnvironmentalSustainability
https://yourtenantgoeshere.sharepoint.com/sites/Employee/HealthAndSafety
https://yourtenantgoeshere.sharepoint.com/sites/Employee/Fundraising
https://yourtenantgoeshere.sharepoint.com/sites/Employee/Glossary
https://yourtenantgoeshere.sharepoint.com/sites/Employee/Parking
https://yourtenantgoeshere.sharepoint.com/sites/Employee/purchasing
Using your own urls, and saving it as sites.csv in the same folder as the PowerShell script.
Then you can run the script and it’ll connect to every site and apply the extension and provide the configuration properties, but only if the extension hasn’t already been installed.
You could also tweak the script and the CSV to pass different configuration properties for each site, but I’ll reserve it for another post.
Leave me a comment if you’d like me to post it.
I hope it helps!