Building the Amplience blog (pt.1): JAMstack & Dynamic Content

Joe Warner
September 30, 2019
8 mins
Product updatesEngineering

In this blog we'll look at how we built the Amplience blog site itself, using the tools and technologies that we've been building up over several years, including our modern headless Content Management System (CMS) - Dynamic Content. If you're a developer looking to build lightning fast microsites using Dynamic Content, then this post will help get you started.

If you can't wait to get started, you can find the source code on GitHub.

This blog post is part 1 of a series of 2. In Part 1 we explain how the blog is delivered as a static website using the JAMstack architecture, while in Part 2 we go into detail about how we made use of Dynamic Content’s preview and visualization features to allow users to preview their content before it’s published.

What we’ll cover

  • Why we chose the technologies we used

  • How to get data into your application using our SDK

  • Common patterns used when building a static site

We will not be going into too much detail into framework specific implementation details. Instead we’ll focus on how to build a site using Dynamic Content and how a simple workflow works.

What technology did we use to build this blog?

The Architecture

We chose to use the combination of technologies known as JAMStack: JavaScript, APIs and Markup. JAMStack is great for building fast and reliable sites, and at its core it uses static site generators. These often use GitHub commits or webhooks to trigger site builds when you update content, fetch data at build time in order to reduce API calls and prebuild all your pages before deploying them to a CDN to maximise site performance.

The App framework

For the framework we used Next.js. It's based on React with Server Side Rendering (SSR) and static export capabilities. We want to export our site statically at build time and don't require runtime SSR for a basic blog site, but the SSR features are useful for building dynamic preview and visualization features (see part 2 of this blog series).

Next.js also has an extensive plugin system that enabled us to build a Progressive Web App with offline capabilities.

We also decided to use TypeScript, to define structured types based on our schemas and to create lovely typesafe components.

Markdown

To render a blog's markdown content as rich text we use react-markdown. This was chosen because it’s easy to use and provides control over rendering and styling.

Hosting

For hosting we chose Netlify. Netlify is essentially a Continuous Integration(CI)/Continuous Deployment(CD) pipeline integrated with GitHub: you push changes to your master branch, it rebuilds your site and pushes to the production CDN.

Netlify includes easy to configure "build hooks" that will listen for an external webhook event and rebuild the site when triggered. Build hooks are set up in Netlify in 'site settings' -> 'build and deploy' and provide a webhook URL that we add to a webhook in Dynamic Content.

We created a webhook in Dynamic Content with the URL from Netlify and chose to trigger it with the "Edition published" event. This event is sent when an edition containing a slot with content for the blog is published.

We now have a continuous deployment pipeline set up that publishes our content whenever the codebase or the content changes.

Content

To create, schedule and deliver the content for our blog we used Dynamic Content.

In this section, we’ll provide a quick overview of Dynamic Content’s functionality.

The following features of Dynamic Content were key to building the blog:

  • Content Types to model our data

  • Webhooks to trigger builds

  • The Content Delivery API to fetch our data at build time

  • Visualization and preview so that we can have a side by side view of the content in the browser when editing changes. We’ll cover visualization and preview in detail in Part 2.

Dynamic Content uses content types, defined using JSON Schema, that specify the way the content is structured and validated and can be used to model complex relationships between content. Content type schemas are easy to create using the Dynamic Content Schema Editor, as shown in the example below.

Once content types are deployed, your users can start using them to create content. This is done from the production section of the Dynamic Content app. To create a new piece of content, the user just chooses the type of content to add and fills in the content editing form as shown below, adding images and video from their own media repository. The content editing form also applies your business rules, such as the maximum and minimum length of the copy text, defined as part of the content type.

A visualization of how the blog will appear once it’s published is shown on the right hand side of the window. We’ll explain how to show this visualization, as well as how to preview the blog site at a specified date and time, in part 2 of this blog.

Content can either be published directly or scheduled to be published later by creating an event, within which you can create an edition containing one or more slots with content to be published at a specified date and time. That’s how we schedule content for the blog posts.

Content Model

When building this blog we wanted to design a simple content model that would be able to grow to meet our needs in the future. We settled on 5 Content types and 1 Slot to begin with, we felt this would give us room to expand it later.

Our Content Model

Here is a model of the schemas we've created for our blog:

We have:

  • A root Slot which we use to plan and schedule our posts.

  • Our Blog List content type. This is made up of a list of Blog Posts, with a title and a subheading.

This was all we needed in order to create our homepage for our first iteration.

A Blog Post is where the main content lives. It includes the following properties:

  • Title

  • Date of creation

  • A description of the post

  • A slug for SEO friendly URLs

  • A list of content: this can be made up of rich text, videos and images

  • A list of the blog post authors

When modeling your content you should have reuse at the front of your mind. Each one of our content types could be reused to make new blocks. The Author content type could be used for filtering, while Blog Post and Blog List could be used to populate a list of related posts. This shows the power of a content type based model.

Fetching content at build time

So, how do we populate our application with data? Well in Next.js you have to provide a map of all the pages with all the resources it needs to build that page. Your React Page containers will have a getInitialProps function which will be called at build time to populate your page and then export it as static HTML.

To retrieve the content we made use of the Dynamic Content Delivery SDK. Setting up the SDK is simple, just provide your account settings and then pass the content id to the getContentItem function as shown in the code below. When the request is resolved your JSON data is returned.

1 const { ContentClient } = require('dc-delivery-sdk-js');
2
3const client = new ContentClient({
4    account: process.env.ACCOUNT_NAME,
5    baseUrl: process.env.BASE_URL
6});
7
8client.getContentItem('b23799dc-d557-11e9-a2a2-784f436f1007')
9      .then(data => console.log(data.toJSON()))

But how does this relate to Next.js and our blog? Below is a basic example of code in the

1next.config.js
file that fetches blog posts, creates a map of all the blogs and defines their path. It also provides an id so that the blog post itself can handle fetching of its data.

1// next.config.js
2const { ContentClient } = require('dc-delivery-sdk-js');
3
4const client = new ContentClient({
5    account: process.env.ACCOUNT_NAME,
6    baseUrl: process.env.BASE_URL
7});
8
9async function generateBlogPages() {
10    const routeContentReference = process.env.DYNAMIC_CONTENT_REFERENCE_ID;
11    // fetching your content item by id and return a list of references to blog posts
12    const data = (await client.getContentItem(routeContentReference)).toJSON();
13    const { blogList } = data;
14
15    return blogList.reduce((pages, blog) => {
16        const slug = encodeURIComponent(blogPost.urlSlug);
17        const blogId = blogPost._meta.deliveryId;
18        // creating a page map with the requested data to fetch that blog
19
20        return Object.assign(pages, {
21            [`/blog/${slug}`]: {
22                page: '/blog',
23                query: { blogId, slug }
24            }
25        });
26    }, {});
27}
28
29module.exports = {
30    env: {
31        account: process.env.ACCOUNT_NAME,
32        baseUrl: process.env.BASE_URL
33    },
34    exportPathMap: async () => {
35        const blogPages = await generateBlogPages();
36        return Object.assign({}, blogPages, {
37            '/': {
38                page: '/'
39            }
40        });
41   }
42}

A simplified

1blog.tsx
file is shown below. It is passed a query from
1next.config.js
and can then fetch the blog post for that route, process the data and then return it to the component. The code in blog.tsx file would be used to display a single post and in this example it would take a title, description and the content of the post in markdown format.

One of the benefits of using a Static Site Generator is that you can pass meta data on a per page basis in order to gain better SEO.

1import { NextPage } from 'next'
2import { NextSeo } from 'next-seo'
3import { ContentClient } from 'dc-delivery-sdk-js'
4import ReactMarkdown from 'react-markdown'
5import Layout from '../layouts/default'
6
7interface BlogPost {
8  title: string
9  description: string
10  content: string
11}
12
13const BlogPage: NextPage<BlogPost> = ({ title, description, content }) => (
14  <Layout>
15    <NextSeo title={title} description={description} />
16    <h1>{title}</h1>
17    <p>{description}</p>
18    <ReactMarkdown source={content} />
19  </Layout>
20)
21
22BlogPostPage.getInitialProps = async ({ query }) => {
23  const { blogId } = query
24
25  const client = new ContentClient({
26    account: process.env.ACCOUNT_NAME,
27    baseUrl: process.env.BASE_URL
28  })
29
30  const blog = await client.getContentItem(blogId)
31
32  const { title, description, content } = blog.toJSON()
33
34  return {
35    title,
36    description,
37    content
38  }
39}
40
41export default BlogPage

Images

We wanted to ensure that our site met accessibility standards from the very start. To do this we created image/video content-types that made it a requirement to supply an alt tag, used for accessibility to describe an image to those users who are unable to see it. This means that when our site renders an image we can create a component that expects an alt tag and ensures all imagery is accessible.

Here's the image content type schema:

1{
2  "$schema": "http://json-schema.org/draft-04/schema#",
3  "id": "https://schema.localhost.com/image.json",
4  "title": "Image",
5  "description": "Image schema",
6  "allOf": [
7    {
8      "$ref": "http://bigcontent.io/cms/schema/v1/core#/definitions/content"
9    }
10  ],
11  "type": "object",
12  "properties": {
13    "image": {
14      "title": "Image",
15      "description": "insert an image",
16      "type": "object",
17      "anyOf": [
18        {
19          "$ref": "http://bigcontent.io/cms/schema/v1/core#/definitions/image-link"
20        }
21      ]
22    },
23    "altText": {
24      "type": "string",
25      "minLength": 0,
26      "maxLength": 150,
27      "title": "Alt text",
28      "description": "insert image alt text"
29    }
30  },
31  "propertyOrder": [
32    "image",
33    "altText"
34  ],
35  "required": [
36    "image",
37    "altText"
38  ]
39}

At build time we generate image URLs using the Dynamic Content Delivery SDK.

This allows us to make use of the features of Amplience Dynamic Media and serve multiple permutations of an asset, with different size, format and quality options, from the same source URL. A good use case for this would be using the same asset for the high quality feature image used at the top of a blog post and for a lower quality thumbnail image.

This also gives us the building blocks to build image source sets so that the appropriately sized image can be chosen for each screen size.

A basic example of building an image source set is shown below. We just provide an array with the desired sizes and generate URLs from the same source image, but with the width and height parameters for each size.

1import { Image, ImageFormat } from "dc-delivery-sdk-js";
2
3const createSourceSet = ({ image, alt }, sizes = []) => {
4 return sizes.map(([ width, height ]) => (
5   new Image(image, { account })  
6      .url()
7      .seoFileName(alt)
8      .width(width)
9      .height(height)
10      .format(ImageFormat.PNG)
11      .build()
12 ));
13}
14
15//...
16import { createSourceSet } from "./util";
17//...
18
19Page.getInitialProps = async ({ query: { blogPostId } }) => {
20  const account = process.env.ACCOUNT_NAME;
21  const blogPost = await getBlogPost(blogPostId);
22  const { featuredImage } = blogPost;
23
24  const images = createSourceSet(featuredImage, [
25    [320, 180],
26    [640, 360],
27    [1024, 576]
28  ]);
29
30  Object.assign(blogPost, {
31    featuredImage: {
32        images,
33        alt
34    }
35  })
36
37  return blogPost;
38}

Where to go from here

We’ve made the source code for the blog available on GitHub and it’s a great starting point for your own projects. It features out of the box: a Progressive Web App manifest, offline support and SASS support using TypeScript and it should give you everything you need to make a lean static site powered by Dynamic Content.

If you want to know more about how to use Next.js as a Static Site Generator view their docs.

In part 2 of this blog series we’ll look at how the visualization and preview features were implemented in the blog, allowing you to view individual blog posts before they’re published and see how the entire site will look at a specified date and time.