Provisioning Publishing Pages using PowerShell


Deploying Publishing Pages with SharePoint Packages

Deploying your solution with some preconfigured Publishing Pages is not only of great value when using automated testing, but can also help your users build up the solution. Instead of configuring everything from scratch, they can directly start working with content and filling all placeholders. One way of deploying preconfigured Publishing Pages is to create a Feature and create all the necessary pages using code. The biggest disadvantage of such approach is probably mixing code with configuration and content. You have absolutely no overview of which pages are created where, how do they look like and should something change you have to recompile your code and rebuild and redeploy your SharePoint Package. A much better way is to configure Publishing Pages using the UI just the way you want them to, export them using the Mavention Export Page extension which is now a part of the Community Kit for SharePoint: Development Tools Edition (CKS:DEV), and use the generated XML files to provision them together with your solution. Although this process is much more convenient and manageable than using code, it still has one flaw. To provision the exported pages, you have to create Features, which is okay if you want your pages to be created for specific Web Templates, but is a waste of time if you have pages that must be provisioned only once!

Deploying Publishing Pages with PowerShell

Not so long ago I showed you how to provision List Instance data using PowerShell. Using a very similar approach you can provision your Publishing Pages. The following code snippet contains the PowerShell script required to provision Publishing Pages:

param (
    $SiteUrl = "http://mavention"
)

function Import-PublishingPage {
    param (
        $SiteUrl = $(throw "Required parameter -SiteUrl missing"),
        [xml]$PageXml = $(throw "Required parameter -PageXml missing")
    )

    $site = New-Object Microsoft.SharePoint.SPSite($SiteUrl)
    $psite = New-Object Microsoft.SharePoint.Publishing.PublishingSite($site)
    $web = Get-SPWeb $SiteUrl
    $pweb = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($web)
    $pagesListName = $pweb.PagesListName

    # get prerequisites
    $pageName = $PageXml.Module.File.Url
    if (-not($pageName)) {
        throw "Page name missing in <File Url='...'/>"
    }

    $plDefinition = $PageXml.Module.File.Property | Where { $_.Name -eq "PublishingPageLayout" }
    if (-not($plDefinition)) {
        throw "Page Layout reference missing in <File><Property Name='PublishingPageLayout' Value='...'/></File>"
    }

    $plUrl = New-Object Microsoft.SharePoint.SPFieldUrlValue($plDefinition.Value)
    $plName = $plUrl.Url.Substring($plUrl.Url.LastIndexOf('/') + 1)
    $pl = $psite.GetPageLayouts($false) | Where { $_.Name -eq $plName }

    if (-not($pl)) {
        throw "Page Layout '$plName' not found"
    }

    [Microsoft.SharePoint.Publishing.PublishingPage]$page = $null
    $file = $web.GetFile("$pagesListName/$pageName")
    if (-not($file.Exists)) {
        Write-Host "Page $pageName not found. Creating..." -NoNewline
        $page = $pweb.AddPublishingPage($pageName, $pl)
        Write-Host "DONE" -ForegroundColor Green
    }
    else {
        Write-Host "Configuring '$($file.ServerRelativeUrl)'..."
        $item = $file.Item
        $page = [Microsoft.SharePoint.Publishing.PublishingPage]::GetPublishingPage($item)
        if ($page.ListItem.File.CheckOutStatus -eq [Microsoft.SharePoint.SPFile+SPCheckOutStatus]::None) {
            $page.CheckOut()
        }
    }

    if ($PageXml.Module.File.AllUsersWebPart) {
        Write-Host "`tImporting Web Parts..." -NoNewline

        # fake context
        [System.Web.HttpRequest] $request = New-Object System.Web.HttpRequest("", $web.Url, "")
        $sw = New-Object System.IO.StringWriter
        $hr = New-Object System.Web.HttpResponse($sw)
        [System.Web.HttpContext]::Current = New-Object System.Web.HttpContext($request, $hr)
        [Microsoft.SharePoint.WebControls.SPControl]::SetContextWeb([System.Web.HttpContext]::Current, $web)

        $wpMgr = $web.GetLimitedWebPartManager("$pagesListName/$pageName", [System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared)
        foreach ($webPartDefinition in $PageXml.Module.File.AllUsersWebPart) {
            $err = $null
            $sr = New-Object System.IO.StringReader($webPartDefinition.InnerText)
            $xtr = New-Object System.Xml.XmlTextReader($sr);
            $wp = $wpMgr.ImportWebPart($xtr, [ref] $err)
            $oldWebPartId = $webPartDefinition.ID.Trim("{", "}")
            $wp.ID = "g_" + $oldWebPartId.Replace("-", "_")
            $wpMgr.AddWebPart($wp, $webPartDefinition.WebPartZoneID, $webPartDefinition.WebPartOrder)
            Write-Host "." -NoNewline
        }

        [System.Web.HttpContext]::Current = $null
        Write-Host "`n`tWeb Parts successfully imported"
    }
    else {
        Write-Host "`tNo Web Parts found"
    }

    Write-Host "`tImporting content..."
    $li = $page.ListItem
    foreach ($property in $PageXml.Module.File.Property) {
        Write-Host "`t$($property.Name)..." -NoNewline
        $field = $li.Fields.GetField($property.Name)
        if (-not($field.IsReadOnlyField)) {
            try {
                $value = $field.GetValidatedString($property.Value.Replace("~SiteCollection/", $site.ServerRelativeUrl).Replace("~Site/", $web.ServerRelativeUrl))
                if ($value) {
                    $li[$property.Name] = $value
                    Write-Host "DONE" -ForegroundColor Green
                }
                else {
                    Write-Host "SKIPPED (Invalid value)" -ForegroundColor Red
                }
            }
            catch {
                Write-Host "SKIPPED (Invalid value)" -ForegroundColor Red
            }
        }
        else {
            Write-Host "SKIPPED (ReadOnly)" -ForegroundColor Red
        }
    }
    $li.Update()
    Write-Host "`tContent import completed" -ForegroundColor Green

    $page.CheckIn("")
    $file = $page.ListItem.File
    $file.Publish("")
    #$file.Approve("")

    Write-Host "Page successfully imported" -ForegroundColor Green
}

$pages = @{
    "home_default.aspx.xml" = "/";
    "aboutus_default.aspx.xml" = "/about-us";
}

$pages.GetEnumerator() | % {
    [xml]$pageXml = Get-Content "Pages\$($_.Name)"
    Import-PublishingPage "$SiteUrl$($_.Value)" $pageXml
}

How it works

We start with defining parameters that we accept. The way this script is written is that it accepts one parameter which is the URL of the Publishing Site where the pages should be provisioned to (line 2). The list of pages itself is defined later in the script. Next we define a rather large function which does the real work creating and configuring Publishing Pages (lines 5-114). In line 116 we define the list of pages that should be imported. This list is a hashtable where the file containing the page is the key and the server-relative URL of the Publishing Web where the given Publishing Page should be imported to is the value. Two important things to remember here are that every file should contain only one exported Publishing Page and that all files should be stored in the same directory (hence the filenames prefixed with the site name). Finally, in lines 122-124 we iterate through the array of pages and import them one by one using their exported definitions. The process of importing a Publishing Pages begins with creating an instance of Site Collection and Web (lines 11-14). To avoid issues with importing we cannot unfortunately use the Get-SPSite cmdlet and have to instantiate new SPSite object. Next we verify if all prerequisites are set. We start with checking if the file name has been defined (lines 18-21) and then check if the specified Page Layout is available (lines 23-34). Before we start configuring the Publishing Page we have to ensure that it’s available. The import script supports both importing new pages (lines 38-42) as well as updating pages that already exist (lines 43-50). Once we established that the page is available, we proceed with importing Web Parts (lines 52-79). We have to process Web Parts before content to support provisioning Web Parts in content. Web Parts which are placed in content are referred to by ID which is available only after the Web Part has been imported. Additionally, to support importing Content Query Web Parts, we have to create a fake HTTP Context (lines 56-60). After all Web Parts have been imported we can proceed with importing content (lines 81-106). We import the contents by retrieving all properties from the exported Publishing Pages and iterating through them. For every property we retrieve a value and before we set it on the instance of the Publishing Page, we parse the ~SiteCollection/ and ~Site/ tokens. This allows us to provision content of our pages no matter the target URL. Finally we check in and publish the Publishing Page. If your scenario requires approval, you can uncomment line 111 to approve the Publishing Page after it has been published. A sample exported Publishing Page file may look like this:

<?xml version="1.0" encoding="utf-8"?>
<Module Name="MaventionPage" Url="$Resources:osrvcore,List_Pages_UrlName;">
  <File Path="MaventionPage\TemplatePage.aspx" Url="default.aspx" Type="GhostableInLibrary">
    <Property Name="ContentType" Value="Mavention Page" />
    <Property Name="PublishingPageLayout" Value="~SiteCollection/_catalogs/masterpage/MaventionPage.aspx, Mavention Page" />
    <Property Name="Title" Value="Mavention" />
    <Property Name="PublishingPageContent" Value="&lt;div class=&quot;ms-rtestate-read ms-rte-wpbox&quot;&gt;&lt;div class=&quot;ms-rtestate-notify  ms-rtestate-read 34935611-d8a7-437c-9794-9eb7e3708b86&quot; id=&quot;div_34935611-d8a7-437c-9794-9eb7e3708b86&quot;&gt;&lt;/div&gt;&#xD;&#xA;&lt;div id=&quot;vid_34935611-d8a7-437c-9794-9eb7e3708b86&quot; style=&quot;display:none&quot;&gt;&lt;/div&gt;&lt;/div&gt;&#xD;&#xA;" />
    <AllUsersWebPart WebPartZoneID="wpz" WebPartOrder="0" ID="34935611-d8a7-437c-9794-9eb7e3708b86">
      <![CDATA[<webParts>
  <webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
    <metaData>
      <type name="Mavention.SharePoint.MaventionNl.WebParts.SampleWebPart.SampleWebPart" />
      <importErrorMessage>Cannot import this Web Part.</importErrorMessage>
    </metaData>
    <data>
      <properties>
        <property name="AllowZoneChange" type="bool">True</property>
        <property name="ExportMode" type="exportmode">All</property>
        <property name="HelpUrl" type="string" />
        <property name="Hidden" type="bool">False</property>
        <property name="TitleUrl" type="string" />
        <property name="Description" type="string" />
        <property name="AllowHide" type="bool">True</property>
        <property name="AllowMinimize" type="bool">True</property>
        <property name="Title" type="string">Sample Web Part</property>
        <property name="ChromeType" type="chrometype">None</property>
        <property name="AllowConnect" type="bool">True</property>
        <property name="Width" type="unit" />
        <property name="Height" type="unit" />
        <property name="HelpMode" type="helpmode">Navigate</property>
        <property name="CatalogIconImageUrl" type="string" />
        <property name="AllowEdit" type="bool">True</property>
        <property name="TitleIconImageUrl" type="string" />
        <property name="Direction" type="direction">NotSet</property>
        <property name="AllowClose" type="bool">True</property>
        <property name="ChromeState" type="chromestate">Normal</property>
      </properties>
    </data>
  </webPart>
</webParts>]]></AllUsersWebPart>
  </File>
</Module>

As you can see this is just the same XML structure as you would need if you wanted to provision a Publishing Page using a Feature. While processing an XML file of an exported Publishing Page, the script retrieves the name of the page from the Url attribute in line 3 and combines it with the name of the Pages Library of the target Publishing Web. Next it imports all the Web Parts defined using the AllUsersWebPart tags and finally imports content as defined by Property tags. The page I’ve just showed you, contains one Web Part which is located in the contents of the Publishing HTML field (line 7). The way it works is that the Web Part must be imported to the wpz Web Part Zone (line 8), which is a hidden zone rendered automatically by SharePoint. And because we provide the Web Part ID ourselves (line 8 ) we don’t have to do anything to keep this working because the script automatically imports the Web Part using its ID as defined in the AllUsersWebPart tag. After you execute the script above you should end up with a Publishing Page with a Web Part added to the contents of the Publishing HTML field. Preconfigured Publishing Page provisioned using PowerShell

Others found also helpful: