Using Scrivito as a Content Provider

If you have a growing web application and are on the lookout for better tools to maintain structured content, why not use Scrivito for this? Equipped with a RESTful API, Scrivito can act as a headless CMS towards your app, and at the same time offer you a UI for maintaining all kinds of content.

In this tutorial, we are going to illustrate how Scrivito-managed content can be accessed from within a Gatsby app using Scrivito’s RESTful API. Gatsby is a React-based web application framework with a close attention to CMSs able to act as content providers. Though, almost needless to say, any app built with any web application framework can consume Scrivito-based content. You will require a Scrivito CMS as well as a Scrivito app, e.g. our Example app, to follow along.

First, let’s spend a moment on the content creation and maintenance procedures.

Providing a data input method

Usually, when using a Scrivito app to deliver a website, most content is created directly on the pages. For this, one would use widgets to be able to edit and arrange the various page components as desired. In a context in which content is seen and treated as data, however, the demand to have it nicely displayed doesn’t exist. All that is needed are simple forms for acquiring and modifying the data.

This is where Scrivito’s flexibility and customizability comes in: it lets you create data objects according to your structure requirements and provide a suitable editing configuration for them to enable editors to work with the data.

To illustrate this and prepare the data to be displayed by our Gatsby app later on, let’s begin with defining a simple Product class like we did in our Displaying a Product List from a Search article. Create a folder named Product in the “src/Objs” directory of your Scrivito app and place the following file in it:

src/Objs/Product/ProductObjClass.js (Scrivito)
Copy
import * as Scrivito from 'scrivito';

Scrivito.provideObjClass('Product', {
  attributes: {
    title: 'string',
    description: 'html',
    image: 'reference',
    link: 'link',
    code: 'string',
  },
});

To make instances of this class editable, add this editing configuration to the Product subdirectory:

src/Objs/Product/ProductEditingConfig.js (Scrivito)
Copy
import * as Scrivito from 'scrivito';

Scrivito.provideEditingConfig('Product', {
  title: 'Product',
  description: 'A product.',

  attributes: {
    title: {
      title: 'Title',
    },
    description: {
      title: 'Description',
    },
    image: {
      title: 'Image',
    },
    link: {
      title: 'Link to details',
    },
    code: {
      title: 'Reference code',
      description: 'Format: ABC-1234',
    },
  },

  properties: ['title', 'description', 'image', 'link', 'code'],
  titleForContent: obj => obj.get('title'),
  descriptionForContent: obj => obj.get('code'),
  hideInSelectionDialogs: true,
});

Adding a Product filter to the Content Browser

For finding and creating Product instances via the Content Browser, let’s extend its configuration, which can be found in “src/config/scrivitoContentBrowser.js”. There are two places where we need to add the Product class: the defaultFilters function and the FILTER_PRESENTATIONS constant.

src/config/scrivitoContentBrowser.js (Scrivito)
Copy
function defaultFilters() {
  return {
    _objClass: {
      options: {
        All: {
          title: "All",
          icon: "folder",
          query: Scrivito.Obj.all(),
          selected: true,
        },
        Product: filterOptionsForObjClass("Product"),
        Image: filterOptionsForObjClass("Image"),
⋮
}

const FILTER_PRESENTATIONS = {
// …
  SearchResults: { title: "Search results", icon: "lens" },
  Video: { title: "Videos", icon: "video" },
  Product: { title: "Products", icon: "suitcase" },
};
⋮

Afterwards, you can select the Product filter in the Content Browser and add an item by clicking the plus icon. If a Product item is selected, its content can be changed using the properties view to the right.

Connect Gatsby!

Now that we have a means to create and maintain Product items, we’d like to access them from within a React app. If you don’t have such an app already, setting up Gatsby and a small app for experimenting is almost as easy as setting up the Scrivito Example App.

Next, let’s imagine what having a product list rendered could look like in our Gatsby app:

src/pages/index.js (Gatsby)
Copy
import React from "react";
import ScrivitoProductList from "../components/ScrivitoProductList";

export default () => {
  return (
    <div style={{ color: 'purple' }}>
      <h1>Hello Gatsby! Hello Scrivito!</h1>
      <ScrivitoProductList />
    </div>
  )
}

Let’s dive in and develop our ScrivitoProductList component!

Providing the product list component

As mentioned above, Scrivito’s REST API lets us perform searches. Since our component requires a list of products to render, the first thing we need to develop is the interface for fetching this list. We will name the function that does this fetchScrivitoObjects and give it one parameter, the name of the Scrivito object class the objects to search for must have.

Note that, to keep it simple, we’ve put the tenantId of the Scrivito CMS we are going to talk with as a constant into the JavaScript file instead of providing it via some config or the environment.

src/components/fetchScrivitoObjectData.js (Gatsby)
Copy
const tenantId = "a1b2c3d4e5f67890a2b3c4d5e6f7890a"

export async function fetchScrivitoObjects(objClassName) {
  const searchRequest = {
    method: "POST",
    path: "workspaces/published/objs/search",
    params: {
      query: [
        {
          field: "_obj_class",
          operator: "equals",
          value: objClassName,
        },
      ],
    },
  }

  const objIds = (await scrivitoFetch(searchRequest)).results.map(
    result => result.id
  )
  return Promise.all(
    objIds.map(
      async objId =>
        await scrivitoFetch({
          method: "GET",
          path: `workspaces/published/objs/${objId}`,
        })
    )
  )
}

async function scrivitoFetch({ method, path, params }) {
  const data = {
    method,
    headers: {
      "Content-Type": "application/json",
    },
  }
  if (params && (method === "POST" || method === "PUT")) {
    data.body = JSON.stringify(params)
  }

  const response = await fetch(
    `https://api.scrivito.com/tenants/${tenantId}/${path}`,
    data
  )
  return await response.json()
}

Both fetchScrivitoObjects and the function for actually performing the requests, scrivitoFetch, are asynchronous. In our ScrivitoProductList component we are going to assign the result returned by fetchScrivitoObjects to a state variable to cause the component to update after the search results have become available.

fetchScrivitoObjects first issues the search request and extracts the object IDs from the results array included in the returned JSON data. Then it iterates the object IDs to retrieve and return the respective object data.

For now, this is all we require to build the ScrivitoProductList component. Here we go:

src/components/ScrivitoProductList.js (Gatsby)
Copy
import * as React from "react";
import { fetchScrivitoObjects } from "./fetchScrivitoObjectData";

class ScrivitoProductList extends React.Component {
  constructor(props) {
    super(props);
    this.state = { products: [] };
  }

  async componentDidMount() {
    this.setState({
      products: await fetchScrivitoObjects("Product")
    });
  }

  render() {
    return this.state.products.map(product => {
      return (
        <div key={product._id}>
          <h3>{product.title[1]}</h3>
          <p>
            {product.description[1]}
            <br />
            <small>
              <b>
                <a href={product.link[1].url}>Learn more</a>
              </b>
              <span> [{product.code[1]}]</span>
            </small>
          </p>
        </div>
      );
    });
  }
}

export default ScrivitoProductList;

After the component has mounted, we are fetching the products from the Scrivito CMS using fetchScrivitoObjects("Product"). The render method then iterates the product items to output their respective attribute values. You might notice that custom attributes such as title are obviously represented as arrays. The first element of such arrays is the attribute’s type, the second its value.

Are we done? Sure – unless we want the product images to show up. :) But you can already test this in your browser.

Fetching and rendering images

Scrivito’s REST API supports fetching the data associated with binaries, e.g. PDF or image data. As our products are equipped with images, let’s render them!

In the Product object class, a product’s image is referenced by an attribute named image. The value of this attribute is the ID of the image object to use.

Analogously to the product list, we’d like to be able to have our images rendered via a component, like so:

Copy
<ScrivitoImage objId={product.image[1]} />

To get at the image data the component can work with, we’ll once again provide a function, fetchScrivitoImageBlob, and add it to the “fetchScrivitoObjectData.js” file we’ve introduced above:

src/components/fetchScrivitoObjectData.js (Gatsby)
Copy
⋮
export async function fetchScrivitoImageBlob(objId) {
  const blobId = (await scrivitoFetch({
    method: 'GET',
    path: `workspaces/published/objs/${objId}`,
  })).blob[1].id;
  return (await scrivitoFetch({
    method: 'GET',
    path: `blobs/${encodeURIComponent(blobId)}/optimize`,
  }));
}

fetchScrivitoImageBlob first fetches the CMS object with the passed-in objID. This object's blob attribute contains the id of the actual blob which is then fetched and returned to be able to obtain the URL of the binary data. This is exactly what we require to render the <img> tag in our ScrivitoImage component:

src/components/ScrivitoImage.js (Gatsby)
Copy
import * as React from "react";
import {fetchScrivitoImageBlob} from "./fetchScrivitoObjectData";

class ScrivitoImage extends React.Component {
  constructor(props) {
    super(props);
    this.state = { url: null };
  }

  async componentDidMount() {
    const imageBlob = await fetchScrivitoImageBlob(this.props.objId);
    this.setState({url: imageBlob.public_access.get.url});
  }

  render() {
    if (!this.state.url) return null;
    return (
      <img
        src={this.state.url}
        alt=""
        style={this.props.style || ""}
      />
    );
  }
};

export default ScrivitoImage;

Now that we have the ScrivitoImage component, we can use it in our ScrivitoProductList component to have the product images displayed:

src/components/ScrivitoProductList.js (Gatsby)
Copy
import * as React from "react";
import { fetchScrivitoObjects } from "./fetchScrivitoObjectData";
import ScrivitoImage from "./ScrivitoImage";

class ScrivitoProductList extends React.Component {
  constructor(props) {
    super(props);
    this.state = { products: [] };
  }

  async componentDidMount() {
    this.setState({
      products: await fetchScrivitoObjects("Product")
    });
  }

  render() {
    return this.state.products.map(product => {
      return (
        <div key={product._id} style={{ clear: "both" }}>
          <ScrivitoImage
            objId={product.image[1]}
            style={{
              width: "150px",
              margin: "5px 15px 15px 0px",
              float: "left"
            }}
          />
          <h3>{product.title[1]}</h3>
          <p>
            {product.description[1]}
            <br />
            <small>
              <b>
                <a href={product.link[1].url}>Learn more</a>
              </b>
              <span> [{product.code[1]}]</span>
            </small>
          </p>
        </div>
      );
    });
  }
}

export default ScrivitoProductList;

That was it! You now have a React component, ScrivitoProductList, that renders a list of data objects in the way you to want to present them in your Gatsby app. Additionally, using the ScrivitoImage component, you can even have images displayed!

What’s next?

The above approach was meant to give you an idea of how the content maintained in a Scrivito CMS can be accessed using its REST API. However, our code merely covers the basics. Next to adapting the code so that it fits your needs, the following aspects might be worth thinking about:

  • Missing attribute values, e.g. images, should be handled properly using fallbacks.
  • By default, the maximum number of hits returned by searches is 10. For fetching further batches, the continuation value included in the search result is available. For details, see the REST API documentation.
  • If you want the Product objects in the Scrivito CMS to be displayable as pages using a Scrivito-based app, you can provide a page component for them.

Happy coding!