Menu
Menu Sheet Overlay
Search

Integrating Amplience CMS with Mobify, Part 1: Creating Custom Pages with Amplience Content

Note: Find a working Amplience/Mobify integration example in Mobify's Reference Integrations repo. To access the private repo, reach out to your Mobify contact.

Introduction

This 2-part series will help you integrate content from Amplience content management system (CMS) such as blogs, images and banners into your Mobify Platform project. In this first article, you’ll learn how to create custom pages controlled by Amplience content. As an example, we’ll demonstrate how you can add a simple blog page to your Progressive Web App (PWA).

Overview of the schema

Note: Refer to the Amplience documentation for an article on how to create schema and its content using the Amplience Dynamic Content Hub.

Let’s explore how each object in the schema relates, and what properties they contain:

  1. BlogSlot

BlogSlot is the first schema object you’ll want to create. It's one of the most important schemas in the Amplience backend because you can schedule its content and create various editions of it. Think of this schema as the page, and any other schema object will just be an object used on the page.

BlogSlot Schema

BlogSlot schema.

  1. BlogList

Next is the BlogList schema, which contains a list of BlogPost objects. By creating this intermediate schema you’ll get two benefits: the overall architecture will be more flexible to use in other schemas, and a visualization component will be assigned to it, so you can see it in isolation. You could alternatively place your BlogPost directly on the BlogSlot, but you would give up those benefits.

BlogList Schema

BlogList schema.

  1. BlogPost

The BlogPost schema represents an individual blog post. It has an author, title, content, and other important data that describe the blog post. Amplience provides some basic built-in types like image and content:

BlogPost Schema

BlogPost schema.

Combining the schemas

Working together, the three schemas create a simple blog:

BlogList Schema

Bird’s eye view of the schema interaction.

Using the schema to create a blog

Before you begin

These steps will prepare your project to consume content:

  1. Make sure you have access to your Mobify project, and that you’ve run through our Getting Started instructions.

  2. We’ll be using the Amplience API content client to retrieve data, so you’ll need it to follow along. Install the client by running the following command from the root directory of your project, replacing the text <your-pwa-project-name> at the end with your real project name:

npm run lerna add -- dc-delivery-sdk-js react-markdown --scope=<your-pwa-project-name>
  1. Next, create a configuration file that will store the Amplience information. This file will contain information such as the virtual staging environment location, account name, and various content ID’s. Name this file amplience.config.js and place it at the root of your application, like this:
// amplience-integration/packages/pwa/amplience.config.js

module.exports = {
    AMPLIENCE_BLOG_SLOT_ID: '<BLOG_SLOT_ID>',
    AMPLIENCE_ACCOUNT_NAME: '<ACCOUNT_NAME>',
    AMPLIENCE_VSE: '<VSE_URI>',
}

Note: The Amplience API does not have endpoints for content search, so you need to hardcode the content ID required for your application. When you create a BlogSlot, find its content ID, and then save this ID into a configuration file for use later.

Creating the components

This implementation consists of three pages, three components and one higher order component (HOC). To keep your code DRY (Don’t Repeat Yourself!), you can use the HOC to help abstract any functionality you don’t want to repeat.

The main role of the HOC is to retrieve content from the Amplience backend, hence the name withContent. It allows you to wrap any page in your Mobify PWA, and by defining a single static function to return the content ID, it will automatically have the content mapped onto its properties.

Click to expand the HOC code example!
// amplience-integration/packages/pwa/app/components/with-content/index.jsx

/* global NODE_ENV */
import React from 'react'

import {ContentClient} from 'dc-delivery-sdk-js'
import {HTTPNotFound} from 'progressive-web-sdk/dist/node-ssr/universal/errors'
import {AMPLIENCE_ACCOUNT_NAME, AMPLIENCE_VSE} from '../../../amplience.config.js'

/**
 * Wrap any "RouteComponent" (a page) with this HoC to have the Amplience content
 * object mapped onto it's props. You can specify which content object is retrieved
 * from the server by defining the `getContentId` method to return the id.
 *
 * This component is safe to use on the server-side.
 */
const withContent = (WrappedComponent, {stagingEnvironment} = {}) => {
    /* istanbul ignore next */
    const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name

    const WithContent = (props) => <WrappedComponent {...props} />

    // If a staging environment has been passed in via arguments, use it, otherwise
    // determine the environment to use based on the `NODE_ENV` (local dev will use
    // the staging environment defined in the config file, production will not).
    const client = new ContentClient({
        account: AMPLIENCE_ACCOUNT_NAME,
        stagingEnvironment:
            stagingEnvironment || NODE_ENV !== 'production' ? AMPLIENCE_VSE : undefined
    })

    WithContent.displayName = `WithContent(${wrappedComponentName})`

    /**
     * This function should return the id of the content you want your wrapped component
     * to get. It can be a static value, but also can be a value determined by the arguments
     * passed in. E.g. parsing a content id from the params or location.
     *
     * @param {object} args - Argument passed in from `getProps` includes req, res, params, location.
     * @param {object} client - The Amplience 'dc-delivery-sdk-js' instance, in case you need to do
     *    additional requests after getting content. E.g. If your content object contains other linked
     *    content, you can use the client to retrieve it.
     */
    // eslint-disable-next-line no-unused-vars
    WithContent.getContentId = async (args) => {
        throw new Error('`getContentId` has not been defined.')
    }

    /**
     * Define this method in your `withContent` component to post process any data
     * retrieved from the Amplience backend. You can alternatively do the same work
     * in the components `render` method, but doing it here will ensure any processing
     * only happens once.
     */
    WithContent.processContent = async (content) => content

    WithContent.getProps = async (args) => {
        let content
        let props

        if (!AMPLIENCE_ACCOUNT_NAME) {
            throw new Error('`AMPLIENCE_ACCOUNT_NAME` not set')
        }

        const processContent = WrappedComponent.processContent || WithContent.processContent
        const getContentId = WrappedComponent.getContentId || WithContent.getContentId
        const contentId = await getContentId(args)

        // If the wrapped component has a `getProps` method defined, call it.
        if (WrappedComponent.getProps) {
            props = await WrappedComponent.getProps(args)
        }

        try {
            content = await client.getContentItem(contentId)
        } catch (e) {
            // Looks at those this client throws strings as errors.
            throw new HTTPNotFound(e)
        }

        const processedContent = await processContent({...content.toJSON()}, {...args, client})

        return {
            ...props,
            ...processedContent
        }
    }

    return WithContent
}

export default withContent

Next you’ll want to create components representing the BlogList and BlogPost schemas. As a rule of thumb, it’s best to create a React component for each Amplience content schema. (This will become important later, when you create the visualization page.)

Notice how the properties of each React component match the schema’s properties:

Click to expand the BlogList code example!
// amplience-integration/packages/pwa/app/components/blog-list/index.jsx

import React from 'react'
import PropTypes from 'prop-types'

import Button from 'progressive-web-sdk/dist/components/button'
import Icon from 'progressive-web-sdk/dist/components/icon'
import List from 'progressive-web-sdk/dist/components/list'

// TODO: We need to create components for the blog list and blog post schema's.
// We need to do this so we can properly use the visualizations.
const BlogList = ({title, subtitle, blogPosts = []}) => (
    <div className="c-blog-list">
        <h1 className="c-blog-list__title">{title}</h1>

        <p className="c-blog-list__subtitle">{subtitle}</p>

        <hr />

        <List>
            {blogPosts.map(({title, description, author, urlSlug}, index) => (
                <article key={index} className="c-blog-list__item">
                    <div className="c-blog-list__item-icon">
                        <Icon name="info" size="large" />
                    </div>

                    <div className="c-blog-list__item-details">
                        <div className="c-blog-list__item-info">
                            <header className="c-blog-list__item-header">{title}</header>
                            <div className={`c-blog-list__item-description`}>{description}</div>
                        </div>
                        <div className={`c-blog-list__item-footer`}>
                            <div
                                className={`c-blog-list__item-author`}
                            >{`Authored by ${author}`}</div>
                            <div className={`c-blog-list__item-action`}>
                                <Button
                                    className="pw--tertiary"
                                    href={`/blog/${encodeURIComponent(urlSlug)}`}
                                >
                                    View
                                </Button>
                            </div>
                        </div>
                    </div>
                </article>
            ))}
        </List>
    </div>
)

BlogList.propTypes = {
    title: PropTypes.string,
    subtitle: PropTypes.string,
    blogPosts: PropTypes.array
}

export default BlogList
Click to expand the BlogPost code example!
// amplience-integration/packages/pwa/app/components/blog-post/index.jsx

import React from 'react'
import PropTypes from 'prop-types'

import ReactMarkdown from 'react-markdown'
import {createSourceSet} from '../../utils/utils'
import ImageComponent from 'progressive-web-sdk/dist/components/image'

const BlogPost = ({title, description, content, image}) => {
    const imageWidth = 640
    const imageHeight = 480
    const images = image && createSourceSet(image, [[imageWidth, imageHeight]])

    return (
        <div className="c-blog-post">
            {images && <ImageComponent src={images[0]} alt={image.altText} />}

            <h1 className="u-padding-top-md u-margin-bottom-md">{title}</h1>

            <p className="u-margin-bottom-lg">{description}</p>

            {content && <ReactMarkdown source={content[0].text} />}
        </div>
    )
}

BlogPost.propTypes = {
    blogPost: PropTypes.object,
    content: PropTypes.array,
    description: PropTypes.string,
    image: PropTypes.object,
    title: PropTypes.string
}

export default BlogPost

Creating the pages

Now that you’ve defined your components and a way to get data from the Amplience backend, it’s time to create the pages for a functioning blog experience.

You’ll need to show two page components: the BlogSlot view and the BlogPost view. Notice the use of the withContent HOC:

Click to expand the BlogSlot code example!
// amplience-integration/packages/pwa/app/pages/blog-slot/index.jsx

import React from 'react'
import PropTypes from 'prop-types'
import Helmet from 'react-helmet'

import withContent from '../../components/with-content'
import BlogList from '../../components/blog-list'
import {AMPLIENCE_BLOG_SLOT_ID} from '../../../amplience.config.js'

const BlogSlot = ({blogList}) => (
    <div className="t-blog">
        <Helmet>
            <title>Blog</title>
            <meta name="description" content="" />
        </Helmet>

        <BlogList {...blogList} />
    </div>
)

BlogSlot.propTypes = {
    blogList: PropTypes.object
}

BlogSlot.getTemplateName = () => 'BlogSlot'

BlogSlot.getContentId = () => AMPLIENCE_BLOG_SLOT_ID

export default withContent(BlogSlot)
export {BlogSlot as WithoutContentBlogSlot}
Click to expand the BlogPost code example!
// amplience-integration/packages/pwa/app/pages/blog-post/index.jsx

import React from 'react'
import Helmet from 'react-helmet'

import {HTTPNotFound} from 'progressive-web-sdk/dist/node-ssr/universal/errors'
import {AMPLIENCE_BLOG_SLOT_ID} from '../../../amplience.config.js'

import withContent from '../../components/with-content'
import BlogPostComponent from '../../components/blog-post'

const BlogPost = (props) => (
    <div className="t-blog-post">
        <Helmet>
            <title>Blog</title>
            <meta name="description" content="" />
        </Helmet>

        <BlogPostComponent {...props} />
    </div>
)

BlogPost.getTemplateName = () => 'BlogPost'

BlogPost.getContentId = () => AMPLIENCE_BLOG_SLOT_ID

BlogPost.processContent = async (content, {params}) => {
    const {blogList} = content
    const {urlSlug} = params

    // Find the blog-post using the url slug.
    const post = blogList.blogPosts.find((post) => post.urlSlug === urlSlug)

    if (!post) {
        throw new HTTPNotFound('Could not find Blog Post')
    }

    return {
        ...post
    }
}

export default withContent(BlogPost)
export {BlogPost as WithoutContentBlogPost}

Integrating the blog into your Mobify PWA

Now that you’ve created the page components representing your BlogSlot and BlogPost schemas, complete the process by adding them to your routes.jsx file, like this:

Click to expand the routes.jsx code example!
// amplience-integration/packages/pwa/app/routes.jsx

import React from 'react'
import Loadable from 'react-loadable'
import {Route, IndexRoute} from 'react-router'

import PageLoader from './components/page-loader'
import PWAApp from './components/pwa-app/index'

const Home = Loadable({
    loader: () => import('./pages/home/index').then((module) => module.default),
    loading: PageLoader
})

const ProductList = Loadable({
    loader: () => import('./pages/product-list/index').then((module) => module.default),
    loading: PageLoader
})

const ProductDetails = Loadable({
    loader: () => import('./pages/product-details/index').then((module) => module.default),
    loading: PageLoader
})

const BlogSlot = Loadable({
    loader: () => import('./pages/blog-slot/index').then((module) => module.default),
    loading: PageLoader
})

const BlogPost = Loadable({
    loader: () => import('./pages/blog-post/index').then((module) => module.default),
    loading: PageLoader
})

const Visualization = Loadable({
    loader: () => import('./pages/visualization/index').then((module) => module.default),
    loading: PageLoader
})

const routes = (
    <Route path="/" component={PWAApp}>
        <IndexRoute component={Home} />
        <Route path="category/:categoryId" component={ProductList} />
        <Route path="products/:productId" component={ProductDetails} />
        <Route path="blog" component={BlogSlot} />
        <Route path="blog/:urlSlug" component={BlogPost} />
    </Route>
)

export default routes

Adding visualizations

Now that your blog is working, it’s time to take advantage of the Amplience visualizations feature. Typically, content administrators want to preview any content changes before they publish. This is where Amplience visualizations come in. For each content schema defined in your backend, you can configure a visualization URL. With this URL, content administrators can preview the component tasked with the job of “visualizing” the content data.

First, you’ll need to take two steps to ensure your PWA can support visualizations:

  1. Expand your PWA’s functionality, adding the ability to hide the header and footer when needed.
  2. Create a page that will render a select component based on a query parameter passed into it.

Now it’s time to define the visualization page. Let’s start with the page component:

Click to expand the visualization page code example!
// amplience-integration/packages/pwa/app/pages/visualization/index.jsx

import React from 'react'
import PropTypes from 'prop-types'
import Loadable from 'react-loadable'
import queryString from 'query-string'

import {AMPLIENCE_VSE} from '../../../amplience.config.js'

import PageLoader from '../../components/page-loader'
import withContent from '../../components/with-content'

const BlogSlot = Loadable({
    loader: () => import('../blog-slot/index').then((module) => module.WithoutContentBlogSlot),
    loading: PageLoader
})

const BlogPost = Loadable({
    loader: () => import('../blog-post/index').then((module) => module.WithoutContentBlogPost),
    loading: PageLoader
})

const BlogList = Loadable({
    loader: () => import('../../components/blog-list/index').then((module) => module.default),
    loading: PageLoader
})

const Visualization = (props) => {
    const {query} = props.location
    const contentType = query.type
    let Component

    switch (contentType) {
        case 'blogpost':
            Component = BlogPost
            break
        case 'blogslot':
            Component = BlogSlot
            break
        case 'bloglist':
            Component = BlogList
            break
        default:
            console.error('matched no components')
            break
    }

    return <Component {...props} />
}

Visualization.getContentId = ({location}) => {
    const queryValues = queryString.parse(location.search)
    return queryValues.id
}

Visualization.propTypes = {
    location: PropTypes.object
}

// NOTE: The visualization page will always get it's data from the staging environment.
export default withContent(Visualization, {stagingEnvironment: AMPLIENCE_VSE})

// staging environment.
export default withContent(Visualization, {stagingEnvironment: AMPLIENCE_VSE})

Notice how we defined the static getContentId method in the code example above. This will allow you to parse the content ID from the URL. You’ll also parse the component type value from the URL and use that to determine which component to render.

Now that you’ve defined the visualization page, you’ll need to add it to your routes.jsx file, like this:

Click to expand the routes.jsx code example!
// amplience-integration/packages/pwa/app/routes.jsx

import React from 'react'
import Loadable from 'react-loadable'
import {Route, IndexRoute} from 'react-router'

import PageLoader from './components/page-loader'
import PWAApp from './components/pwa-app/index'

const Home = Loadable({
    loader: () => import('./pages/home/index').then((module) => module.default),
    loading: PageLoader
})

const ProductList = Loadable({
    loader: () => import('./pages/product-list/index').then((module) => module.default),
    loading: PageLoader
})

const ProductDetails = Loadable({
    loader: () => import('./pages/product-details/index').then((module) => module.default),
    loading: PageLoader
})

const BlogSlot = Loadable({
    loader: () => import('./pages/blog-slot/index').then((module) => module.default),
    loading: PageLoader
})

const BlogPost = Loadable({
    loader: () => import('./pages/blog-post/index').then((module) => module.default),
    loading: PageLoader
})

const Visualization = Loadable({
    loader: () => import('./pages/visualization/index').then((module) => module.default),
    loading: PageLoader
})

const routes = (
    <Route path="/" component={PWAApp}>
        <IndexRoute component={Home} />
        <Route path="category/:categoryId" component={ProductList} />
        <Route path="products/:productId" component={ProductDetails} />
        <Route path="blog" component={BlogSlot} />
        <Route path="blog/:urlSlug" component={BlogPost} />
        <Route path="visualization" component={Visualization} />
    </Route>
)

export default routes

The final step is to setup your Amplience content types to define the correct visualization URL. Complete these steps for each content type:

  1. Sign in to your Amplience Dynamic Content account.

  2. Click on Development on the top menu.

  3. From the Development page, click on Content types.

  4. Click on one of your content types in the list, which will load a detailed view with additional settings.

  5. Scroll to the bottom and click the Add a visualization button, which will add 2 new fields to fill out above the button: the Visualization URI, and the Visualization label.

    1. Update the Visualization label field with a short name for this visualization.

    2. Update the Visualization URI field using the following format, replacing where we've written <published-project-hostname> and <content-type> with your own project details:

      https://<published-project-hostname>/visualization?type=<content-type>&id={{content.sys.id}}&contentOnly=true

Here's how it will look on your screen:

Amplience Visualizations

Amplience Visualizations.

To preview content changes, ensure your Visualizations sidebar is open while editing your production data. This is how your Amplience Dynamic Content dashboard should look while editing a BlogSlot content slot:

Amplience Dashboard

Amplience Dynamic Content Dashboard.

That concludes Part 1 of our series! You should now have a basic understanding of how you can create custom Mobify pages with Amplience content.

Next steps

Next, you can learn about how to integrate Amplience content directly into your PWA’s existing pages with Part 2 of our series on Integrating Amplience CMS with Mobify. You can also review a working Amplience/Mobify integration example in Mobify's Reference Integrations repo. To access the private repo, reach out to your Mobify contact.

IN THIS ARTICLE:

Feedback

Was this page helpful?