Using Content & Experience Cloud content in a React app

March 30, 2018 | 11 minute read
Dolf Dijkstra
Cloud Solutions Architect
Text Size 100%:

In this blog I will introduce an integration pattern to download content from Content & Experience Cloud and use that 'off-line'. By off-line I mean that the content is downloaded before the application is built and is used statically as part of the application instead of dynamically calling the Content & Experience Cloud REST API.

This blog discusses a non-public API. The API might change without warning. A Content as a Service API might be published later as part of the Content & Experience Cloud.

Decoupled Content Pattern

The pattern is useful when you want to use content that is managed by a content team and you don't want your application to make dynamic queries to Content & Experience Cloud at runtime. You may have a requirement to serve your application from a web server, without any other runtime dependancies.

The design trade-off you make is that for the lowest possible number  of dependant runtime components in your architecture you lose on capabilities to serve the latest content. To get access to the latest content updates you will have to download the content changes and build and deploy your application.

Another design consideration is that this pattern only works for small amounts of content, or the experience will suffer. In the (simple) example that I describe in this blog, the browser is downloading all the content when the application is launched. If you have lots of content or maybe a slow network, the user experience is far from pleasant.

There are of course variants where you just have to deploy the content changes to the web server without needing to build the application again. Or where the initial content download by the visitor is programmed to be more selective, leading to better initial load experience.

The example application

The application that I will walk you through is a simple Single Page App (SPA) built in React. The SPA displays a number of Blog posts on the home page. The content is managed by Content & Experience Cloud and is published by the editorial team. Each time when the team posts a  new Blog or updates an existing one, the React application needs to be built. The content team might notify the development team that they have published content or the development team polls the REST API periodically for updates. The polling mechanism is not in this example. We assume that the editorial team notifies the developers to kick off  a build via email or chat.

The process to come to a live application is

  • Create the application with the React tools
  • Develop the application by creating the React Component and modules
  • Develop the NodeJS module to download the content from Content & Experience Cloud
  • Build the application with the React tools
  • Deploy the build files to a web server.

Create the React Application

The short version is:

   $ create-react-app my-react-content-app
 

The slightly longer version: follow the guide at https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md till the instruction to run create-react-app.

After that command, follow the screen instruction:

$ cd my-react-content-app     
$ yarn start

By now you have  a running test server for the out of the box sample application. Your browser should open and show the app in the browser.

Develop the application

To further develop the application you'll have the change the source files. Open your favourite source editor and create or update the following files

src/index.js

import React from 'react' 
import ReactDOM from 'react-dom' 
import App from './App.jsx' 
import registerServiceWorker from './registerServiceWorker' 
ReactDOM.render(<App />, 
document.getElementById('root')) 
registerServiceWorker()

src/App.jsx

import React, { Component } from 'react' 
import Layout from './Layout' 
export default class App extends Component {   
  render () {     
   return (       
     <React.Fragment>         
     <Layout />       
     </React.Fragment>     
   )   
  } 
}

src/Layout.jsx

import React, { Component } from 'react'
import content from './content.json'
const allItems = content.ALL.data.items
const items = allItems.filter(item => item.type === 'Blog')
const fullItem = item => content[item.id].data

const toHref = link =>
  link.href.replace('/items/', '/digital-assets/') + '/default'

export default class Layout extends Component {
  render () {
    return (
      <div>
        {' '}
        <h1>Hello Content</h1> {items.length == 0 ? <NoItems /> : <List />}{' '}
      </div>
    )
  }
}

const NoItems = () => <div>No items to display</div>

const List = () => (
  <div>
    {' '}
    {items.map(fullItem).map((item, index) => (
      <Blog key={index} item={item} />
    ))}{' '}
  </div>
)

const Blog = ({ item }) => {
  const { data } = item
  const {
    blog_category,
    blog_textposition,
    blog_textcolor,
    blog_content,
    blog_image_ad,
    blog_image_thumbnail,
    blog_image_header,
    blog_image_ad_small,
    blog_author
  } = data
  const content = { __html: blog_content }
  const image = toHref(blog_image_thumbnail.link)
  return (
    <div>
      {' '}
      <div>
        {' '}
        <strong>{item.name}</strong>: {blog_category}{' '}
      </div>{' '}
      <div>
        {' '}
        <img src={image} />{' '}
      </div>{' '}
      <div dangerouslySetInnerHTML={content} />{' '}
    </div>
  )
}

src/content.json

{
  "ALL": {
    "data": {
      "items": []
    }
  }
}

After you have copied and saved all the files, you should see that the errors that occurred during the creation and modification of the source files in the browser window have cleared and you see a page with

Screen Shot No Items

Not an exciting page, as it does not show a lot. Interestingly, you have laid down all the ground work for the display of the Blog posts. I'll explain.

The React application is loaded from public/index.html. The development server is injecting src/index.js into index.html. You have slightly changed index.js and removed the reference to the index.css file. React has the capability to load CSS files per Component. We don't need to scaffolded CSS file, so we exclude it.

Then, index.js is loading App.jsx. This is the main entry point of the application. App.jsx does load Layout.jsx. In later phases of your application development you could create more complex Layouts, for the sample we use a very simple one.

In Layout.jsx we do all the heavy lifting. The content is loaded from content.json:

    import content from './content.json' 

If you look at the source from content.json you'll see that it is almost empty, items is an empty array. In the next phase we will use a node module to download the content from Content & Experience Cloud and update content.json.

In the Layout class, the rendering is done; a div is created, an <h1> is created with Hello Content and the <NoItems> or <List> component is used based on the number of Blog content items in content.json.

If there would have been Blog content items in content.json, the List Component would have rendered and each Blog item would have been rendered with the Blog Component.

Since we have no Blog items, the NoList Component is shown in your browser window. Let's download some content.

Downloading Content

Content & Experience Cloud comes with a REST Delivery API that developer can use to access published content. I have blogged on this before. I will show here how you can use this REST API to update content.json. Again, open you favourite editor and create

src/fetchContentFromCEC.js

const fetch = require('node-fetch')
const path = require('path')
const fs = require('fs')
const token = '11ad271cc1ae61bd03249332e8445c96'
const host = 'https://demo-gse00009991.sites.us2.oraclecloud.com'
const itemsURL = ({ maxResults, sortOrder }) =>
  `${host}/content/published/api/v1/items?contentType=published&orderBy=${esc(
    sortOrder
  )}&limit=${maxResults}&access-token=${token}`
const write = data => {
  fs.writeFileSync(
    path.join(__dirname, '/content.json'),
    JSON.stringify(data, null, 2)
  )
  return data
}
const esc = encodeURIComponent
const noDigitalAssets = e => e.type !== 'DigitalAsset'
const fetchItem = link => {
  return fetch(`${link.href}?access-token=${token}`)
    .then(r => r.json())
    .then(data => ({ [link.id]: { data: data } }))
}
const fetchItems = data => {
  const links = data.ALL.data.items
    .filter(noDigitalAssets)
    .map(e => ({ id: e.id, href: e.link.href, rel: e.link.rel }))
    .sort((a, b) => a.href.localeCompare(b.href))
    .filter((e, i, a) => a.indexOf(e) === i)
  const itemFetches = links.map(fetchItem)
  return Promise.all(itemFetches)
    .then(a => a.reduce((a, e) => Object.assign(a, e), {}))
    .then(r => Object.assign(data, r))
}
const fetches = fetch(
  itemsURL({ maxResults: 500, sortOrder: 'updateddate:desc' })
)
  .then(response => response.json())
  .then(data => ({ ALL: { data: data } }))
fetches.then(fetchItems).then(write)

The module first get's a list of all the content items that are published to this publish target, and then downloads all the content items, excluding the DigitalAssets. We don't need to download the DigitalAssets for our sample, as our application  loads the images of Content & Experience Cloud at runtime. If you want the images included in your build package you'll have the extend the code and download the binary files to and sure them in the public folder.

The fetchContentFromCEC.js node module makes use of node-fetch as the HTTP client library, so we have to add that you our package.json by executing:

    $ yarn add node-fetch --dev 

 

You can now download the content from a server that is also used for http://www.mycontentdemo.com.

    $ node src/fetchContentFromCEC.js 

After this code has completed, your browser should reload and show some Blog posts.

Screen Shot With Content

A couple of words about what just happend. The fetchContentFromCEC module downloaded the content and put this in content.json in a certain structure. They are in the ALL field with all the content items, and then, per content item a field with the item's ID and as the value the details of the item. In Layouts.jsx this object is used to filter and retrieve the Blog content items. The methods allItems, items and fullItem are used for content item access and filtering.

Because the items object now hold some content items the List component is rendered. This component iterates over the individual Blog items and maps them to a Blog Component and passes in the content items detail data. This data is then used by the Blog Component to render the data as HTML. This is by itself straight forward. There is some manipulation of the link.href field to create the proper img src attribute.

There is one slight complexity. As React is making sure that the HTML it renders is Cross-Site-Scripting safe, it escapes all the data you feed it to render. As the Blog content items content raw HTML in the field blog_content. you have to instruct React that this field is safe to render. For this you have to use the construct with dangerouslySetInnerHTML. You can read more on this on the React website. If you would not use this method, the page would show as HTML source for the blog_content fields.

You may want to change the Layout.jsx Component and include a CSS file to make the display of the Blog posts a bit user friendlier. I leave this up to your imagination how that would look like.

Assuming that you are happy with the result, the next step is the build the application for production use.

Building the React Application

Building the React Application for production use is very simple

    yarn build 

This creates an optimised production build with minimised files for optimal user experience. You can follow the on screen instructions if you want to preview the application.

The production files are stored in the build directory. If you inspect the file build/static/js/main.<hex>.js you'll see that it is minimalized and has all the JavaScript code and the content.json file included. From that you can understand that the content from Content & Experience Cloud is not included in the static file for your application. As such the runtime dependency is removed. The build dependency on Content & Experience Cloud does exist, at least if you download the content as part of your build process.

Deploying

The last step of the process is to deploy the build directory on a production web farm. I will not explain in detail on how to do this, but you can use tools like rsync or ftp to push the files, or use Docker to build an Docker image with the build folder and Nginx.  You can than deploy this image to a Kubernetes Cluster on Oracle Container Services.

Further enhancements

The sample application in this blog post is very simple. The intent is to give you with a couple of simple files and steps an idea on how you can include Content & Experience Cloud content into a React Application. If you want to do this in a real world application it is likely that you need to show content items in various parts of the application. For this you will need to either query and filter content.json differently or structure content.json differently. In the sample application is the entry point to the content the ALL field. If you would have other lists to display in other parts of the application, you could for instance, create a local mapping file with query names and queries that are executed at build time to Content & Experience Cloud. Those query name would then be stored in content.json with their content item data. For instance a query with the name MOST_RECENT_5_PROMOS could query Content & Experience Cloud for the most recent 5 published Promo content items. A component could just reference that name and then instead of starting at ALL that component would start at MOST_RECENT_5_PROMOS:

    const allItems = content.MOST_RECENT_5_PROMOS.data.items 

You can use this pattern each time you want to retrieve content from Content & Experience at build time and use that in your applications. This does not have to be  a React app, but can also be a Mobile app, a Chatbot, a Oracle Jet Application etc etc. The key point is that you remove a runtime dependency and based on your application's needs that might be a bad or a good thing.

 

 

Dolf Dijkstra

Cloud Solutions Architect


Previous Post

Intro to Graphs at Oracle

Michael J. Sullivan | 9 min read

Next Post


Continuous Integration with Apiary, Dredd, and Wercker

Nick Montoya | 7 min read