Build a blog with GatsbyJS and Contentful

June 10th, 2021

GatsbyJS Banner

GatsbyJS is a famous static site generator that is used to build many web pages nowadays, with NextJS, those are commonly used to create sites with great SEO and performance.

In this post, we are going to create a simple blog webpage using GatsbyJS and Contentful CMS, if you haven't heard about this headless CMS, I have a post explaining its benefits as a content infrastructure.

Important: To follow this tutorial, you need basic knowledge of ReactJS, GatsbyJS and Contentful CMS, we are going to use Typescript too, so that is good to know.

Creating the models on Contentful

First, we need to create the models and content in Contentful so, make an account if you haven't already.

It is very intuitive, with the free plan, you have access to only one space, so create a blank space and then go to the Content model option on the upper menu.

In that option, we are going to create our models, think of them as blueprints for the content we are going to make, if we are going to write blog posts, those posts are going to have a title, the main image, a summary, the post's body, etc. That structure is going to be created here, one great thing about Contentful is that we can make relationships between models, for example, we can create a Tag model that only has a description, and a Post model that has a field where one or many tags of type: Tag model are stored, well, you will understand that better when we create our models.

Tag model

Let's create a simple model name Tag, on the Content model section, click the green button labeled Add content type a pop-up form will show up, fill the fields Name, and automatically the field Api Identifier will be completed and an optional description.

Now we need to add all the fields the model needs, click on the blue button Add field, a pop-up with all the available field types you have to choose, I won't stop explaining all of those, I think they are very straightforward.

Select Text type and write a name and Field ID will be automatically filled and click Create.

Great! Now you are familiar with creating a model, let's create the BlogPost model.

Blog post model

The same as before, create the following fields with the following types:

  • Title: text field

  • Slug: text field

  • Main image: media

  • Published date: Date & time

  • Body: rich-text

  • Summary: text

  • Tags: Reference

That last field is when the magic comes in, a reference value is a field that references another model in a relationship one-to-one and one-to-many.

Write the name and select the reference type, click on the button Create and configure. Here, you can choose if this field is required and if you wanted to be only a specific type, on the validation tab select Accept only specified entry type and you will see all the models you have created, select Tag. Click on the Confirm button and the blog post model will be done. Finally, click on the Save green button on the top right corner.

Adding blog post and tags

Now that you have done the models, it's time to create blog posts and tags, go to the Content tab on the top menu, select the Add entry button and choose Tag.

Now you have a form with all the fields that you created earlier, complete all the required fields, and hit the Publish green button.

Now go back and follow the same steps to create a blog post, on the Tags field you will have the option to create a new Tag content or choose an existing one.

Once you have all done, let's do the last step on Contentful.

Get Space ID and Access Token

To use this information on Gatsby or another consumer, you will need your Space ID and your Access Token.

To get them go to the Settings option on the top menu and select API Keys, click on Add API Key, choose a name and a description, under those fields you will have your Space ID and your Access Token, copy both and save them because we are using them later. Remember DO NOT SHARE YOUR ACCESS TOKEN WITH ANYONE.

Now we can go to the code!

Creating the Gatsby project

Use the Gatsby CLI to create a new Gatsby project, now we need to install the libraries we are using: gatsby-source-contentful, react-syntax-highlighter, env-cmd, fontawesome. We are also using TailwindCSS to make the styling quicker, take a look at the Gatsby and Tailwind guide if you don't know how to set up the plugin.

Let's configure the Contentful plugin, on the gatsby-config.js file add:

1module.exports = {
2  siteMetadata: {
3    title: `Gatsby Contentful`,
4    description: `Connection to contentful example`,
5    author: `Vicente De Paz`,
6  },
7  plugins: [
8    `gatsby-plugin-postcss`,
9    `gatsby-plugin-sass`,
10    `gatsby-plugin-typescript`,
11    {
12      resolve: `gatsby-source-contentful`,
13      options: {
14        spaceId: process.env.CONTENTFUL_SPACE_ID,
15        accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
16      },
17    },
18    `gatsby-plugin-image`
19  ],
20};

As you can see, we need to configure the gatsby-source-contentful plugin with a spaceId and an accessToken, the ones we got in the previous section. We store those in the environment variables due is sensible information. It's quite easy to do:

First, create a file called .env.development on the root of the project, then write the variables as follows:

1GATSBY_GRAPHQL_IDE=playground
2GATSBY_CONTENTFUL_SPACE_ID=your_space_id
3GATSBY_CONTENTFUL_ACCESS_TOKEN=your_access_token

The first one is to activate the GraphQL playground IDE we will use later, replace your_space_id and your_access_token with the ones you copied from contentful.

Second, we edit the develop script in our package.json:

1"develop": "env-cmd -f .env.development gatsby develop",
2
Now we can access all the environment variables using proccess.env.VARIABLE_NAME

The other plugins we added are to add extra value to our example ✍(◔◡◔).

Creating main components

Now we are going to create the main page, but before that, let's make some components and a simple layout:

Main layout

Create two files in the directory src/components/layouts/MainLayout:

index.tsx

1import React from 'react';
2
3import './styles.scss';
4
5interface IMainLayout {
6  children: React.ReactNode;
7}
8
9const MainLayout = ({ children }: IMainLayout) => {
10  return (
11    <div className="page-container">
12      <div className="content-wrap">
13        {children}
14      </div>
15    </div>
16  );
17};
18
19export default MainLayout;

styles.scss

1.page-container {
2  display: flex;
3  flex-direction: column;
4  min-height: 100vh;
5  background-color: lighten($color: #c7ceff, $amount: 5);
6}
7
8.content-wrap {
9  flex: 1;
10}

PostCard component

Now let's create a postcard component in src/components/PostCard:

index.tsx

1import React from 'react';
2
3import './styles.scss';
4
5type Props = {
6  title: string;
7  date: string;
8  header: string;
9};
10
11const PostCard = ({ title, date, header }: Props) => {
12  return (
13    <div className="bg-white rounded shadow-md hover:shadow-lg p-6 m-2 w-64 md:w-96 h-36 max-w-xl">
14      <p className="font-bold">{title}</p>
15      <p className="text-sm italic text-gray-500">{date}</p>
16      <hr />
17      <div className="overflow-y-scroll h-16 scrollbar">
18        <p className="mt-4 mb-2">{header}</p>
19      </div>
20    </div>
21  );
22};
23
24export default PostCard;

styles.scss

1.scrollbar::-webkit-scrollbar {
2  width: 13px;
3  height: 13px;
4}
5.scrollbar::-webkit-scrollbar-thumb {
6  background: linear-gradient(13deg, #f9d4ff 14%, #c7ceff 64%);
7  border-radius: 10px;
8}
9.scrollbar::-webkit-scrollbar-thumb:hover {
10  background: linear-gradient(13deg, #c7ceff 14%, #f9d4ff 64%);
11}
12.scrollbar::-webkit-scrollbar-track {
13  background: #ffffff;
14  border-radius: 10px;
15  box-shadow: inset 7px 10px 12px #f0f0f0;
16}

Main page

With those two components created, we can proceed to create the main page.

You need to know we are using GraphQL to query the contentful information, I'm not going deep explaining GraphQL, in few words, is a syntax that allows you to query certain information on a GraphQL server.

If you run develop on your project and go to GraphQL playground you can play and create all the queries you will use.

On the left side, you will write your query and the result will be on the right side, write the following code and then hit the play button:

1query {
2  allContentfulAsset {
3    edges {
4      node {
5        contentful_id
6        file {
7          url
8        }
9      }
10    }
11  }
12}

You will see the data arranged as a json object, so in the code, you'll need to access the result with the same structure as the response you see.

Now that you know how it works, let's create the main page component and explain it:

src/pages/index.tsx

1import { graphql, Link, useStaticQuery } from 'gatsby';
2import React from 'react';
3import PostCard from '../components/PostCard';
4
5import MainLayout from '../components/layouts/MainLayout';
6
7import '../static/styles/styles.scss';
8
9type QueryDataType = {
10  allContentfulBlogPost: {
11    edges: {
12      node: {
13        title: string;
14        header: string;
15        slug: string;
16        publishedDate: string;
17      };
18    }[];
19  };
20};
21
22export default function Home() {
23  const data = useStaticQuery<QueryDataType>(graphql`
24    query {
25      allContentfulBlogPost(sort: { fields: publishedDate, order: DESC }) {
26        edges {
27          node {
28            title
29            header
30            slug
31            publishedDate(formatString: "MMMM Do. YYYY")
32          }
33        }
34      }
35    }
36  `);
37  const { allContentfulBlogPost } = data;
38  return (
39    <MainLayout>
40      <div className="my-20">
41        <h1 className="text-3xl font-serif text-center">Post Menu</h1>
42        <div className="my-6 flex flex-wrap justify-center">
43          {allContentfulBlogPost.edges.map((edge) => (
44            <Link to={`/post/${edge.node.slug}`} key={edge.node.slug}>
45              <PostCard title={edge.node.title} date={edge.node.publishedDate} header={edge.node.header} />
46            </Link>
47          ))}
48        </div>
49      </div>
50    </MainLayout>
51  );
52}

We are using the useStaticQuery custom hook to query the blogposts information, also, because we are using Typescript, we need to type the result, that's why we have the QueryDataType type.

You will see something like this, depending on the information you registered on contentful:

Blogpost Screenshot 1

Build time rendering pipeline

Because we are using GatsbyJS let's take advantage of its build-time rendering and create a page for every post in contentful:

gatsby-node.js

1const path = require('path');
2
3module.exports.createPages = async ({ graphql, actions }) => {
4  const { createPage } = actions;
5  const postTemplate = path.resolve('./src/components/templates/post/index.tsx');
6  const res = await graphql(`
7    query {
8      allContentfulBlogPost {
9        edges {
10          node {
11            slug
12          }
13        }
14      }
15    }
16  `);
17
18  res.data.allContentfulBlogPost.edges.forEach(edge => {
19    createPage({
20      component: postTemplate,
21      path: `/post/${edge.node.slug}`,
22      context: {
23        slug: edge.node.slug,
24      },
25    });
26  });
27};

As you can see, we use graphql here too, and we use a template stored in src/components/templates/post before creating that file, we need to write the last extra component.

IFrameContainer

This component will be a wrapper for every iFrame we use in our post's body, yeah, we can ember videos on our post! but it's a little bit tricky, so let's explain that in the extra content, for now, here you have the component:

src/components/IframeContainer/index.tsx

1import React from 'react';
2
3import './styles.scss';
4
5type props = {
6  children: React.ReactNode;
7};
8
9const IframeContainer = ({ children }: props) => {
10  return <span className="iframecontainer">{children}</span>;
11};
12
13export default IframeContainer;

src/components/IframeContainer/styles.scss

1.iframecontainer {
2  padding-bottom: 56.25%;
3  position: relative;
4  display: block;
5  width: 100%;
6
7  iframe {
8    height: 100%;
9    width: 100%;
10    position: absolute;
11    top: 0;
12    left: 0;
13  }
14}

Post template

Here you have the last component, we are almost done!

src/components/templates/post/index.tsx

1import React from 'react';
2import { graphql, Link } from 'gatsby';
3import { GatsbyImage } from 'gatsby-plugin-image';
4import SyntaxHighlighter from 'react-syntax-highlighter';
5import { monokaiSublime } from 'react-syntax-highlighter/dist/esm/styles/hljs';
6import { BLOCKS, INLINES, MARKS } from '@contentful/rich-text-types';
7import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
8import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
9import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
10
11import IframeContainer from '../../IframeContainer';
12import MainLayout from '../../layouts/MainLayout';
13import Routes from '../../../routes/routes';
14
15import './styles.scss';
16
17export const query = graphql`
18  query($slug: String!) {
19    contentfulBlogPost(slug: { eq: $slug }) {
20      title
21      publishedDate
22      mainImage {
23        gatsbyImageData(layout: CONSTRAINED, placeholder: BLURRED)
24        contentful_id
25        title
26        file {
27          url
28        }
29      }
30      tags {
31        description
32      }
33      body {
34        raw
35        references {
36          gatsbyImageData(layout: CONSTRAINED, placeholder: BLURRED)
37          contentful_id
38          title
39          file {
40            url
41          }
42        }
43      }
44    }
45  }
46`;
47
48const Post = ({ data }: any) => {
49
50  const { contentfulBlogPost } = data;
51
52  const options = {
53    renderNode: {
54      [BLOCKS.EMBEDDED_ASSET]: node => {
55        const id = node.data.target.sys.id;
56        const resource = contentfulBlogPost.body.references.find(
57          ref => ref.contentful_id === id
58        );
59        return (
60          <GatsbyImage
61            image={resource.gatsbyImageData}
62            alt={resource.title}
63            style={{ display: 'flex', justifyContent: 'center' }}
64          />
65        );
66      },
67      [INLINES.HYPERLINK]: node => {
68        if (node.data.uri.includes('player.vimeo.com/video')) {
69          return (
70            <IframeContainer>
71              <iframe
72                title="Unique Title 001"
73                src={node.data.uri}
74                frameBorder="0"
75                allowFullScreen
76              ></iframe>
77            </IframeContainer>
78          );
79        } else if (node.data.uri.includes('youtube.com/embed')) {
80          return (
81            <IframeContainer>
82              <iframe
83                title="Unique Title 002"
84                src={node.data.uri}
85                allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
86                frameBorder="0"
87                allowFullScreen
88              ></iframe>
89            </IframeContainer>
90          );
91        }
92
93        return (
94          <a href={node.data.uri} target="_blank" rel="noopener noreferrer">
95            {node.content[0].value}
96          </a>
97        );
98      },
99    },
100    renderMark: {
101      [MARKS.CODE]: node => {
102        return (
103          <SyntaxHighlighter
104            language="javascript"
105            style={monokaiSublime}
106            customStyle={{ fontSize: 15 }}
107            PreTag={({ children, ...preProps }) => (
108              <span {...preProps}>{children}</span>
109            )}
110            showLineNumbers
111          >
112            {node}
113          </SyntaxHighlighter>
114        );
115      },
116    },
117  };
118
119  return (
120    <MainLayout>
121      <div className="my-20 w-full">
122        <div className="flex justify-center">
123          <div className="w-3/4 lg:w-1/3">
124            <h1 className="text-3xl font-serif text-center">
125              {contentfulBlogPost.title}
126            </h1>
127            <p className="text-sm italic text-gray-500 text-center">
128              {contentfulBlogPost.publishedDate}
129            </p>
130            <hr />
131          </div>
132        </div>
133        <div className="flex justify-center">
134          <div className="bg-white rounded shadow-md hover:shadow-lg p-6 m-2 w-3/4 lg:w-2/3">
135            <div className="rich-content">
136              {documentToReactComponents(
137                JSON.parse(contentfulBlogPost.body.raw),
138                options
139              )}
140            </div>
141            <div className="text-center sm:text-left">
142              <Link
143                to={Routes.HOME}
144                className="hover:text-blue-400 text-gray-400"
145              >
146                <FontAwesomeIcon icon={faChevronLeft} />
147                {'\u00A0'}Go back
148              </Link>
149            </div>
150          </div>
151        </div>
152      </div>
153    </MainLayout>
154  );
155};
156
157export default Post;

And the styles, src/components/templates/post/styles.scss

1.rich-content {
2  h1 {
3    font-size: xx-large;
4  }
5
6  h2 {
7    font-size: x-large;
8  }
9
10  h3 {
11    font-size: large;
12  }
13
14  p {
15    margin-top: 10px;
16    margin-bottom: 10px;
17  }
18
19  a {
20    color: darken($color: lightskyblue, $amount: 10);
21
22    &:hover {
23      color: darken($color: lightskyblue, $amount: 20);
24    }
25  }
26
27  ul {
28    list-style: disc;
29    margin-left: 25px;
30    margin-bottom: 10px;
31
32    li {
33      p {
34        margin: 0;
35      }
36    }
37  }
38
39  ol {
40    list-style: decimal;
41    margin-left: 25px;
42    margin-bottom: 10px;
43
44    li {
45      p {
46        margin: 0;
47      }
48    }
49  }
50}

Now, let me explain this last component, in a page or a template, we can use directly a graphql query instead of using the useStaticQuery hook, as you can see, we use the slug filter, which is the variable we pass on the build time render, it is a large and complex structure, so I'm saving time typing the result as any (don't do this at home kids! ( ͡~ ͜ʖ ͡°)).

We need to export the query and receive it through component props, now, if you run this big query on the GraphQL playground, you will notice that the Contentful richtext is a stringify json object, this is a relatively new change in the library, if you parse the json object, you will see a strange structure, we can't render the post body just like that!

That's when the rich-text-react-rendered comes in, I recommend you to read this short guide and its official documentation for a better understanding.

Rich text options

This plugin provides a handy function called documentToReactComponents and it receives two parameters, first we pass the object to be rendered, and second an object with a few options that allow us to customize how certain sections are rendered.

I recommend you again to read the official documentation, in the options object we have renderNode and renderMark, nodes are the big blocks of content with text or assets, and marks are inline text modifications like italics, bold, code, etc. The plugin also exports ENUMS containing all the types like BLOCKS, INLINES and MARKS.

Inside renderNode, we create an object and we use the ENUMS as property names, first we will render the assets (images) because they are not supported by default.

Images

Images are represented as EMBEDDED_ASSET in the code, so we use that value of the ENUM BLOCKS, the value's property will be a function that receives the actual node and return a JSX with the custom component we want to render.

For performance reasons, we don't receive all the asset data on the body raw property, if you inspect, we only got a few of them, but we can query all the references' information, including the gatsbyImageData, and the contentful_id for that asset:

GraphQL query

1body {
2        raw
3        references {
4          gatsbyImageData(layout: CONSTRAINED, placeholder: BLURRED)
5          contentful_id
6          title
7          file {
8            url
9          }
10        }
11      }

Now we can make a match with every asset node in the raw body and the reference. We find in the references we got, a ref with the contentful_id equals the node asset id:

1[BLOCKS.EMBEDDED_ASSET]: node => {
2        const id = node.data.target.sys.id;
3        const resource = contentfulBlogPost.body.references.find(
4          ref => ref.contentful_id === id
5        );
6        return (
7          <GatsbyImage
8            image={resource.gatsbyImageData}
9            alt={resource.title}
10            style={{ display: 'flex', justifyContent: 'center' }}
11          />
12        );
13      },

I recommend you to validate in there, just in case you don't find any reference that matches 😜. If you are curious, you will see that we are using the GatsbyImage component!

And that's it! well... there are those two parts of the code in options that need to be explained, but I think the main objective of the tutorial is reached.

Conclusion

We have created our blog using GatsbyJS and Contentful CMS as data source. As you can see, it is pretty easy to setup, and now that you have all done, every time you want to create a new post, you just only go to Contentful and write the post! the blog will be automatically updated with every build. Also, Contentful has webhooks so you can implement automatic builds everytime the content changes on your Contentful workspace.

Here is the source code on Github if you want it!