Writing and implementing complex custom content sources
This document provides guidance tailored for developers on tackling the complexities of creating complex, custom content sources. Whether new to programming or seasoned in React.js, this resource extends existing documentation to help overcome challenges in integrating multiple API requests.
Pre-requisites
A developer intending to benefit from this content should have gone through Arc XP University developers training, as well as the available documentation on content sources, as follows:
Workflow
Note that the code in this document is provided “as is” and with no warranty. Both code examples are provided for informational purposes only.
The content source
The following code example retrieves a story and full details of each related story. It shows two API requests; however, more code can be added following the same logic.
File location: content > sources > custom-related-content.js
import axios from "axios";import { CONTENT_BASE, ARC_ACCESS_TOKEN } from "fusion:environment";
const params = [ { name: "website", displayName: "Website", type: "text", }, { name: "id", displayName: "Story ID", type: "text", },];
const fetch = (query) => { const website = query.website; const id = query.id; const url1 = `${CONTENT_BASE}/content/v4/?website=${website}&_id=${id}`; const url2 = `${CONTENT_BASE}/content/v4/ids?published=true&website=${website}&ids=`; const headers = { "content-type": "application/json", Authorization: `Bearer ${ARC_ACCESS_TOKEN}`, }
return axios({ url: url1, headers: headers, method: "GET", }) .then((content) => { let ids = []; if (content?.data?.related_content?.basic) { content.data.related_content.basic.forEach((item) => ids.push(item._id)); }
if (ids && ids.length > 0) { return axios({ url: url2 + ids.join(","), headers: headers, method: "GET", }) .then((relatedContent) => { // Data can be transformed as needed prior to return it. return { "story": content.data, "relatedContent": relatedContent.data }; }) .catch((e) => { throw new Error("API fetch error. Error retrieving related stories."); }); } else { return { "story": content.data } } }) .catch((e) => { throw new Error("API fetch error. Error retrieving stories."); });};
export default { fetch, params,};
To reduce the size of the API response and the subsequent impact on bandwidth usage, we recommend restricting the fields retrieved by implementing the included_fields
parameter in both requests. For more information, see:
Content API - Swagger Doc
You can modify this code sample in a variety of ways:
- The examples use only Content API, but you can use any API
- You can chain more API requests if needed
- Instead of nesting the second API request, you can implement multiple async_/_await (with the necessary validations)
- You can modify the data response in any way prior to returning it
Testing the content source
The following procedure provides a way to test the content source code.
- Navigate to Page Builder > Developer Tools > Debugger.
- Select the appropriate site from the top left drop-down field.
- From Select Type, choose Content Debugger.
- Choose custom-related-stories from the list of content sources.
- Enter a story ID on the text field and click Fetch.
If the chosen story doesn’t have related content, an alert informs you, “Could not fetch content”.
If the story has related items, you receive an object with two primary children: story
and relatedContent
, as shown here:
Checking if a story has related content
- Open the story in Composer
- Ensure the story details appear (open the details panel, if necessary).
- Click the Related Items tab. Related stories appear, if they exist.
Implementing the content source
This custom content source sample could be extended and implemented in a variety of ways. The following custom code defines an entire bespoke block. It was created as a proof-of-concept, and it should be used only as guidance, not as a production resource.
File location: components > features > RelatedStories > default.jsx
import React from "react";import PropTypes from 'prop-types';import { useContent } from "fusion:content";
const RelatedStories = (props) => { const { config } = props.customFields;
let response = useContent({ source: config.contentService, query: config.contentConfigValues, }) || '';
// This console log is useful to see how the response looks like. console.log('API response -->', response);
if (!response || !response['story']) { return <div>Oops! Something's wrong!</div> } else { return ( <div> <h1>Related Stories Fetched Via Custom Content Source</h1> <br/> {response['story'] && response['story'].headlines && response['story'].headlines.basic && <div> <h2>THIS IS THE MAIN STORY</h2> <h3>{response['story'].headlines.basic}</h3> <br/> </div> } {response['relatedContent'] && response['relatedContent'].content_elements && <div> <h2>THIS ARE RELATED STORIES</h2> {response['relatedContent'].content_elements.map((item, index) => { return ( <div> {item['headlines'] && item['headlines'].basic && <p>{index}) {item['headlines'].basic} - {item['subheadlines'].basic}</p> } </div> ) })} </div> } </div> ); }};
RelatedStories.label = "[CE Block] Related Stories";RelatedStories.propTypes = { customFields: PropTypes.shape({ config: PropTypes.contentConfig(), })}
export default RelatedStories;
The recommendation is to use out-of-the-box blocks, or if none of them meet the requirements, then fork the closest block and modify it.