Provisioning Publishing Pages

On the interweb there seems to be some confusion on how to provision publishing resources through features. Here is what works for SharePoint 2010. First you wil need a Feature scoped for Site (SPSite), put your ContenType module here. And a Feature scoped for Web and add the other modules here!

1. Create a Page ContentType

Add a New ContentType to your project. VS 2010 will ask you from which ContentType you want to inherit, select Article Page. You will get a new ContentTypeId! Supply a Name, Group and Description for your Page ContentType.

 <?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="https://schemas.microsoft.com/sharepoint/">
  <!-- Parent ContentType: Article Page (0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D) -->
  <ContentType ID="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D00d77cd9769a6443cd8dce93ee832e8303"
               Name="CustomPageContentType"
               Group="Site Content Types"
               Description="My Page Content Type"
               Inherits="TRUE"
               Version="0">
    <FieldRefs>
      <!-- Supply fields here -->
    </FieldRefs>
  </ContentType>
</Elements>

Add the (Publishing) Fields you want to use in your Publishing Pages.

2. Provision a MasterPage

Now add an empty Module, name it MasterPages and remove the sample.txt. Add your Site.Master page to your newly created module Open the Element.xml of your module Apply the following changes:

  • Add RootWebOnly="TRUE" to you Module
  • Add a Title to your File
  • Add a Description to your File
  • Add a ContentType to your File with the value $Resources:cmscore,contenttype_masterpage_name; , cause this is a masterpage!
 <?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="https://schemas.microsoft.com/sharepoint/">
  <!-- Mind the RootWebOnly attibute here -->
  <Module Name="MasterPages" Url="_catalogs/masterpage" RootWebOnly="TRUE">
    <File Path="PageLayouts\Site.master" Url="Ordina.master" Type="GhostableInLibrary" >
      <!-- Required -->
      <Property Name="Title" Value="Site Master Page" />
      <!-- optional -->
      <Property Name="MasterPageDescription" Value="Master Page for Site" />
      <!-- optional -->
      <Property Name="PublishingPreviewImage" Value="" />
      <!-- Required and important, leave this value like this (MasterPage) -->
      <Property Name="ContentType" Value="$Resources:cmscore,contenttype_masterpage_name;" />
    </File>
  </Module>
</Elements>

3. Provision a Publishing PageLayout

Add your PageLayout to either a new Module or the MasterPage Module. If you create a new Module, don't forget to add the RootWebOnly="TRUE" .

 <?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="https://schemas.microsoft.com/sharepoint/">
  <!-- Mind the RootWebOnly attibute here -->
  <Module Name="PageLayouts" Url="_catalogs/masterpage" RootWebOnly="TRUE">
    <File Path="PageLayouts\PageLayout.aspx" Url="PageLayouts/PageLayout.aspx" Type="GhostableInLibrary" >
      <!-- Required -->
      <Property Name="Title" Value="Site WelcomePage" />
      <!-- Optinal -->
      <Property Name="MasterPageDescription" Value="Description" />
      <!-- Required and important, leave this value like this, THIS IS A PageLayout! -->
      <Property Name="ContentType" Value="$Resources:cmscore,contenttype_pagelayout_name;" />
      <!-- Required and important and use your CustomPageContentType ContentTypeId here ! -->
      <Property Name="PublishingAssociatedContentType"
                Value=";#CustomPageContentType;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D00d77cd9769a6443cd8dce93ee832e8303;#"/> <!-- Optional, add default webpart(s) to zones -->
      <AllUsersWebPart WebPartZoneID="WebPartZoneTop" WebPartOrder="1">
        <![CDATA[ WebPart XML here ]]>
      </AllUsersWebPart>
    </File>
  </Module>
</Elements>

You can also add "default" WebParts to you page layout with the AllUsersWebPart tag.

4. Provision a Publishing Page

And you can even provision Publishing Pages with a module. Now add an empty Module, name it DefaultPages and remove the sample.txt. Open the Element.xml of your module I have provided a sample here, please note the following:

  • Use the ContentTypeBinding to bind your CustomPageContentType to the Pages list
  • Use the default.aspx from the default SharePoint Site Definition SPS by adding the SetupPath attribute to your Module (more here).
  • You can provision WebParts and Field Values
  • To deploy multiple Publishing Pages add another File ellement, and change the Url attribute of the File Element, to create another page.
  • You can use the feature to provision Pages from within you Onet.xml, but don't forget to activate dependand features first ( Publishing Infrastructure, Publishing, optional ContentTypes )
 <?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="https://schemas.microsoft.com/sharepoint/">
  <!-- Add your CustomPageContentType ContentTypeId to the Pages List, have had problems with this element in subsites. -->
  <ContentTypeBinding ContentTypeId="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D00d77cd9769a6443cd8dce93ee832e8303"
                      ListUrl="Pages" />
  <Module Name="ProvisionDefaultPages" Url="$Resources:osrvcore,List_Pages_UrlName;"
          SetupPath="SiteTemplates\SPS" >
    <!-- Note the Url of this File element -->
    <File Path="default.aspx" Url="default.aspx" Type="GhostableInLibrary">
      <!-- Required -->
      <Property Name="Title" Value="HomePage" />
      <!-- Required, Use the ContentType CustomPageContentType you created earlier,no need for the ContentType ID, Name is enough ! -->
      <Property Name="ContentType" Value="CustomPageContentType" />
      <!-- Required, Use the PageLayout you created earlier -->
      <Property Name="PublishingPageLayout" Value="~SiteCollection/_catalogs/masterpage/PageLayout.aspx, Site WelcomePage" />
      <!-- Optional Extra attributes -->
      <Property Name="PublishingPageContent" Value="WebContent field value" /> <!-- Optional, add default webpart(s) to zones -->
      <AllUsersWebPart WebPartZoneID="WebPartZoneTop" WebPartOrder="1">
        <![CDATA[ WebPart XML here ]]>
      </AllUsersWebPart>
    </File>
    <!-- Note the Url of this File element -->
    <File Path="default.aspx" Url="anotherpage.aspx" Type="GhostableInLibrary">
      <!-- Required -->
      <Property Name="Title" Value="Another Page" />
      <!-- Required, Use the CustomPageContentType ContentType you created earlier, no need for the ContentType ID, Name is enough -->
      <Property Name="ContentType" Value="CustomPageContentType" />
      <!-- Required, Use the PageLayout you created earlier -->
      <Property Name="PublishingPageLayout" Value="~SiteCollection/_catalogs/masterpage/PageLayout.aspx, Site WelcomePage" />
      <!-- Optional Extra attributes -->
      <Property Name="PublishingPageContent" Value="WebContent field value" /> <!-- Optional, add default webpart(s) to zones -->
      <AllUsersWebPart WebPartZoneID="WebPartZoneTop" WebPartOrder="1">
        <![CDATA[ WebPart XML here ]]>
      </AllUsersWebPart>    </File>
  </Module>
</Elements>

5. Small bonus tip

When you deploy MasterPages and PageLayouts through features, they become part of the site collection. You provision these files in the _catalog/MasterPages SPList of your Site Collection! Now default SharePoint won't load the contents of your deployed masterpages and pagelayouts into the actual ContentDb. It only uses a reference in the ContentDb to the actual file on Disk in your Feature folder. So updating these PageLayouts and MasterPages can be done by uploading a new Solution with the updated Files. But user's can change files in the Site Collection through SharePoint Designer. If a User Customizes a MasterPage or PageLayout, it becomes Customized or Un-Ghosted. This means the file contents are now stored in the ContentDb! And when you re-deploy your solution with your new feature and files (update the feature version number!) you would expect to see your changes, but you won't! You can use the object model to undo this, Re-Ghosting, by calling the RevertContentStream() on the SPFile of your PageLayout or MasterPage. I have written a method for that which you can call from the Feature Event Receiver Activate and Upgrade (version number of your feature ;-) ):

 /// <summary>
    /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade.
    /// </summary>
    /// <remarks>
    /// The GUID attached to this class may be used during packaging and should not be modified.
    /// </remarks>
    [Guid("fa5fdea4-f3be-4b17-8942-528517b2e28c")]
    public class ProvisioningEventReceiver : SPFeatureReceiver {

        public override void FeatureActivated(SPFeatureReceiverProperties properties) {
            UpdateFilesForFeature(properties);
        }

        //public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        //{
        //}

        public override void FeatureUpgrading(SPFeatureReceiverProperties properties, string upgradeActionName, System.Collections.Generic.IDictionary<string, string> parameters) {
            UpdateFilesForFeature(properties);
        }

        /// <summary>
        /// Update files for a Feature, based on defaults and file extentions.
        /// </summary>
        /// <param name="feature">The feature</param>
        public static void UpdateFilesForFeature(SPFeatureReceiverProperties feature) {
            // locate the webpart and dwp file for this feature
            string layoutdirectory = feature.Definition.RootDirectory;
            if (!layoutdirectory.EndsWith("/"))
                layoutdirectory += "/";
            layoutdirectory += "PageLayouts";

            string[] featurefiles = System.IO.Directory.GetFiles(layoutdirectory);

            List<string> featurefilesList = new List<string>();
            foreach (string filename in featurefiles) {
                string ext = System.IO.Path.GetExtension(filename).ToLowerInvariant();

                if (ext.EndsWith("aspx") || ext.EndsWith("master") || ext.EndsWith("xml"))
                    featurefilesList.Add(System.IO.Path.GetFileName(filename).ToLowerInvariant());

                Logging.LogMessage("added to list: " + filename);
            }

            if (featurefilesList.Count > 0) {
                Logging.LogMessage("no files found");
                SPSite site = Base.Feature.GetActivationSite(feature);
                if (site != null) {
                    // locate the masterpages/page layouts list
                    SPList masterpagegallery = null;

                    foreach (SPList list in site.RootWeb.Lists) {
                        if (list.BaseTemplate == SPListTemplateType.MasterPageCatalog) {
                            masterpagegallery = list;
                            break;
                        }
                    }
                    if (masterpagegallery != null) {
                        Logging.LogMessage("Checking Masterpage Gallery: " + masterpagegallery.Title);
                        Logging.LogMessage("items " + masterpagegallery.Items.Count);
                        foreach (SPListItem pagelayout in masterpagegallery.Items) {
                            Logging.LogMessage("checking layout " + pagelayout.Name);
                            if (featurefilesList.Contains(pagelayout.File.Name.ToLowerInvariant())) {
                                // just uncustomize the pages for the given feature, so that MOSS will point to
                                // the disk version of these (new) files.
                                if (pagelayout.File.CustomizedPageStatus == SPCustomizedPageStatus.Customized) {
                                    // make the page uncustomized
                                    pagelayout.File.RevertContentStream();
                                    Logging.LogMessage("Uncustomized the item: " + pagelayout.File.Name);
                                }
                                else {
                                    Logging.LogMessage("The item was already uncustomized: " + pagelayout.File.Name);
                                }
                            }
                        }
                    }
                }
            }
        }
    }