DEV Community

Cover image for Using a headless CMS with React
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Using a headless CMS with React

Written by Ovie Okeh✏️

I would like to build a blog on my personal React website, but I have some questions. Where do I store the images and content for each post? How do I store the content? Sure, I could hardcode each post, upload the images to a CDN and manually link to it, but would that be able to scale to 100 posts? 200?

What I need is a content management system (CMS) like WordPress, but I’m quite happy with my React site and I don’t want to switch. Oh, I know — I’ve heard some colleagues talking about headless content management systems. Could that be what I need?

OK, I did some research and discovered that a headless CMS is exactly what I need. It gives me an interface to write my blog posts along with the ability to deliver it anywhere I want. That sounds good, but which one do I go for? I know there are a lot of options out there.

I asked around and Contentful was recommended a lot, so I guess it’s worth a try. This is what I plan to do:

  • Set up Contentful to host my blog posts
  • Upload and publish some posts
  • Pull in my blog posts to my React app
  • Serve it to my imaginary readers

LogRocket Free Trial Banner

Setting up Contentful

Hmm… So I read a bit more about Contentful on the official website, and it claims that it’s not a traditional headless CMS. It’s a “Content Infrastructure” and apparently will give me more flexibility on how to structure my content.

If you ask me, though, I think it’s just a variant of a headless CMS because it satisfies the criteria for being one. If it allows you to write once and deliver anywhere, then it’s a headless CMS to me. 🤷

Anyway, I signed up for a free account and it turns out that setting it up was really easy. After I clicked on signup, I was greeted with this page:

Contentful Get Started Page
I do both, so what should I select? 🤔

I decided to Explore content modeling , so I clicked on the left button and a sample project was created for me. I’m an explorer, though, so I decided to create my own project from scratch. Projects are called spaces in Contentful, btw.

I clicked on the sidebar to my left and clicked on the + Create space button, which opened the modal below:

Space Type Modal
Free stuff never hurt anyone.

I then had to select a name for my new project, so I went with something creative because I’m just drowning in creativity.

Choose A Name Modal
Pick a better space name, I dare you.

And finally, to confirm that I indeed wanted a new space, I was presented with the last and final modal to conquer.

Confirmation Modal

OK, I now have a new space created. It’s time to create my blog posts.

Creating a blog post

Before I could create a blog post, I had to create something called a Content Model, which is simply the structure of how a type of content should look. I’m choosing to think of this as a schema for my content.

I had to come up with the structure of how the posts should look, and thankfully, it was pretty easy. It was as simple as writing down what data each post needs and the type of that data. In my case, these are the following pieces of data required, along with the data type:

  • Title – Short text
  • Slug – Short text
  • Description – Long text
  • Featured Image – An image
  • Date – Date & time
  • Body – Long text

After writing down the required pieces of data, I went ahead and created my Content Model in Contentful. In the my blog space I just created, I clicked on Content model on the top navigation menu and clicked on Add content type in the following page.

A modal popped up, and I filled in the name for my new Content Model. I just called it “Blog Post” and started adding the fields I listed above. By the time I was done adding all the different fields, I had something similar to the below:

Content Model Modal
Easy peasy.

Now that I had my blog post content model (or schema, if you prefer) set up, I decided it was time to add the actual blog posts that I would pull into my React app.

Still in the my blog space, I clicked on Content on the top navigation menu and clicked on Add Blog Post. If you’re following along and you named your content model something else, Add Blog Post might be something different.

Anyway, clicking on that button took me to a page where I could write and edit my blog posts like so:

Write And Edit Blog Post Page

This is why I needed a CMS in the first place — a place to write and edit my blog posts so that I could deliver them anywhere I like. I went ahead and added three dummy posts so that I would have something to pull into my React app.

Here’s how my list of blog posts looked by the time I was done:

List Of Blog Posts

OK, this is has been going well, and I feel it’s time to recap what I’ve learned so far:

  • A headless content management system allows me to create my content once and deliver it anywhere I like
  • Contentful is one such CMS, with more advanced functionality like well-structured schemas for my content
  • I can create and edit my content in a variety of formats, including Markdown and Rich Text
  • Contentful also provides a CDN for storing and hosting any media I choose to upload in my blog posts

Integrating Contentful into a React app

Before I could integrate Contentful into my app, I actually had to create the app first. I wanted my blog to look exactly like the one below.

So what are the different components for this app?

  • An App.jsx component to handle routing to the different pages
  • A Posts.jsx component to display the list of posts on the site
  • A SinglePost.jsx component to display a single post

Well, it turns out not a whole lot. Of course, if you have your own personal site and are looking to follow this tutorial, you might have many more components, but for this case, that’s all I needed.

Building the app

I ran the following scripts to set up my project and install the required dependencies:

mkdir react-contentful && cd react-contentful
npm init -y
npm i --save react react-dom react-router-dom react-markdown history contentful
npm i --save-dev parcel-bundler less
Enter fullscreen mode Exit fullscreen mode

There are two particularly important packages I just installed: react-markdown and contentful.

react-markdown allows me to parse Markdown content into HTML tags. I needed it because I’m storing my post content as “Long text” in Contentful, and this means my post body will be in Markdown.

contentful is the official Node package from Contentful that will allow me to interact with its API. I needed it to retrieve my content from Contentful. Every other package is self-explanatory.

Creating my files

After installing all the required dependencies, I went ahead and created the different files and folders I needed for this project. I’m going to leave out the content of some of the files from this tutorial, but I’ll add links so you can copy them and follow along.

  • Run this script to create all the required folders:
mkdir public src src/components src/custom-hooks src/components/{posts,single-post}
Enter fullscreen mode Exit fullscreen mode
  • Run this script to create all the required files:
touch public/index.html public/index.css src/{index,contentful}.js
Enter fullscreen mode Exit fullscreen mode
  • Run this script to create all the components:
touch src/components/App.jsx src/components/helpers.js src/components/posts/Posts.jsx src/components/posts/Posts.less src/components/single-post/SinglePost.jsx src/components/single-post/SinglePost.less
Enter fullscreen mode Exit fullscreen mode
  • Run this script to create all the custom Hooks:
touch src/custom-hooks/{index,usePosts,useSinglePost}.js
Enter fullscreen mode Exit fullscreen mode

I will not go through the code for the following files because they’re not essential to this tutorial:

Populating the files

Now that I had my project structure ready with all the required files and folders, I started writing code, and I’ll start with the most essential pieces first.

src/contentful.js

const client = require('contentful').createClient({
  space: '<my_space_id>',
  accessToken: '<my_access_token>'
})

const getBlogPosts = () => client.getEntries().then(response => response.items)

const getSinglePost = slug =>
  client
    .getEntries({
      'fields.slug': slug,
      content_type: 'blogPost'
    })
    .then(response => response.items)

export { getBlogPosts, getSinglePost }
Enter fullscreen mode Exit fullscreen mode

So I started with the code that interacts with Contentful to retrieve my blog posts.

I wanted to query Contentful for my content, so I went through the contentful package docs and discovered that I needed to import the package and pass it a config object containing a space ID and my access token.

Getting this information was trivial and all I had to do was follow the instructions on the Contentful docs.

After getting my space ID and my access token, I required the contentful package and called the createClient method with a config object containing my credentials. This gave me an object, client, that allowed me to interact with Contentful.

So to recap, I wanted to retrieve:

  • All my blog posts
  • A single blog post by its slug

For retrieving all my blog posts, I created a function, getBlogPosts, that did this for me. Inside this function, I called client.getEntries(), which returns a Promise that eventually resolves to a response object containing items, which is my array of blog posts.

For retrieving a single blog post, I created a function called getSinglePost, which takes in a “slug” argument and queries Contentful for any post with that slug. Remember that “slug” is one of the fields I created in my blog post content model, and that’s why I can reference it in my query.

Inside the getSinglePost function, I called client.getEntries() again, but this time, I passed a query object specifying that I wanted any content that:

  • Has a slug matching the “slug” argument
  • Is a blog post

Then, at the end of the file, I exported both functions so I could make use of them in other files. I created the custom Hooks next.

custom-hooks/usePosts.js

import { useEffect, useState } from 'react'

import { getBlogPosts } from '../contentful'

const promise = getBlogPosts()

export default function usePosts() {
  const [posts, setPosts] = useState([])
  const [isLoading, setLoading] = useState(true)

  useEffect(() => {
    promise.then(blogPosts => {
      setPosts(blogPosts)
      setLoading(false)
    })
  }, [])

  return [posts, isLoading]
}
Enter fullscreen mode Exit fullscreen mode

The usePosts Hook allows me to retrieve my blog posts from Contentful from the Posts.jsx component.

I imported three modules into this file:

  1. useEffect: I needed this to update the custom Hook’s state
  2. useState: I needed this to store the list of blog posts as well as the current loading state
  3. getBlogPosts: This function allowed me to query Contentful for my blog posts

After importing all the required modules into this file, I kicked off the call to fetch my blog posts by calling the getBlogPosts() function. This returns a Promise, which I stored in the promise variable.

Inside the usePosts() Hook, I initialized two state variables:

  1. posts, to hold the list of blog posts
  2. isLoading, to hold the current loading state for the blog posts fetch request

Then, in the useEffect call, I resolved the Promise I created earlier and then updated the posts state variable with the new blog posts data. I also set the loading state to be false after this was done.

At the end of this Hook, I returned an array containing the posts and the isLoading variables.

custom-hooks/useSinglePost.js

import { useEffect, useState } from 'react'

import { getSinglePost } from '../contentful'

export default function useSinglePost(slug) {
  const promise = getSinglePost(slug)

  const [post, setPost] = useState(null)
  const [isLoading, setLoading] = useState(true)

  useEffect(() => {
    promise.then(result => {
      setPost(result[0].fields)
      setLoading(false)
    })
  }, [])

  return [post, isLoading]
}
Enter fullscreen mode Exit fullscreen mode

The useSinglePost custom Hook is very similar to the usePosts Hook, with a few minor exceptions.

Unlike usePosts, where I kicked off the call to getBlogPosts outside of the Hook, I made the call (but to getSinglePost()) inside the useSinglePost Hook. I did this because I wanted to pass in the “slug” argument to the getSinglePost function, and I couldn’t do that if it was invoked outside the custom Hook.

Moving on, I also had the same state variables to hold the single post being retrieved, as well as the loading state for the request.

In the useEffect call, I resolved the Promise and updated the state variables as appropriate.

I also returned an array containing the post and the isLoading state variables at the end.

components/App.jsx

import React from 'react'
import { Router, Switch, Route } from 'react-router-dom'
import { createBrowserHistory } from 'history'

import Posts from './posts/Posts'
import SinglePost from './single-post/SinglePost'

export default function App() {
  return (
    <Router history={createBrowserHistory()}>
      <Switch>
        <Route path="/" exact component={Posts} />
        <Route path="/:id" component={SinglePost} />
      </Switch>
    </Router>
  )
}
Enter fullscreen mode Exit fullscreen mode

App.jsx is the root component responsible for routing the user to the correct page.

I imported a bunch of required dependencies. I also needed a refresher on how React Router works, so I went through this short article.

components/posts/Posts.jsx

So now that I had all my custom Hooks and querying functions setup, I wanted to retrieve all my blog posts and display them in a grid, like so:

Article Preview Grid On Blog Homepage
This design deserves an Awwward.

I started off with a bunch of dependency imports, among which is the usePosts custom Hook for fetching all my blog posts from Contentful. I also created a nice little helper called readableDate, which helped me parse the date the article was published into a user-friendly format.

import React from 'react'
import { Link } from 'react-router-dom'

import { usePosts } from '../../custom-hooks/'
import { readableDate } from '../helpers'
import './Posts.less'

...continued below...
Enter fullscreen mode Exit fullscreen mode

I created the component next. It’s a simple functional component without any state variables to manage or keep track of.

Right at the beginning, I made use of the usePosts Hook to get my posts and the loading state. Then I defined a function, renderPosts, to iterate over the list of blog posts and returned a bunch of JSX for each post.

Inside this function, I checked the loading state first. If the request is still loading, it returns the loading message and ends execution there. Otherwise, it maps over the array of posts, and for each one, returns a <Link /> element.

This Link element will redirect my readers to the slug of whatever post they click on. Inside this link element, I also rendered some important information like the featured image of the article, the date it was published, the title, and a short description.

Finally, in the return statement of the Posts component, I called the renderPosts() function.

...continuation...
export default function Posts() {
  const [posts, isLoading] = usePosts()

  const renderPosts = () => {
    if (isLoading) return <p>Loading...</p>

    return posts.map(post => (
      <Link
        className="posts__post"
        key={post.fields.slug}
        to={post.fields.slug}
      >
        <div className="posts__post__img__container">
          <img
            className="posts__post__img__container__img"
            src={post.fields.featuredImage.fields.file.url}
            alt={post.fields.title}
          />
        </div>

        <small>{readableDate(post.fields.date)}</small>
        <h3>{post.fields.title}</h3>
        <p>{post.fields.description}</p>
      </Link>
    ))
  }

  return (
    <div className="posts__container">
      <h2>Articles</h2>

      <div className="posts">{renderPosts()}</div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

So, to recap, here’s what I did in this component:

  • I called the usePosts() custom Hook. This returns two variables, posts and isLoading. posts is either going to be empty or contain the list of blog posts on my Contentful space. isLoading is either true or false, depending on whether the request to fetch the blog posts is still pending
  • I defined a renderPosts() function that will either render a loading message to the DOM or render my blog posts. It checks the isLoading variable to determine whether the blog posts are ready and then renders the appropriate content to the DOM
  • In the return statement, I returned a bunch of JSX along and called renderPosts()

Moving on to the next component.

components/single-post/SinglePost.jsx

I also needed to render single blog posts, and to do this, I needed a SinglePost component, which should look like this:

Example Of A Single Post
Interesting post.

Again, I started off with a bunch of dependency imports, as usual:

import React from 'react'
import { Link, useParams } from 'react-router-dom'
import MD from 'react-markdown'

import { useSinglePost } from '../../custom-hooks'
import { readableDate } from '../helpers'
import './SinglePost.less'
Enter fullscreen mode Exit fullscreen mode

There are a couple of new, unfamiliar imports here:

  • useParams: This will allow me to read the dynamic route parameters from React Router
  • MD: This will help me convert my Markdown content to HTML and render it

Apart from the new ones, I also imported the useSinglePost custom Hook as well as the readableDate helper.

Next, I created the actual component.

...continued...
export default function SinglePost() {
  const { id } = useParams()
  const [post, isLoading] = useSinglePost(id)

  const renderPost = () => {
    if (isLoading) return <p>Loading...</p>

    return (
      <>
        <div className="post__intro">
          <h2 className="post__intro__title">{post.title}</h2>
          <small className="post__intro__date">{readableDate(post.date)}</small>
          <p className="post__intro__desc">{post.description}</p>

          <img
            className="post__intro__img"
            src={post.featuredImage.fields.file.url}
            alt={post.title}
          />
        </div>

        <div className="post__body">
          <MD source={post.body} />
        </div>
      </>
    )
  }
...continued below...
Enter fullscreen mode Exit fullscreen mode

Before I continue, I would like to talk a little bit about how useParams works. In App.jsx, I had the following snippet of code:

<Route path="/:id" component={SinglePost} />
Enter fullscreen mode Exit fullscreen mode

This simply routes any request that matches the URL pattern passed to path to the SinglePost component. React Router also passes some additional props to the SinglePost component. One of these props is a params object that contains all the parameters in the path URL.

In this case, params would contain id as one of the parameters because I explicitly specified id in the path URL for this particular route. So, if I navigated to a URL like localhost:3000/contentful-rules, params would look like this:

{
  id: 'contentful-rules'
}
Enter fullscreen mode Exit fullscreen mode

This is also where useParams comes into play. It will allow me to query the params object without having to destructure it from the component’s props. I now have a way to grab whatever slug is in the current URL.

OK, back to the component. Now that I had a way to get the slug of whichever article was clicked on, I was now able to pass the slug down to the useSinglePost custom Hook, and I was able to get back the post with that slug as well as the loading state for the request to fetch the post.

After getting the post object and the loading state from the useSinglePost Hook, I defined a renderPost function that will either render a loading message to the DOM or the actual post, depending on the loading state.

Also notice that towards the end of the snippet, I have this line of code:

<MD source={post.body} />
Enter fullscreen mode Exit fullscreen mode

This is the React Markdown component that I need to parse my Markdown post body into actual HTML that the browser recognizes.

...continued...

  return (
    <div className="post">
      <Link className="post__back" to="/">
        {'< Back'}
      </Link>

      {renderPost()}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Finally, I have the return statement to render my data from this component. I added a link back to the homepage so that my users would be able to go back to the homepage easily. After the link, I simply called the renderPost() function to render the post to the DOM.

To recap, here’s what I did in this component.

  • I called the useSinglePost() custom Hook. This returns two variables, post and isLoading. post will either be null or an object containing the post data. isLoading is either true or false, depending on whether the request to fetch the post is still pending
  • I defined a renderPost() function that will either render a loading message to the DOM or render the blog post. It checks the isLoading variable to determine whether the blog post is ready and then renders the appropriate content to the DOM
  • In the return statement, I returned a bunch of JSX along and called renderPost()

Putting it all together

After writing the code for all the components and adding the appropriate styling, I decided to run my project to see if it all worked. In my package.json, I added the following scripts:

"scripts": {
    "start": "parcel public/index.html",
    "build": "parcel build public/index.html --out-dir build --no-source-maps"
  },
Enter fullscreen mode Exit fullscreen mode

When I ran npm run start in my terminal, Parcel built my React app for me and served it over port 1234. Navigating to http://localhost:1234 on my browser displayed my app in all its glory, along with the blog posts.

I tried clicking on a single blog post and I was redirected to a page where I was able to read that blog post, so it seems that my little experiment with React and Contentful worked as I wanted it to.

Site Preview

I’m fully aware that this is not the best way to build something as simple as a static blog, though. There are much better options, like Next.js and Gatsby.js, that would make this process a whole lot easier and would actually result in a faster, more accessible blog by default.

But if your use case is simply to get your content from Contentful into your React app, then this guide should be helpful to you.


Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, tracking slow network requests and component load time, try LogRocket.

Alt Text

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.


The post Using a headless CMS with React appeared first on LogRocket Blog.

Top comments (0)