Making Content Reusable

Having to maintain a lot of duplicate content can be cumbersome. If, for example, you have a block of content consisting of the same text and download button on a dozen or more landing pages, you don’t want to touch each and every one when the text or the button’s link needs to be changed. Handling multiple instances of the same content in this way is not only error prone but also tiresome.

In this tutorial, we will show you how to create and maintain “global content”, with “global” referring to a Scrivito-based website as a whole. We will create a CMS object class for content of any kind, and a widget class for placing this content anywhere on any page. This combined will allow editors to reuse parts of pages wherever they wish. Changes can be made in place and cause all other instances of the same content to be automatically changed as well. We will be calling such an assembly of reusable content pieces a “content module” or just a “module”.

Sounds great? Let’s begin.

The “ContentModule” object class

The Scrivito Example App, on which this tutorial is based, already includes object types for maintaining content meant to be used anywhere on the website, Author, Event and Job. Instances of them can be rendered either as individual pages – because they have a component associated with them –, but also in overviews where only a subset of their attributes is used.

Our ContentModule instances, in contrast, never constitute pages; their only purpose is to hold content that can be used again and again on various pages. For this reason, all we need for creating instances of them is a corresponding object class and an editing configuration.

The object class

Our object class provides ContentModule instances with two attributes, title and content. The former lets us make a module’s purpose clear to editors when working with it. The latter is a widgetlist for freely putting the module’s content together:

src/Objs/ContentModule/ContentModuleObjClass.js
import * as Scrivito from "scrivito";

const ContentModule = Scrivito.provideObjClass("ContentModule", {
  attributes: {
    title: "string",
    content: [
      "widgetlist",
      { only: ["HeadlineWidget", "TextWidget", "ImageWidget", "ButtonWidget"] },
    ],
  },
});

export default ContentModule;

Notice that the widgetlist we are providing is restricted to a few basic widget types as we don’t want editors to build “monster modules” made up of containers such as columns and boxes, etc. The possibility to add containers to a module would also bear the risk of putting a module into a module of the same type, which could cause them to not work anymore.

The editing configuration

The ContentModule’s editing configuration is the smallest ever. There’s no need to add a title or description to the title and content attributes here as their names are self-explanatory. The only place where the ContentModule’s properties show up is the Content Browser.

src/Objs/ContentModule/ContentModuleEditingConfig.js
import * as Scrivito from "scrivito";

Scrivito.provideEditingConfig("ContentModule", {
  title: "Content Module",
  properties: ["title", "content"],
});

Making content modules known to the Content Browser

Since the Content Browser is the place where Content Modules can be defined or deleted, let’s extend its configuration accordingly. Just add the lines given below to the return value of the defaultFilters function and the FILTER_PRESENTATIONS constant:

src/config/scrivitoContentBrowser.js
function defaultFilters() {
  return {
…
    _objClass: {
      options: {
…
        Download: filterOptionsForObjClass("Download"),
        Video: filterOptionsForObjClass("Video"),
        ContentModule: filterOptionsForObjClass("ContentModule"),
      }
    },
…
}
…
const FILTER_PRESENTATIONS = {
…
  SearchResults: { title: "Search results", icon: "lens" },
  Video: { title: "Videos", icon: "video" },
  ContentModule: { title: "Content modules", icon: "thumbnails" },
};

Using the Content Browser, you could now start creating content modules and even give them a title or add widgets to their content.

However, at this point, you cannot place content modules onto pages and edit them there, so let’s build a widget for that.

The “ContentModuleWidget” class

Our widget’s purpose is to enable editors to place content modules onto pages. For this, we require just a single attribute that lets them specify the module to use. Typically, attributes pointing to objects are of the reference type, so here we go:

src/Widgets/ContentModuleWidget/ContentModuleWidgetClass.js
import * as Scrivito from "scrivito";

const ContentModuleWidget = Scrivito.provideWidgetClass("ContentModuleWidget", {
  attributes: {
    module: ["reference", { only: "ContentModule" }],
  },
});

export default ContentModuleWidget;

The only objects a widget of this class will be able to handle are ContentModules. This restriction comes into effect when the module attribute is edited and the Content Browser then opens to let the user select the attribute’s target object.

The widget’s editing configuration

The editing configuration of our ContentModuleWidget simply makes the module attribute editable on the properties view of widget instances and adds a title and a description to it.

src/Widgets/ContentModuleWidget/ContentModuleWidgetEditingConfig.js
import * as Scrivito from "scrivito";

Scrivito.provideEditingConfig("ContentModuleWidget", {
  title: "Content Module",
  titleForContent: widget =>
    widget.get("module") ? widget.get("module").get("title") : null,
  attributes: {
    module: {
      title: "Content module",
      description: "The kind of module to include",
    },
  },
  properties: ["module"],
});

By the way, the titleForContent key defines the tooltip text displayed for instances of the widget when hovering over their handle.

The widget’s component

The main task of the ContentModuleWidget’s component is to look at its module attribute and, if set, render the content attribute of the ContentModule object it points to.

src/Widgets/ContentModuleWidget/ContentModuleWidgetComponent.js
import * as React from "react";
import * as Scrivito from "scrivito";
import InPlaceEditingPlaceholder from "../../Components/InPlaceEditingPlaceholder";

Scrivito.provideComponent("ContentModuleWidget", ({ widget }) => {
  const module = widget.get("module");
  const editing = Scrivito.isInPlaceEditingActive();
  return (
    <div>
      {editing && module ? (
        <div className="alert-info">
          Changing this module updates all its instances.
        </div>
      ) : null}
      {module ? (
        <Scrivito.ContentTag content={module} attribute="content" />
      ) : (
        <InPlaceEditingPlaceholder>
          Select the module in the widget properties.
        </InPlaceEditingPlaceholder>
      )}
    </div>
  );
});

Depending on whether the UI is in editing mode and a module is present, a hint either about the consequences of changing the content, or on how to specify the module is displayed.

All of a sudden, we’re done! If you already have a ContentModule object, you can now add one or more ContentModuleWidgets to a page, specify the ContentModule to use (via the properties), and add content to it, directly on the page.

Final words

Being able to change recurring content simply by touching any of its occurrences is definitely of great help to editors. It obviates the need to recall where this content has been included as well and to then repeat the adjustments there too, which saves a lot of time and makes it really easy to overhaul specific pieces of content.

If most or all of your content modules are made up of roughly the same widgets, you could even go one step further and predefine the widgets a new module should be equipped with. See Creating a Custom Page Type for instructions on how to specify an object’s initial content.

Note that our approach to providing global content has a small catch. Pages containing one or more content modules won’t show up in search results if the search term was only found in these modules. To remedy this, one would have to programmatically add the pages containing the modules to the search result. Those pages can be found using the linksTo operator in a search.