8/31/2017

Programmatically Change Modern Page Banner Image

In this post I want to describe how to programmatically change Modern Page Banner Image
The code below is written on JavaScript using JSOM (JavaScript Client Object Model) but it can be easily converted to C# (CSOM) and PowerShell as well.
Let's say you've created some site using any provisioning mechanism (for example, PnP Provisioning Engine). And you also created multiple modern pages on the site and want to change the banner image for all of them.
After some investigation I found a way to achieve that: list items in Site Pages library have a field named LayoutWebpartsContent. If you look at the value of this field you'll see something like:
<div>
  <div data-sp-controldata="%7B%22id%22&#58;%22cbe7b0a9-3504-44dd-a3a3-0e5cacd07788%22,%22instanceId%22&#58;%22cbe7b0a9-3504-44dd-a3a3-0e5cacd07788%22,%22title%22&#58;%22Title%20Region%22,%22description%22&#58;%22Title%20Region%20Description%22,%22serverProcessedContent%22&#58;%7B%22htmlStrings%22&#58;%7B%7D,%22searchablePlainTexts%22&#58;%7B%7D,%22imageSources%22&#58;%7B7D,%22links%22&#58;%7B%7D%7D,%22dataVersion%22&#58;%221.0%22,%22properties%22&#58;%7B%22title%22&#58;%22Logo%20test%22,%22imageSourceType%22&#58;4,%22translateX%22&#58;50,%22translateY%22&#58;50%7D%7D" data-sp-canvascontrol=""></div>
</div>
So it's an HTML markup that contains interesting attribute data-sp-controldata. It looks like some escaped JSON.
Let's unescape it and also replace &#58; with :. The result looks like that:
{
    "id": "cbe7b0a9-3504-44dd-a3a3-0e5cacd07788",
    "instanceId": "cbe7b0a9-3504-44dd-a3a3-0e5cacd07788",
    "title": "Title Region",
    "description": "Title Region Description",
    "serverProcessedContent": {
        "htmlStrings": {},
        "searchablePlainTexts": {},
        "imageSources": {},
        "links": {}
    },
    "dataVersion": "1.0",
    "properties": {
        "title": "Logo test",
        "imageSourceType": 4,
        "translateX": 50,
        "translateY": 50
    }
}
If you proceed the same steps for the page that contains some banner image, you'll see a bit different result:
{
    "id": "cbe7b0a9-3504-44dd-a3a3-0e5cacd07788",
    "instanceId": "cbe7b0a9-3504-44dd-a3a3-0e5cacd07788",
    "title": "Title Region",
    "description": "Title Region Description",
    "serverProcessedContent": {
        "htmlStrings": {},
        "searchablePlainTexts": {},
        "imageSources": {
            "imageSource": "/sites/contoso/SiteAssets/SitePages/banner.png"
        },
        "links": {}
    },
    "dataVersion": "1.0",
    "properties": {
        "title": "Logo test",
        "imageSourceType": 2,
        "translateX": 50,
        "translateY": 50
    }
}
The differences here are in imageSource and imageSourceType properties (Actually you can also see different values for tranlateX and translateY if you changed focal point in the UI): imageSource contains server relative URL of the image that is used as a banner and imageSourceType contains 2 instead of 4.
This information is enough to add (or replace) your own banner image on any page.
First, you need to add the picture to some location on your tenant - SharePoint or OneDrive.
Second, change imageSource and imageSourceType properties as shown below:
var ctx = SP.ClientContext.get_current(); // SP context
var web = ctx.get_web(); // current web
var list = web.get_lists().getByTitle('Site Pages'); // pages library
var items = list.getItems(SP.CamlQuery.createAllItemsQuery()); // all items
ctx.load(items); // loading items from server...
ctx.executeQueryAsync(function() {
    var item = items.get_item(0); // here I'm working with single item by index. But you can iterate through all pages here
    var layoutWebpartsContent = item.get_item('LayoutWebpartsContent'); // getting content
    console.log(layoutWebpartsContent); // let's display the content
    
    var dataAttrContent = /data-sp-controldata="([^"]+)"/gmi.exec(layoutWebpartsContent); // getting data-sp-controldata content
    
    if (dataAttrContent.length) { 
        // we found the attribute.
        // Let's unescape and parse it to JSON
        // the content of the attribute is a 'group' in RegExp result. It will be located as second entry (with index 1)
        var unescaped = unescape(dataAttrContent[1]) // unescape
        var content = JSON.parse(unescaped.replace(/&#58;/gmi, ':')); // replace &#58; with :

        //
        // changing imageSource
        //
        if (!content.serverProcessedContent) {
            content.serverProcessedContent = {};
        }
        if (!content.serverProcessedContent.imageSources) {
            content.serverProcessedContent.imageSources = {};
        }
        content.serverProcessedContent.imageSources.imageSource = '/sites/contoso/SiteAssets/SitePages/banner.png';

        //
        // Changing imageSourceType
        //
        if (!content.properties) {
            content.properties = {};
        }
        content.properties.imageSourceType = 2;

        //
        // escaping back and updating item
        //
        debugger;
        var newContent = JSON.stringify(content);
        newContent = escape(newContent); // escaping
        newContent = newContent.replace(/%3A/gmi, ':').replace(/%2C/gmi, ','); // we need to replace %3A (:) with &#58; and %2C (,) with ,
        layoutWebpartsContent = layoutWebpartsContent.replace(dataAttrContent[1], newContent);
        item.set_item('LayoutWebpartsContent', layoutWebpartsContent);
        item.update();
        ctx.executeQueryAsync(function() {
            console.log('success');
        }, function() {
            console.log('fail');
        });
    }
});
That's it.
Now your page should have the banner you wanted.
Have fun!

2 comments:

  1. Hi Alex,

    Thank you for this blog. Appreciated. I have 2 questions here.

    1. where and how do you run the above JS code.
    2. I am having some struggle in getting this code in CSOM. do you have any sample which i can refer.

    Thanks!
    Ravi

    ReplyDelete
    Replies
    1. Hi RaviChandra,

      For the JS code - you need to run it in browser console on any classic page where the JSOM scripts are loaded. You can also run it in console on modern page but you'll need to load JSOM scripts by yourself.

      Regarding the CSOM.
      First, you need to reference SharePoint Online CSOM and Newtonsoft.Json packages from the NuGet.

      Then you can use code similar to this:

      ///
      /// Needed to serialize/deserialize JSON
      ///
      class ImageSources
      {
      public string imageSource { get; set; }
      }
      ///
      /// Needed to serialize/deserialize JSON
      ///
      class ServerProcessedContent
      {
      public ImageSources imageSources { get; set; }
      }
      ///
      /// Needed to serialize/deserialize JSON
      ///
      class Properties
      {
      public int imageSourceType { get; set; }
      }
      ///
      /// Needed to serialize/deserialize JSON
      ///
      class LayoutWebPartContent
      {
      public ServerProcessedContent serverProcessedContent { get; set; }
      public Properties properties { get; set; }
      }

      // Somewhere in your method:

      ClientContext ctx = new ClientContext(webUrl);
      ctx.Credentials = new SharePointOnlineCredentials(username, secureString);

      var sitePagesList = ctx.Web.Lists.GetByTitle("Site Pages");
      var items = sitePagesList.GetItems(CamlQuery.CreateAllItemsQuery());
      ctx.Load(items);
      ctx.ExecuteQuery();

      var item = items[0]; // here I'm working with single item by index. But you can iterate through all pages here
      var layoutWebpartContent = item["LayoutWebpartsContent"].ToString();
      var regExp = new Regex("data-sp-controldata=\"([^\"]+)\"", RegexOptions.Multiline | RegexOptions.IgnoreCase);
      var dataAttrContent = regExp.Match(layoutWebpartContent).Groups[1].Value;


      var unescaped = System.Web.HttpUtility.HtmlDecode(dataAttrContent);
      var content = JsonConvert.DeserializeObject(unescaped.Replace(":", ":"));

      //
      // changing imageSource
      //
      if (content.serverProcessedContent == null)
      {
      content.serverProcessedContent = new ServerProcessedContent();
      }
      if (content.serverProcessedContent.imageSources == null)
      {
      content.serverProcessedContent.imageSources = new ImageSources();
      }
      content.serverProcessedContent.imageSources.imageSource = "/sites/contoso/SiteAssets/SitePages/banner.png";

      //
      // Changing imageSourceType
      //
      if (content.properties == null)
      {
      content.properties = new Properties();
      }
      content.properties.imageSourceType = 2;

      //
      // escaping back and updating item
      //
      var newContent = JsonConvert.SerializeObject(content);
      newContent = System.Web.HttpUtility.HtmlEncode(newContent); // escaping
      newContent = newContent.Replace("%3A", ":").Replace("%2C", ","); // we need to replace %3A (:) with : and %2C (,) with ,
      layoutWebpartContent = layoutWebpartContent.Replace(dataAttrContent, newContent);
      item["LayoutWebpartsContent"] = layoutWebpartContent;
      item.Update();
      ctx.ExecuteQuery();

      Basically, it's almost a copy-paste with the additional code to work with JSON.

      Delete