loading...
Cover image for NextJS, Contentful CMS, GraphQL, oh my!
Hack4Impact

NextJS, Contentful CMS, GraphQL, oh my!

bholmesdev profile image Ben Holmes ・12 min read

We built the new Hack4Impact.org in a month-long sprint once we had designs in hand. To move this fast, we needed to make sure that we used tools that played to our strengths, while setting us up for success when designers and product managers want to update our copy. As the title excitedly alludes to, NextJS + Contentful + GraphQL was the match for us!

No, this post won't help you answer what tools should I use to build our site's landing page? But it should get your gears turning on:

  • How to access Contentful's GraphQL endpoints (yes, they're free-to-use now!) πŸ“
  • How to talk to GraphQL server + debug with GraphiQL πŸ“Ά
  • How we can roll query results into a static NextJS site with getStaticProps πŸ—ž
  • Going further with rich text πŸš€

Onwards!

Wait, why use these tools?

Some readers might be scoping whether to adopt these tools at all. As a TLDR:

  1. NextJS was a great match for our frontend stack, since we were already comfortable with a React-based workflow and wanted to play to our strengths. What's more, NextJS is flexible enough to build some parts of your website statically, and other parts dynamically (i.e. with serverside rendering). This is pretty promising as our landing site expands, where we might add experiences that vary by user going forward (admin portals, nonprofit dashboards, etc).
  2. Contentful is one of the more popular "headless CMSs" right now, and it's easy to see why. Content types are more than flexible enough for our use cases, and the UI is friendly enough for designers and product managers to navigate confidently. It thrives with "structured content" in particular which is great for static sites like ours! Still, if you're looking for a simplified, key-value store for your copy, there are some shiny alternatives to consider.
  3. GraphQL is the perfect pairing for a CMS in our opinion. You simply define the "shape" of the content you want (with necessary filtering and sorting), and the CMS responds with the associated values. We'll dive into some code samples soon, but it's much simpler than a traditional REST endpoint.

Note: There's roughly 10 billion ways to build a static site these days (citation needed), with another 10 billion blog posts on how to tackle the problem. So don't take these reasons as prescriptive for all teams!

Setting up our Contentful environment

Let's open up Contentful first. If you're 100% new to the platform, Contentful documents a lot of core concepts over here to get up to speed on "entries" and "content models."

When you're feeling comfortable, whip up a new workspace and create a new content model of your choosing. We'll use our "Executive Board Member" model as an example here.

Contentful model example

Once you've saved this model, go and make some content entries in the "Content" panel. We'll be pulling these down with GraphQL later, so I recommend making more than 1 entry to demo sorting and filtering! You can filter by your content type for a sanity check:

Contentful entry example, filtered by "Executive Board Member"

Before moving on, let's get some API keys for our website to use. Just head to "Settings > API keys" and choose "Add API key" in the top right. This should allow you to find two important variables: a Space ID and a Content Delivery API access token. You'll need these for some important environment variables in your local repo.

Whipping up a basic NextJS site

If you already have a Next project to work off of, great! Go cd into that thing now. Otherwise, it's super easy to make a NextJS project from scratch using their npx command:

npx create-next-app dope-contentful-example
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Note: You can optionally include the --use-npm flag if you want to ditch Yarn. By default, Next will set up your project with Yarn if you have it installed globally. It's your prerogative though!

You may have found a "NextJS + Contentful" example in the Next docs as well. Don't install that one! We'll be using GraphQL for this demo, which has a slightly different setup.

Now, just cd into your new NextJS project and create a .env file with the following info:

NEXT_PUBLIC_CONTENTFUL_SPACE_ID=[Your Space ID from Contentful]
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN=[Your Content Delivery API access token from Contentful]
Enter fullscreen mode Exit fullscreen mode

Just fill these in with your API keys and you're good to go! Yes, the NEXT_PUBLIC prefix is necessary for these to work. It's a little verbose, but it allows Next to pick up your keys without the hassle of setting up, say, dotenv.

Fetching some GraphQL data

Alright, so we've set the stage. Now let's fetch our data!

We'll be using GraphiQL to view our "schemas" with a nice GUI. You can install this tool here, using either homebrew on MacOS or the Linux Subsystem on Windows. Otherwise, if you want to follow along as a curl or Postman warrior, be my guest!

Opening the app for the first time, you should see a screen like this:

GraphiQL welcome screen

Let's point GraphiQL to our Contentful server. You can start by entering the following URL, filling in [Space ID] with your API key from the previous section:

https://graphql.contentful.com/content/v1/spaces/[Space ID]
Enter fullscreen mode Exit fullscreen mode

If you try to hit the play button ▢️ after this step, you should get an authorizaton error. That's because we haven't passed an access token with our query!

To fix this, click Edit HTTP Headers and create a new header entry like so, filling in [Contentful access token] with the value from your API keys:

Edit HTTP headers screen. Make sure Header name = "authorizaton" and Header value = "Bearer [Contentful access token]"

After saving, you should see some info appear in your "Documentation Explorer." If you click on the query: Query link, you'll see an overview of all your Content models from Contentful.

Schema explorer overview

Neat! From here, you should see all of the content models you created in your Contentful space. You'll notice there's a distinction between individual entries and a "collection" (i.e. executiveBoardMember vs. executiveBoardMemberCollection). This is because each represents a different query you can perform in your API call. If this terminology confuses you, here's a quick breakdown:

  • items highlighted in blue represent queries you can perform. These are similar to REST endpoints, as they accept parameters and return a structured response. The main difference is being able to nest queries within other queries to retrieve nested content. We'll explore this concept through example.
  • items highlighted in purple represent parameters you can pass for a given query. As shown in the screenshot above, you can query for an individual ExecutiveBoardMember based on id or locale (we'll ignore the preview param for this tutorial), or query for a collection / list of members (ExecutiveBoardMemberCollection) filtering by locale, amount of entries (limit), sort order, and a number of other properties.
  • items highlighted in yellow represent the shape of the response you receive from a given query. This allows you to pull out the exact keys of a given content entry that you want, with type checking built-in. Each of these are hyperlinks, so click on them to inspect the nested queries and response types!

Getting our hands dirty

Let's jump into an example. First, let's just get the list of names and emails for all "Executive Board Member" entries. If you're following along with your own Contentful space, just pick a few text-based keys you want to retrieve from your content model. Since we're looking for multiple entries, we'll use the executiveBoardMemberCollection query for this.

Clicking into the yellow ExecutiveBoardMemberCollection link (following the colon : at the end of the query), we should see a few options we're free to retrieve: total, skip, limit, and items. You'll see these 4 queries on every collection you create, where items represents the actual list of items you're hoping to retrieve. Let's click into the response type for items to see the shape of our content:

Schema for our Executive Board Member content model

This looks really similar to the content model we wrote in Contentful! As you can see, we can query for any of these fields to retrieve a response (most of them being strings in this example).

Writing your first query

Alright, we've walked through the docs and found the queries we want... so how do we get that data?

Well, the recap, here's the basic skeleton of info we need to retrieve:

executiveBoardMemberCollection -> query for a collection of entries
  items -> retrieve the list items
    name -> retrieve the name for each list item
    email -> and the email
Enter fullscreen mode Exit fullscreen mode

We can convert this skeleton to the JSON-y syntax GraphQL expects:

{
  executiveBoardMemberCollection {
    items {
      name
      email
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

... and enter this into GraphiQL's textbox and hit play ▢️

Entering our query into GraphiQL, with response data appearing on the right

Boom! There's all the data we entered into Contentful, formatted as a nice JSON response. If you typed your query into GraphiQL by hand, you may have noticed some nifty autocomplete as you went. This is the beauty of GraphQL: since we know the shape of any possible response, it can autofill your query as you go! πŸš€

Applying filters

You may have noticed some purple items in parenthesis while exploring the docs. These are parameters we can pass to Contentful to further refine our results.

Let's use some of the collection filters as an example; say we only want to retrieve board members that have a LinkedIn profile. We can apply this filter using the where parameter:

Applying a filter using "where." Shows how GraphiQL autocomplete will reveal potential parameter values.

As you can see, where accepts an object as a value, where we can apply a set of pre-determined filters from Contentful. As we type, we're greated with a number of comparison options that might remind you of SQL, including the exists operator for nullable values. You can find the complete list of supported filters in the docs off to the right, but autocomplete will usually take you to the filter you want πŸ’ͺ

In our case, our query should look something like this:

executiveBoardMemberCollection(where: {linkedIn_exists: true}) { ... }
Enter fullscreen mode Exit fullscreen mode

...and our result should filter out members without a LinkedIn entry.

Pulling our data into NextJS

Alright, we figured out how to retrieve our data. All we need is an API call on our NextJS site and we're off to the races 🏎

Let's pop open a random page component in our /pages directory and add a call to getStaticProps():

// pages/about
export async function getStaticProps() {
  return {
    props: {
      // our beautiful Contentful content
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

If you're unfamiliar with Next, this function allows you to pull in data while your app is getting built, so you access that data in your component's props at runtime. This means you don't have to call Contentful when your component mounts! The data is just... there, ready for you to use πŸ‘

πŸ’‘ Note: There's a distinction between getting these props "on every page request" versus retrieval "at build time." For a full rundown of the difference, check out the NextJS docs.

Inside this function, we'll make a simple call to Contentful using fetch (but feel free to use axios if that's more your speed):

export async function getStaticProps() {
  // first, grab our Contentful keys from the .env file
  const space = process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID;
    const accessToken = process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN;

  // then, send a request to Contentful (using the same URL from GraphiQL)
  const res = await fetch(
      `https://graphql.contentful.com/content/v1/spaces/${space}`,
      {
        method: 'POST', // GraphQL *always* uses POST requests!
        headers: {
          'content-type': 'application/json',
          authorization: `Bearer ${accessToken}`, // add our access token header
        },
        // send the query we wrote in GraphiQL as a string
        body: JSON.stringify({
          // all requests start with "query: ", so we'll stringify that for convenience
          query: `
          {
            executiveBoardMemberCollection {
              items {
                name
                email
              }
            }
          }
                `,
        },
      },
    );
    // grab the data from our response
    const { data } = await res.json()
  ...
}
Enter fullscreen mode Exit fullscreen mode

Woah, that's a lot going on! In the end, we're just re-writing the logic that GraphiQL does under the hood. Some key takeaways:

  1. We need to grab our API keys for the URL and authorization header. This should look super familiar after our GraphiQL setup!
  2. Every GraphQL query should be a POST request. This is because we're sending a body field to Contentful, containing the "shape" of the content we want to receive.
  3. Our query should start with the JSON key { "query": "string" }. To make this easier to type, we create a JavaScript object starting with "query" and convert this to a string.

🏁 As a checkpoint, add a console.log statement to see what our data object looks like. If all goes well, you should get a collection of contentful entries!

Now, we just need to return the data we want as props (in this case, the items in our executiveBoardMemberCollection):

export async function getStaticProps() {
    ...
  return {
    props: {
        execBoardMembers: data.executiveBoardMemberCollection.items,
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

...and do something with those props in our page component:

function AboutPage({ execBoardMembers }) {
  return (
    <div>
        {execBoardMembers.map(execBoardMember => (
        <div className="exec-member-profile">
            <h2>{execBoardMember.name}</h2>
          <p>{execBoardMember.email}</p>
        </div>
      ))}
    </div>
  )
}

export default AboutPage;
Enter fullscreen mode Exit fullscreen mode

Hopefully, you'll see your own Contentful entries pop onto the page πŸŽ‰

Writing a reusable helper functions

This all works great, but it gets pretty repetitive generating this API call on every page. That's why we wrote a little helper function on our project to streamline the process.

In short, we're gonna move all our API call logic into a utility function, accepting the actual "body" of our query as a parameter. Here's how that could look:

// utils/contentful
const space = process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID;
const accessToken = process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN;

export async function fetchContent(query) {
  // add a try / catch loop for nicer error handling
  try {
    const res = await fetch(
      `https://graphql.contentful.com/content/v1/spaces/${space}`,
      {
        method: 'POST',
        headers: {
          'content-type': 'application/json',
          authorization: `Bearer ${accessToken}`,
        },
        // throw our query (a string) into the body directly
        body: JSON.stringify({ query }),
      },
    );
    const { data } = await res.json();
    return data;
  } catch (error) {
    // add a descriptive error message first,
    // so we know which GraphQL query caused the issue
    console.error(`There was a problem retrieving entries with the query ${query}`);
    console.error(error);
  }
}

Enter fullscreen mode Exit fullscreen mode

Aside from our little catch statement for errors, this is the same fetch call we were making before. Now, we can refactor our getStaticProps function to something like this:

import { fetchContent } from '@utils/contentful'

export async function getStaticProps() {
  const response = await fetchContent(`
        {
            executiveBoardMemberCollection {
                items {
                name
                email
            }
          }
      }
  `);
  return {
    props: {
      execBoardMembers: response.executiveBoardMemberCollection.items,
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

...and we're ready to make Contentful queries across the site ✨

Aside: use the "@" as a shortcut to directories

You may have noticed that import statement in the above example, accessing fetchContent from @utils/contentful. This is using a slick webpack shortcut under the hood, which you can set up too! Just create a next.config.json that looks like this:

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@utils/*": ["utils/*"],
      "@components/*": ["components/*"]
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, you can reference anything inside /utils using this decorator. For convenience, we added an entry for @components as well, since NextJS projects tend to pull from that directory a lot πŸ‘

Using a special Contentful package to format rich text

Chances are, you'll probably set up some rich text fields in Contentful to handle hyperlinks, headers, and the like. By default, Contentful will return a big JSON blob representing your formatted content:

GraphiQL response when querying for rich text

...which isn't super useful on its own. To convert this into some nice HTML, you'll need a special package from Contentful:

npm i --save-dev @contentful/rich-text-html-renderer
Enter fullscreen mode Exit fullscreen mode

This will take in the JSON object and (safely) render HTML for your component:

import { documentToHtmlString } from '@contentful/rich-text-html-renderer';

function AboutPage(execBoardMember) {
  return (
    <div
    dangerouslySetInnerHTML={{
    __html: documentToHtmlString(execBoardMember.description.json),
    }}></div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Yes, using dangerouslySetInnerHTML is pretty tedious. We'd suggest making a RichText component to render your HTML.

Check out our project to see how we put it together πŸš€

If you're interested, we deployed our entire project to a CodeSandbox for you to explore!

Head over here to see how we retrieve our executive board members on our about page. Also, check out the utils/contentful directory to see how we defined our schemas using TypeScript.

Our repo is 100% open as well, so give it a ⭐️ if this article helped you!

Thanks for reading! If this article was helpful...

I love writing about this sort of stuff πŸ‘¨β€πŸ’»

❀️ First, please check out the incredible work Hack4Impact is cooking up!

🐦 Follow my Twitter for random web dev tips and articles I find cool

πŸ“— Follow my personal blog for new posts every 2-3 weeks

Discussion

pic
Editor guide
Collapse
bairrada97 profile image
bairrada97

Question, on contentful website It says theres a limit of 2M calls, so If you have 100k users online and they all refresh the page constantly during 24h, that means you'll reach the limit?

Collapse
bholmesdev profile image
Ben Holmes Author

Fair question! So if you use a function like getStaticProps, you’re only calling the Contentful API when you first build the application. This means, after you’ve deployed your built website, the Contentful data exists as static JSON in your JavaScript bundle. So, whenever a user visits your site, they won’t make another call to Contentful to retrieve that data. So you could make 1 call and serve 100k users! You’ll only make future calls when you redeploy your site, which you can automatically trigger on content changes using a webhook

Collapse
bairrada97 profile image
bairrada97

Thats awesome.
But my project its a little different. I think I can't redeploy my site everytime my data changes. I have a third party API that needs to be updated every 15seconds, but for this I dont need Contentful.
Where I need contentful Is on grab static data, for example I have a calendar that when I click on a day It grabs data from API related to the day that I selected, but for the past days the data will not change anymore, will be static so If I click on a day from last month I want to grab the info from contentful and not from third party api, but at same time I dont want my site to be reployed every 15s

Thread Thread
bholmesdev profile image
Ben Holmes Author

Hm okay, so it sounds like you have 2 different data sources (3rd party API that changes often, Contentful data that doesn't change as often). So, to be clear, are you trying to funnel data from this API into Contentful when it becomes "static?" If so, I would recommend caching this data with Next's getServerSideProps and avoiding Contentful together. Lmk if I'm misunderstanding your comment though.

Thread Thread
bairrada97 profile image
bairrada97

Exactly, I want to send the data to contentful when It doesnt change anymore. Im using Nuxt instead of next but It probably has a similar option. But you recommend caching data using getServerSideProps and not use contentful at all?

Thread Thread
bholmesdev profile image
Ben Holmes Author

Right. That's mainly because Contentful is a CMS, so it's meant to be your content editor as well as your storage solution. In your case, it sounds like you'll never update these entries again once they've been saved, so you're probably just looking for a data storage solution. I haven't investigated server-side caching with Next, but it looks like they have an example you can clone right here. Your idea definitely isn't impossible! It was just be a lot of work to set up and maintain. You'd likely run into free tier limits as well, since you'll be writing a lot of entries at a time.

Thread Thread
bairrada97 profile image
bairrada97

Exactly, I have to investigate more, Its a thing that I plan to do later in my project. Because the data will update every 15s but once the day is over that same data will not be updated ever again, and a new day starts with fresh data to be updated every 15s again. And I dont want to use headless CMS because If I have 10k online users and every single of them start to click on every day from last 5y, I'll reach the limit of calls.
Since Im using Vue 3, maybe I can create a db on server with the power of reactivity and once day is over I make a post to that db with the data , and if the user clicks on a day that is lesser than today will grab data from db, if not will grab data from third party api.
And maybe with service workers or with SS cache I can make it work. If only 1 user visits that page from 3y ago, 10k users will see instant on browser because Its already in cache.

Well this is what Im thinking dont know if will work or not

Collapse
amxn profile image
Mohammed Ameen

Hey Ben, great write up! I have a question regarding multi-level nav on the NextJS app fetched through Contentful - What should the content type be like (multi-level navigation) and how would we resolve to it on the Nextjs side.

Thank you!

Collapse
dewaleolaoye profile image
Adewale Olaoye

Thanks for sharing, is this available on Github?