Using OfficeDev PnP to add/update modern quick links web part


In this blog post I will explain how you can add or update Quick links web part using PnP Core, source code can be found here.

Recently I’ve had a challenge to add and update Quick links web part, but turned out it’s not as easy as I thought it would be, there are couple of disussions around it which you can find here and here.

Adding Quick links web part

For adding quick link web part to a page I found a solution here, but unfortunately the json format has been changed and when you run this code you get this error.

So how can we find out what is the json format for each modern web part?

The easiest way is to open workbench page (/_layouts/15/workbench.aspx), add quick link web part to the page, then add some links:

Then click Webpart data on navbar and here is the json format you need when you want to add or update a modern web part.

I tried to copy paste the data into an editor like VSCode and compare the format with what I had in my code and found couple of changes:

        "controlType": 3,
        "id": "25f05322-31ba-479d-9de1-d73685e521ec",
        "position": {
            "zoneIndex": 1,
            "sectionIndex": 1,
            "controlIndex": 1,
            "layoutIndex": 1
        "webPartId": "c70391ea-0b10-4ee9-b2b4-006d3fcad0cd",
        "webPartData": {
            "id": "c70391ea-0b10-4ee9-b2b4-006d3fcad0cd",
            "instanceId": "25f05322-31ba-479d-9de1-d73685e521ec",
            "title": "Quick links",
            "description": "Add links to important documents and pages.",
            "serverProcessedContent": {
                "htmlStrings": {},
                "searchablePlainTexts": {
                    "items[0].title": "Win",
                    "items[1].title": "Home",
                    "items[0].description": "",
                    "items[1].description": "",
                    "items[0].altText": "",
                    "items[1].altText": ""
                "imageSources": {
                    "items[0].rawPreviewImageUrl": ",b9d0e8ec-e67e-46cc-8a43-c2a32bcdec2c,98f09acd-f3bf-4883-8760-66c554371c11/items/5e4d0736-3283-4d8a-85d8-49e9ee06ccac/driveItem/thumbnails/0/c400x99999/content?preferNoRedirect=true"
                "links": {
                    "baseUrl": "",
                    "items[0].sourceItem.url": "",
                    "items[1].sourceItem.url": ""
                "componentDependencies": {
                    "layoutComponentId": "706e33c8-af37-4e7b-9d22-6e5694d92a6f"
            "dataVersion": "2.2",
            "properties": {
                "items": [
                        "sourceItem": {
                            "guids": {
                                "siteId": "b9d0e8ec-e67e-46cc-8a43-c2a32bcdec2c",
                                "webId": "98f09acd-f3bf-4883-8760-66c554371c11",
                                "listId": "5d1abb96-2ccd-4860-b8ea-d9646ff73cf6",
                                "uniqueId": "e2964f35-e452-46b6-9cb5-20507fe98f9f"
                            "itemType": 2,
                            "fileExtension": "",
                            "progId": ""
                        "thumbnailType": 2,
                        "id": 2,
                        "fabricReactIcon": {
                            "iconName": "skypecheck"
                        "sourceItem": {
                            "guids": {
                                "siteId": "b9d0e8ec-e67e-46cc-8a43-c2a32bcdec2c",
                                "webId": "98f09acd-f3bf-4883-8760-66c554371c11",
                                "listId": "f457e8a2-384b-4191-81aa-d5dd10351f20",
                                "uniqueId": "0a211133-d26d-4062-a859-2e471c2caf5d"
                            "itemType": 0,
                            "fileExtension": ".ASPX",
                            "progId": null
                        "thumbnailType": 2,
                        "id": 1,
                        "fabricReactIcon": {
                            "iconName": "quicknote"
                "isMigrated": true,
                "layoutId": "CompactCard",
                "shouldShowThumbnail": true,
                "buttonLayoutOptions": {
                    "showDescription": false,
                    "buttonTreatment": 2,
                    "iconPositionType": 2,
                    "textAlignmentVertical": 2,
                    "textAlignmentHorizontal": 2,
                    "linesOfText": 2
                "listLayoutOptions": {
                    "showDescription": false,
                    "showIcon": true
                "waffleLayoutOptions": {
                    "iconSize": 1,
                    "onlyShowThumbnail": false
                "hideWebPartWhenEmpty": true,
                "dataProviderId": "QuickLinks",
                "webId": "98f09acd-f3bf-4883-8760-66c554371c11",
                "siteId": "b9d0e8ec-e67e-46cc-8a43-c2a32bcdec2c"
        "emphasis": {},
        "reservedHeight": 146,
        "reservedWidth": 744,
        "addedFromPersistedData": true

Although there are many properties in web part data you don’t need to be worried about all of them, so first I update QuickLinkItem.cs:

public enum ThumbnailType
    Image = 1,
    Icon = 2
public class QuickLinkItem
    public string UniqueId { get; set; }
    public string Url { get; set; }
    public string Title { get; set; }
    public string Description { get; set; } = string.Empty;
    public string AltText { get; set; } = string.Empty;
    public string ImageUrl { get; set; }
    public ThumbnailType ThumbnailType { get; set; }
    public string IconName { get; set; }

For changing the icon, if it’s one of the default fabric UI icons, we need to set IconName property and also ThumbnailType should set to 2, if it’s a image URL we need to set ImageUrl property and also ThumbnailType should be 1, then I updated GetQuickLinkItem method:

static JObject GetQuickLinkItem(int quickLinkItemId, WebInfo webInfo, QuickLinkItem quickLinkItem)
    var siteUri = new Uri(webInfo.WebUrl);
    bool external = quickLinkItem.Url[0] != '/' && !quickLinkItem.Url.StartsWith($"https://{siteUri.Host}", StringComparison.CurrentCultureIgnoreCase);
    string blankGuid = new Guid().ToString();
    JObject item = JObject.FromObject(new
        id = quickLinkItemId,
        itemType = 2,
        thumbnailType= quickLinkItem.ThumbnailType,
        siteId = external ? blankGuid : webInfo.SiteId,
        webId = external ? blankGuid : webInfo.WebId,
        uniqueId = quickLinkItem.UniqueId,
        fileExtension = string.Empty,
        progId = string.Empty

    if(quickLinkItem.ThumbnailType == ThumbnailType.Image)
        var imageObject = JObject.FromObject(new
            guids= new
                listId = Guid.NewGuid(),
                siteId = webInfo.SiteId,
                webId = webInfo.WebId,
                uniqueId = quickLinkItem.UniqueId,
        var image = new JProperty("image", imageObject);

    if(quickLinkItem.ThumbnailType == ThumbnailType.Icon)
        var iconProperty = JObject.FromObject(new
             iconName = quickLinkItem.IconName
        var fabricReactIcon = new JProperty("fabricReactIcon", iconProperty);

    return item;

I removed renderInfo, flags, activity and hasInvalidUrl, then added functionality to allow us define the link’s icon. Then we need to update GetQuickLinksServerProcessedContent:

static JObject GetQuickLinksServerProcessedContent(List<QuickLinkItem> quickLinkItems)
    JObject searchablePlainTexts = new JObject();
    JObject imageSources = new JObject();
    JObject links = new JObject();
    JObject componentDependencies = new JObject();
    componentDependencies.Add("layoutComponentId", "706e33c8-af37-4e7b-9d22-6e5694d92a6f");

    for (var index = 0; index < quickLinkItems.Count; index++)
        var quickLink = quickLinkItems[index];
        searchablePlainTexts.Add($"items[{index}].title", quickLink.Title);
        searchablePlainTexts.Add($"items[{index}].description", string.Empty);
        if (!string.IsNullOrEmpty(quickLink.ImageUrl))
            imageSources.Add($"items[{index}].image.url", quickLink.ImageUrl);              
        links.Add($"items[{index}].sourceItem.url", quickLink.Url);

    JObject serverProcessedContent = JObject.FromObject(new
        htmlStrings = new JObject(),

    return serverProcessedContent;

Changing items[index].url to items[index].sourceItem.url will fix the error you saw earlier. so if you run the code again you can see the web part provisioned without any erros (look at those icons!!):

Updating an exsiting quick links web part

To update an existing web part first we need to get the quick links web part from the page, ClientSidePage gives you all controls and web parts in a page and ClientSideWebPart class helps you to get or set web part properties.

public ClientSideWebPart GetQuickLinkWebPart(ClientSidePage page)
    var clientSideControls = page.Controls.Where(c => c.Type.Name == "ClientSideWebPart").ToList();
    var webParts = clientSideControls.ConvertAll(w => w as ClientSideWebPart);
    var quickLinks = webParts.Where(w => w.Title == "Quick links").ToList();
    if (quickLinks.Count > 0)
        return quickLinks.First();
        return null;

In this sample we just assume there is at least one quick links web part in the page, in real world you can get the web part by instance Id, to add links to the web part, we need to update ServerProcessedContent property of ClientSideWebPart, but the problem is this property doesn’t have a setter and it’s readonly! so I started looking into OfficeDev PnP source code here, and I found out that we can update that object through propertiesJson:

private void SetPropertiesJson(string json)
    if (String.IsNullOrEmpty(json))
        json = "{}";

    this.propertiesJson = json;

    var parsedJson = JObject.Parse(json);

    // If the passed structure is the top level JSON structure, which it typically is, then grab the properties from it
    if (parsedJson["webPartData"] != null && parsedJson["webPartData"]["properties"] != null)
    { = (JObject)parsedJson["webPartData"]["properties"];
    else if (parsedJson["properties"] != null)
    { = (JObject)parsedJson["properties"];
    { = parsedJson;

    // If the web part has the serverProcessedContent property then keep this one as it might be needed as input to render the web part HTML later on
    if (parsedJson["webPartData"] != null && parsedJson["webPartData"]["serverProcessedContent"] != null)
        this.serverProcessedContent = (JObject)parsedJson["webPartData"]["serverProcessedContent"];
    else if (parsedJson["serverProcessedContent"] != null)
        this.serverProcessedContent = (JObject)parsedJson["serverProcessedContent"];


Now we can easily get ServerProcessedContent which contains existing links, add new links and update PropertiesJson:

static void UpdateQuicklinkWebPart(ClientSideWebPart webpart, List<QuickLinkItem> quickLinks,WebInfo webInfo)
    var propertiesJson = JObject.Parse(webpart.PropertiesJson);
    var objServerProcessedContent = propertiesJson.Property("serverProcessedContent");
    var serverProcessedContent = webpart.ServerProcessedContent;
    var searchablePlainTexts = (JObject)serverProcessedContent["searchablePlainTexts"];
    var imageSources = (JObject)serverProcessedContent["imageSources"];
    var links = (JObject)serverProcessedContent["links"];
    var items = (JArray)propertiesJson["items"];
    var linkIndex = GetLinkIndex(searchablePlainTexts);

    foreach(var qlink in quickLinks)
        var title = new JProperty($"items[{ linkIndex}].title", qlink.Title);
        var description = new JProperty($"items[{ linkIndex}].description", qlink.Description);
        var altText = new JProperty($"items[{ linkIndex}].altText", qlink.AltText);
        var link = new JProperty($"items[{ linkIndex}].sourceItem.url", qlink.Url);
        // update searchablePlainTexts     

        // update images
        if (!string.IsNullOrEmpty(qlink.ImageUrl))
            imageSources.Add($"items[{linkIndex}].image.url", qlink.ImageUrl);

        // update links

        // update items
        var item = GetQuickLinkItem(linkIndex + 2, webInfo, qlink);
        linkIndex += 1;
    // update PropertiesJson
    if (objServerProcessedContent != null)
    propertiesJson.AddFirst(new JProperty("serverProcessedContent", serverProcessedContent));
    webpart.PropertiesJson = Newtonsoft.Json.JsonConvert.SerializeObject(propertiesJson);

So let’s test our code by adding some links to one of our existing web part:

List<QuickLinkItem> quickLinkItems = new List<QuickLinkItem>
    // internal link
    new QuickLinkItem
        UniqueId = Guid.NewGuid().ToString(),
        Url = "/teams/cl-00103/win/Forms/AllItems.aspx",
        Title = "All Items",
        ImageUrl= "/sites/CodeStore/SiteAssets/CC-Logo-only-red.png",
        ThumbnailType = ThumbnailType.Image
    // external link
    new QuickLinkItem
        UniqueId = Guid.NewGuid().ToString(),
        Url = "",
        Title = "Google Chrome",
        ThumbnailType = ThumbnailType.Icon,
var welcomePage = GetWelcomePage(ctx);
webpart = GetQuickLinkWebPart(welcomePage);
UpdateQuicklinkWebPart(webpart, quickLinkItems,webInfo);

And here is the result!


We have seen that to add Quick links web part or any other modern SharePoint web part we need to keep an eye on json format which changes frequently and try to update our classes based on those. Also to update a web part you need to set PropertiesJson and that will update the web part on the page.

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 2

No votes so far! Be the first to rate this post.

Leave a Reply

Your email address will not be published. Required fields are marked *