Dynamic static sites - Implementing an oxymoron

80 staticlydynamic

Many of us love static sites for their simplicity: they are cheap to deploy and maintain, and also incredibly quick to load. When Gatsby appeared, it even became possible to create hybrid sites using React, which are rendered statically but become interactive once loaded client-side.

Edit on a static page, dynamically

You just write some GraphQL, pass the data to your UI components, and the rest is done for you.

"What if we took a static site, and made it more dynamic?" — Stephan

I laughed at first, shook my head, but then started thinking. What first seemed like a silly contradiction slowly started to make sense to me.

GatsbyJS offers a full-fledged static content authoring experience that allows connection of your projects to many API’s and data sources, including Contentful’s content infrastructure, making it an excellent companion to Gatsby. With Contentful being a first-class citizen in the Gatsby world, content is easily edited in a nice UI and with the use of Netlify, setting deployments up takes less than a minute.

The only benefit classic dynamic websites offer over these new workflows is that content can be edited right on the page — you click ‘edit’, make your changes, and you are done.

Thus, we set out to try and solve this using the contentful delivery and especially the powerful management APIs, some GraphQL and a few terribly styled React components.

If you are curious about static sites, Gatsby, Gatsby plugin development or new ideas on how to use Content Infrastructure, then this post is for you.

There is a template with source files available on Github; this will come in handy as reference in later parts of this article.

Part I - Hitting the ground running:

What we’re going to do first is set up a static site that is automatically deployed to Netlify. Let’s begin by using a starting template. Go ahead and click "Fork" in the upper right corner of the repo to make your own copy to work on.

Fork the template to begin working

After that, clone the project.

Clone the project

cd into the directory and run npm install

While npm is doing its thing, use the time to sign up for a free Contentful account — you can make it even easier and sign in using your Github account with one click.

Once you’re in, choose to create an empty space.

Now in the gatsby-starter-gcn folder, run npm run setup and follow the instructions.

It will bootstrap all the data that is needed for the example to run using the Contentful Import tooling, which in turn uses the Management API, the same thing we will be using later to actually update the content using our plugin.

After that is done, you can run npm run dev and go to http://localhost:8000 to see your fresh new static site.

Next follow these deployment instructions, and optionally some additional settings, to get your website fully set up and accessible on the Internet. Now, every time you push some code or edit content, your site is automatically rebuilt.

Part II - The Gatsby Plugin

To recap our initial plan: We want to allow developers to easily mark parts of the static site as loaded from a specific contentful entity and then show an ‘Edit’ link on the generated site. That allows any admin user with a Management Token to edit the content right on the website; with the updated content automatically showing up on the site after a while.

If you’ve followed Part I, that last part would already be taken care of — whenever you push code or publish an entity on Contentful, Netlify will rebuild your site and distribute it on its content delivery network (CDN).

Before we dive into the implementation details, let’s do a quick refresher on how Gatsby renders static sites:

  • In Gatsby, you define a ‘Page’ type and the GraphQL query, for all the data needed to render a page inline with the ‘Page’ component.
  • During the build stage, before rendering the component, that data is fetched from a ‘Source’ and then later provided to the component for rendering.
  • Contentful provides a source plugin for that out-of-the-box.
  • Gatsby also provides an extension API that allows you to hook into almost every step of the build process and customize things.

Let’s take a look at src/pages/index.js: The query requests allContentfulPost, yielding a list of all entries for the Post content type. From there, it selects a set of properties that it needs to render.

In order to build an editing component, in addition to the actual content, we need to know the associated content model in contentful so that we can render an interface to edit the content.

Here’s the query we’d like to use:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 query {
     node {
         title
         id
         slug
         publishDate(formatString: "MMMM DD, YYYY")
         heroImage {
             ...
         }
         body {
             ...
         }
         contentfulEditor {
             contentTypeId
             entityId
             spaceId
             fields {
                 id
                 type
             }
         }
     }
 }

This little addition tells the internal GraphQL endpoint that we would like to have a contentfulEditor object that contains the Content Type ID, Entity ID, Space ID, and the id and type of all the fields.

"That’s great, Tim, but so far that’s nothing but wishful thinking", I hear you say.

That is true, but having specified our interface now, it will be much easier to know exactly what we need to implement. So let’s jump to that.

Part II (1) - The Plugin

As mentioned earlier, Gatsby allows you to hook into almost every build step. What we would like to do is hook into a point where the data has already been loaded, then add some additional data about the content model on top of it.

The ‘setFieldsOnGraphQLNodeType’ extension point is the perfect candidate for that. It allows you to change the data from the GraphQL query, before any pages were rendered, but after the Contentful source plugin has already fetched the data.

First, we create a folder ‘plugins’ in the project root — Gatsby will automatically look through any folders in the ‘plugins’ directory, so let’s create a folder named ‘contentful-editor’ inside of that.

Within ‘contentful-editor’, create a package.json file containing just ‘{}’ (Gatsby infers the plugin name from the folder structure) and a gatsby-node.js file. That tells Gatsby to register an extension to run for every node; meaning it will be called for every Page that you are about to define.

In there, we will want to do the following:

  • If a node contains data fetched from Contentful, collect all the entities in it and query the API for them
  • Query the Contentful API for all the Content Types that are associated with those entity
  • Append the previously defined data to every data node

Here’s what it will look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module.exports = {
  setFieldsOnGraphQLNodeType: async function (data, { spaceId, accessToken, host }) {
    const node = data.type
    const isContentfulNode = node.name.toLowerCase().startsWith('contentful')

    if (!isContentfulNode) {
      return
    }

    const entityNodes = omitIrrelevantNodes(node.nodes)
    const getContentTypeForEntry = await getContentTypeMapping(entityNodes, { spaceId, accessToken, host })

    for (const entityNode of entityNodes) {
      const id = entityNode.contentful_id
      const ct = getContentTypeForEntry(id)

      entityNode.contentfulEditor = {
        contentTypeId: ct.sys.id,
        entityId: id,
        spaceId: spaceId,
        fields: ct.fields
      }
    }
  }
}

You can find the complete code in the gatsby-node.js file on Github.

Part II (2) - The editor component

Now, we have all the data available, we can move on to rendering an inline editor.

Let’s once again do a quick recap on what we’ve done so far:

  • Set up your own gatsby example project on Github
  • Your example project is automatically deployed to netlify on any content changes or code changes
  • When rendering a page, we can now query for the metadata about the underlying content model for a page

What’s left to do:

  • We need to build a React component that can wrap any other React components that use data from Contentful

Let’s take another look at ‘src/pages/index.js’ (also available as reference on Github): The CardList component iterates over all the posts from Contentful and renders a Card component from every Contentful ‘Post’.

This is the ideal point where we can introduce our wrapper component that enables on-page editing. We wrap it with a ‘ContentfulEditor’ component and pass the ID, ‘contentfulEditor’ data and the post content to it:

1
2
3
4
5
6
7
8
9
<ContentfulEditor contentfulEditor={post.contentfulEditor} entityData={post} key={post.id}>
      <Card
      slug={post.slug}
      image={post.heroImage}
      title={post.title}
      date={post.publishDate}
      excerpt={post.body}
      />
</ContentfulEditor>

Then, we proceed with creating a new file /src/components/ContentfulEditor.js

In there, we want to wrap the ‘Card’ so an ‘Edit’ button is shown on top of it. When that is clicked, a modal that renders an interface to edit content opens; that syncs any changes back to Contentful via the API. That in turn triggers a rebuild on Netlify, updating the actual site after a while.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
render() {
    const { contentfulEditor, children, entityData } = this.props
    const { showModal } = this.state
    const showEditButton = this.context.router.route.location.hash.includes('edit')
    return (
      <EditOverlay>
        {showEditButton && (
          <EditButton onClick={this.editComponent} />
        )}
        {showModal && (
          <EditModal
            closeModal={this.closeModal}
            editorConfig={contentfulEditor}
            entityData={entityData}
          />
        )}
        {children}
      </EditOverlay>
    )
  }
  

When we click on the edit button, we open a modal with the editor. Inside of the editor, we extract the field data and, based on the type of a field, render an editor. This is done by the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 const editors = this.state.fieldData.map((field) => {
      if (field.type === 'Text') {
        return (
          <textarea
            key={field.id}
            value={field.content}
            onChange={(evt) => this.setField(field.id, evt.target.value)}
          />
        )
      }`

      return (
        <input
          key={field.id}
          type="text"
          value={field.content}
          onChange={(evt) => this.setField(field.id, evt.target.value)}
        />
      )
    })

When ‘save’ is clicked, the following code uses the management SDK to upload the data to Contentful:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const saveAndPublish = ({ cmaToken, fieldData, editorConfig }) => {
  const { spaceId, entityId } = editorConfig
  const client = contentful.createClient({
    accessToken: cmaToken
  })

  return client.getSpace(spaceId).then((space) => {
    return space.getEntry(entityId)
  }).then((entry) => {
    for (const field of fieldData) {
      entry.fields[field.id]['en-US'] = field.content
    }
    return entry.update()
  }).then((updatedEntry) => {
    return updatedEntry.publish()
  })
}

Now, just push your changes to Github, wait for Netlify to finish with the new build and you’re ready to go.

Conclusion

Contrary to popular belief, static sites actually can be made dynamic by using Contentful’s Content Management API’s — that’s a major benefit of using a Content Infrastructure over a traditional CMS. Imagine implementing a chatbot that allows you to update your static site via a simple chat message, or the ability to integrate your website with Amazon Alexa and update it with just voice interfaces — your imagination is the only limit as to where and how you can interact with your content.

Blog posts in your inbox

Subscribe to receive most important updates. We send emails once a month.