Standardize the Structure of Data for a Content Layout

The content layout developer needs to standardize the structure of data that the content layout receives.

If all the data is present, content layout can simply render the component. If the data isn't all present, the content layout might need to make additional queries. In all cases, the content layout should never assume a certain data format and instead coerce the data into a format that will render.

You will need to ensure that you have all the data you're expecting. If the data doesn't exist, you'll need to make the additional queries. The following fields will potentially be missing from the data:

  • The "fields" entry for referenced fields

  • Large text fields

Because content layouts are designed for specific content types, the developer of a content layout knows the list of fields needed. For each of these fields, the data needs to be fetched so the content layout can render. You have two options: fetch missing data and then render with complete data, or render immediately and then fetch missing data to fill in the blanks.

Option 1: Fetch Missing Data and Then Render with Complete Data

Create a Promise to retrieve the required data and then continue rendering when all Promises return.

For example, we have the following content types with corresponding fields:

  • starter-blog-author

    • fields

      • starter-blog-author_name -text field

      • starter-blog-author_bio - text field

  • starter-blog-post

    • fields

      • starter-blog-post_title - text field

      • starter-blog-post_content - large text field

      • starter-blog-post_author - reference to a starter-blog-author item

The Content Layout has the following template, to render these expected field values:

{{#fields}}
<div class="blog_container">
    <div class="blog-post-title">{{starter-blog-post_title}}</div>
    {{#starter-blog-post_author.fields}}
    <div class="blog-author-container">
        <div class="blog-author-details">
            <div class="blog-author-name">{{starter-blog-author_name}}</div>
            <div class="blog-author-bio">{{{starter-blog-author_bio}}}</div>
            <span class="more-from-author">More articles from this author</span>
        </div>
    </div>
    {{/starter-blog-post_author.fields}}
    <div class="blog-post-content">{{{starter-blog-post_content}}}</div>
</div>
{{/fields}}

The Content Layout can be called with data from the following queries:

  • Item query with "expand" - all data supplied

    • /content/published/api/v1.1/items/{id}?expand=fields.starter-blog-post_author&channelToken=8dd714be0096ffaf0f7eb08f4ce5630f

    • This is the format of the data that is required to successfully populate all the values in the template. If either of the other queries is used, additional work is required to fetch the data and convert it into this format.

    • "fields": {    
          "starter-blog-post_title": "...",
          "starter-blog-post_summary": "...",
          "starter-blog-post_content": "...",
          "starter-blog-post_author": {
              "id": "CORE386C8733274240D0AB477C62271C2A02",
              "type": "Starter-Blog-Author"
              "fields": {
                  "starter-blog-author_bio": "...",
                  "starter-blog-author_name": "..."
              }
          }
      }
  • Item query, without "expand" - missing referenced item fields "starter-blog-post_author.fields":

    • /content/published/api/v1.1/items/{id}?channelToken=8dd714be0096ffaf0f7eb08f4ce5630f
    • "fields": {    
          "starter-blog-post_title": "...",
          "starter-blog-post_summary": "...",
          "starter-blog-post_content": "...",
          "starter-blog-post_author": {
              "id": "CORE386C8733274240D0AB477C62271C2A02",
              "type": "Starter-Blog-Author"
          }
      }
  • SCIM query - missing large text field "starter-blog-post_content", missing referenced item fields "starter-blog-post_author.fields":

    • /content/published/api/v1.1/items?q=(type eq "Starter-Blog-Post")&fields=ALL&channelToken=8dd714be0096ffaf0f7eb08f4ce5630f

    • "fields": {
          "starter-blog-post_title": "...",
          "starter-blog-post_summary": "...",
          "starter-blog-post_author": {
              "id": "CORE386C8733274240D0AB477C62271C2A02",
              "type": "Starter-Blog-Author"
          }
      }

To be able to consistently render with any of these queries, the render.js from the content layout needs to make sure all the referenced fields are expanded and that the large text fields are present.

If this is not the case,it needs to query these back, fix up the data, and then render with the complete data.

Sample render() function:

render: function (parentObj) {
    var self = this,
        template,
        contentClient = self.contentClient,
        content = self.contentItemData;

     var getRefItems = function (contentClient, ids) {
        // Calling getItems() with no "ids" returns all items.
        // If no items are requested, just return a resolved Promise.
        if (ids.length === 0) {
            return Promise.resolve({});
        } else {
            return contentClient.getItems({
                "ids": ids
            }); 
        }
     };
   
     var fetchIDs = [], // list of items to fetch
         referedFields = ['starter-blog-post_author'], // names of reference fields
         largeTextFields = ['starter-blog-post_content'], // large text fields in this asset
         fieldsData = content.fields;
     // See if we need to fetch any referenced fields
     referedFields.forEach(function (fieldName) {
         if(fieldsData[fieldName] && fieldsData[fieldName].fields) {
            // got data already, nothing else to do
         } else { 
             // fetch this item
             fetchIDs.push(fieldsData[fieldName].id);
         }
     });

     // See if we need to fetch any large text fields
     for(var i = 0; i < largeTextFields.length; i++) {
        if(!fieldsData[largeTextFields[i]]) {
           // need to fetch this content item directly to get all the large text fields
            fetchIDs.push(content.id);
            break;
        }
     }
    // now we have the IDs of all the content items we need to fetch, get them all before continuing
    getRefItems(contentClient, fetchIDs).then(function (referenceData) {
        var items = referenceData && referenceData.items || [];

        // add the data back in
        items.forEach(function (referencedItem){
            // check if it's the current item
            if(referencedItem.id === content.id) {
               // copy across the large text fields 
               largeTextFields.forEach(function (fieldName) {
                   fieldsData[fieldName] = referencedItem.fields[fieldName];
                });
            } else{
                // check for any referenced fields
                for (var i = 0; i < referedFields.length; i++) {
                    if(referencedItem.id === fieldsData[referedFields[i]].id){
                       // copy across the fields values
                       fieldsData[referedFields[i]].fields = referencedItem.fields;
                       break;
                    }
                }
            }
        });

        // now data is fixed up, we can continue as before
        try{
           // Mustache
           template = Mustache.render(templateHtml, content);

             if(template) {
                $(parentObj).append(template);
             }

        } catch (e) {            
            console.error(e.stack);
        }    
    });
}

Option 2: Render Immediately and Then Fetch Missing Data to Fill in the Blanks

Performance can be improved by separating out the items that might not be present and rendering them in a second pass. This will require two Mustache templates, the first to do the initial render, leaving "holes" that are then filled in with the second render once the data is complete.

This requires setting up the Mustache template to support multiple passes either by having separate templates for the "holes" or having the model return template macros rather than actual values. In either case, you'll need to "hide" these holes until the data is retrieved and then populate them and show them with appropriate UI animation to avoid the page "jumping about" too much.