Making Content Layouts Robust

Oracle Content and Experience (OCE) allows you to develop custom Content Layouts and use them to render content items (see here for details). Content layouts receive the content item data and render it into HTML that will be inserted onto the site page. By default, content layouts leverage Mustache templating to render content items – although they can be implemented in any JS technology. In order for the Mustache template to render, it expects the data to be in a certain format and it is up to the content layout render.js file to ensure that the model it passes to the template matches that format.

There are several use-cases where Content Layouts are rendered:

  • When used in OCE Assets web UI, the data may be in an “edited” state to preview changes before saving the changes
  • When used in an OCE Site either in a Content List or Content Item component, the data is augmented with additional information about the site it is running in
  • When used via the OCE Content SDK’s contentClient.renderLayout() call, where the user of the ContentSDK passes whatever data they wish directly to the Content Layout

For performance, there is a general trade-off between creating a single query that can return all required data and using multiple queries so that the outline renders as fast as possible with a fast initial query and areas are subsequently filled in via subsequent queries.  Which model you choose will depend on your data and use-cases.

Problem

Content Layouts that you develop need to be robust to the three types of response data that you can get from the OCE Content API REST calls:

  • Content Items: with expand=all will have both references expanded and large text fields
  • Content Items: with no expand=all will not have references expanded but will have large text fields
  • Content Queries: will not have references expanded or large text fields

Solution

As the developer of the Content Layout, you need to standardize the structure of the data it receives.  Where all the data is present, it can simply render the HTML.  Where the data isn’t all present, it may need to make additional queries.  However, in all cases, it 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 fields that will potentially be missing from the data are:

  • The “fields” entry for Referenced Fields
  • Large Text Fields

Since Content Layouts are designed for specific content types, as the writer of the Content Layout, you know the list of fields that they need.  For each of these, you need to fetch the data so the Content Layout can render.

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.

Example

Assume that 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 may be called with data from the following queries:

  • Item query w/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 are 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"
}
}
  • Search 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 these aren’t the case, it needs to queries 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 may 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.

To achieve this behavior you need to set 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.

In summary, developing content layouts that uses two Mustache templates to update HTML rendered on the site page as data being retrieved is complex and is out of scope for this blog.

Featured image photo by Kelly Sikkema on Unsplash

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s