How to Create a Web API that Returns a Non-HTML Content Type
PageBuilder Engine supports non-HTML output types like JSON data and XML. This can be utilized to build web APIs for other usages. Let’s build an API for your mobile applications to get a list of recommended sections by editor’s pick and their most recent stories.
Goals
-
The mobile API should return a list of sections based on the configuration in PageBuilder Admin.
-
Each configured section should contain the section’s ID, name, and x number of most recent stories (x is configurable).
-
There should be a visual preview for configuring the mobile API in PageBuilder Admin.
Design
JSON output type
Building web APIs using JSON output type is similar to building regular web pages. You need to create an output type and features/chains/layouts. The main difference is you don’t create them as React components. All the files must use .js
file extension, and each rendering method returns a plain JavaScript object. Since usually the payload structure of a web API is either a single object or an array of objects, we can use a single feature to generate the object type payload or a single chain with multiple features to generate the array type payload. This way we can simply create a generic JSON output type and a layout which directly return the first child of the children from the props. Using layout is required due to a known issue in PageBuilder Engine to receive consistent data structure for the children in JSON output.
This is an entity-relationship diagram for object type payload:

This is an entity-relationship diagram for array type payload:

For this example, we will use this design approach; however, keep in mind this is not the only way you can use. The real design should reflect to your use cases.
XML output type
The above design approach can be applied to none-JSON content types as well. We will build an XML output type as an illustration. The key to achieve this is the output type must return a string as the final result.
Creating a generic JSON output type
This generic JSON output type returns the first child in the props.children
which is the same data returned from a layout.
const json = ({ children }) => { // Only return the data from the first child. return Array.isArray(children) ? children[0] : null}
// Specify content typejson.contentType = 'application/json'
export default json
Creating a generic API layout
This generic JSON layout returns the first child in the props.children
which is the same data returned from the first chain or feature. Since we will create visual preview later, we are using output type naming convention api/json.js
for different output types.
const Api = ({ children }) => { // Only return the data from the first child (body) return Array.isArray(children) ? children[0] : null}
Api.sections = [ 'body']
export default Api
Creating a chain to return array type data
We will create visual preview later, so we are using output type naming convention ApiList/json.js
for different output types.
const ApiList = ({ children }) => children
export default ApiList
Creating a content source to fetch section data
const resolve = ({ website, id}) => `/site/v3/website/${website}/section?_id=${id}`
export default { resolve, params: [ { name: 'website', displayName: 'Website', type: 'text' }, { name: 'id', displayName: 'Section ID', type: 'text' } ]}
Creating a content source to fetch stories by section
const resolve = ({ website, section, size = 1, offset = 0}) => { const body = encodeURIComponent(JSON.stringify({ query: { bool: { must: [ { term: { type: 'story' } }, { nested: { path: 'taxonomy.sections', query: { bool: { must: { term: { 'taxonomy.sections._id' : section } } } } } } ] } } })) return `/content/v4/search/published/?website=${website}&body=${body}&size=${size}&from=${offset}&sort=publish_date:desc`}
export default { resolve, params: [ { name: 'website', displayName: 'Website', type: 'text' }, { name: 'section', displayName: 'Section ID', type: 'text' }, { name: 'size', displayName: 'Site', type: 'number' }, { name: 'offset', displayName: 'Offset', type: 'number' } ]}
Creating a feature to combine section data and stories
In this feature, we need to fetch both section and stories data using the content sources we created above and return the desired data structure. Since we will create visual preview later, we are using output type naming convention Section/json.js
for different output types.
import PropTypes from 'prop-types'import Consumer from 'fusion:consumer'
const WEBSITE_ID = 'your_website_id'
@Consumerclass Section { constructor (props) { this.props = props const { customFields: { section, size = 3 } = {} } = props
this.fetchContent({ section: { source: 'section', query: { website: WEBSITE_ID, id: section } }, result: { source: 'stories-by-section', query: { website: WEBSITE_ID, section, size } } }) }
render () { const { section, result } = this.state || {}
if (!section || !result) { return null }
return { id: section._id, name: section.name, stories: result.content_elements } }}
Section.propTypes = { customFields: PropTypes.shape({ section: PropTypes.string.tag({ label: 'Section ID' }).isRequired, size: PropTypes.number.tag({ label: '# of Stories', defaultValue: 3 }) })}
export default Section
Creating corresponding components for default output for visual preview
With all the components created above, you should be able to build the mobile API in PageBuilder Admin as a page to serve recommended sections and their stories using outputType=json
. However the editing process might not be convenient as it’s just a bunch of JSON data if using JSON output for preview. Here we will create the default output type versions for the API layout, chain, and feature. For the default output type, see How to Create and Use Output Types if you haven’t created one.
import React from 'react'import PropTypes from 'prop-types'
const Api = ({ children }) => { return ( <> {children} </> )}
Api.sections = [ 'body']
Api.propTypes = { children: PropTypes.array}
export default Api
import React from 'react'import PropTypes from 'prop-types'
const ApiList = ({ children }) => <div>{children}</div>
ApiList.propTypes = { children: PropTypes.any}
export default ApiList
import React, { Component } from 'react'import PropTypes from 'prop-types'import Consumer from 'fusion:consumer'
const WEBSITE_ID = 'your_website_id'
@Consumerclass Section extends Component { constructor (props) { super(props) const { customFields: { section, size = 3 } = {} } = props
this.fetchContent({ section: { source: 'section', query: { website: WEBSITE_ID, id: section } }, result: { source: 'stories-by-section', query: { website: WEBSITE_ID, section, size } } }) }
render () { const { section, result } = this.state || {}
if (!section || !result) { return null }
const stories = result.content_elements || []
return ( <div> <h3>Section {section.name} ({section._id})</h3> <ol> { stories.map((story) => ( <li key={story._id}>{story.headlines.basic}</li> )) } </ol> </div> ) }}
Section.propTypes = { customFields: PropTypes.shape({ section: PropTypes.string.tag({ label: 'Section ID' }).isRequired, size: PropTypes.number.tag({ label: '# of Stories', defaultValue: 3 }) })}
export default Section
Now check the page you created earlier in PageBuilder Admin, you should be able to see the visual presentation for each section and click through for modification.
Creating corresponding components for XML output type
Since we need to return a string for the XML output type, we will use the Xmlbuilder Npm Module to build the DOM tree from a plain JavaScript object and convert it to an XML string. To simply the object conversion, the feature, chain, and layout should return objects using the Structure. You can also use other XML builders if you have a different preference.
import xmlbuilder from 'xmlbuilder'
const Xml = ({ children }) => { // Only return the data from the first child. console.log(children[0]) return xmlbuilder.create({ service: children[0] || '' }, { separateArrayItems: true }).end({ pretty: true })}
// XML content typeXml.contentType = 'application/xml'
export default Xml
/components/layouts/api/xml.jsconst Api = ({ children }) => { // Only return the data from the first child (body) return Array.isArray(children) ? children[0] : null}
Api.sections = [ 'body']
export default Api
const ApiList = ({ children }) => { // Remove null results return children.filter((child) => !!child)}
export default ApiList
import PropTypes from 'prop-types'import Consumer from 'fusion:consumer'import get from 'lodash.get'
const WEBSITE_ID = 'your_website_id'
@Consumerclass Section { constructor (props) { this.props = props const { customFields: { section, size = 3 } = {} } = props
this.fetchContent({ section: { source: 'section', query: { website: WEBSITE_ID, id: section } }, result: { source: 'stories-by-section', query: { website: WEBSITE_ID, section, size } } }) }
render () { const { section, result } = this.state || {}
if (!section || !result) { return null }
// For the example, we will only include id, headline, and description for each story return { section: { id: section._id, name: section.name, stories: result.content_elements.map((story) => ({ story: { id: story._id, headline: get(story, 'headlines.basic', ''), description: get(story, 'description.basic', '') } })) } } }}
Section.propTypes = { customFields: PropTypes.shape({ section: PropTypes.string.tag({ label: 'Section ID' }).isRequired, size: PropTypes.number.tag({ label: '# of Stories', defaultValue: 3 }) })}
export default Section
After you create all these components, you can use outputType=xml
to view the XML payload.