Hey Mike

Build a Static Blog with Next.js and Contentful — Part Two: Next.js

Published:

Welcome to the second part of this multi-part series about building a static generated blog with Next.js and Contentful. In Part One, the content models for Blog Post and Author were set up in Contentful and some dummy content was entered.

Prerequisites

Before continuing, you should have completed Part One, and then have the following accounts registered, and applications installed:

After the prerequisites have been met, continue on to the first step of bootstrapping a new project.

Bootstrap a Next.js Project

The first step will be to initialize a new project using npm. In a terminal window, perform the following:

npm init next-app nextjs-blog

Choose the "Default starter app" option when prompted. Next step is to deploy the new application to Vercel. Perform the following in your terminal window:

cd nextjs-blog
now

Selecting the default option for each prompt should be sufficient. When deployment is complete, the URL of the new deployment will be copied to your clipboard. You can then paste that into a browser window to see your project live on Vercel.

Deployed Bootstrapped project to Vercel

Next's new data fetching functions for static site generation

With the release of Next v9.3 there are few new data fetching functions geared towards static site generation. In previous versions, the standard way of fetching data before the page was rendered was to use the getInitialProps method. This method enables fetching data server-side, then rendering the requested view on the server based on those initial props. This is great but it means that a page using that function could not be prerendered at build time and thus opted out of Next's static optimization features.

The two new functions that enable developers to fetch data and render static pages at build time are getStaticProps and getStaticPaths.

You can read more about those two functions in Next's documentation but the tl;dr is that the former will fetch data for a given route at build time in order to render the static page, and the latter will define all of the routes that need to be rendered at build time in the case where dynamic routes are in use (e.g. /posts/{post.fields.slug}). The two work together in tandem to render all of the necessary static pages at build time.

Integrating the Contentful API

The next step is to install the Contenful JavaScript Delivery SDK. A package for rendering Markdown is also going to be needed so install React Markdown. Perform the following in a terminal window:

npm install --save contentful react-markdown

Upon successful installation, head over to the Contentful application in a browser to create the access tokens that Contentful requires to access the API. Go to Settings > API Keys and then click "Add API Key", or use the example key that Contentful has already created for you if you prefer.

API Keys

Make note of the Space ID and Content Delivery API access token. Ignore the Content Preview API access token for now. It provides access to the Content Preview API where unpublished content can be accessed for preview purposes. A later part of this tutorial will talk about using this in combination with Next's new Preview Mode feature to enable pre-publication content previews. But for now, just use the Content Delivery API access token.

Back in the Next.js project, create a file in the root folder called .env and make sure it is ignored in .gitignore. Inside that file, create the following environment variables:

CONTENTFUL_SPACE_ID=<insert your Space ID here>
CONTENTFUL_ACCESS_TOKEN=<insert your Access Token here>

Next, modify the pages/index.js file as follows:

import Head from "next/head"
import Layout from "../components/Layout"
import PostList from "../components/PostList"

export default function Index({ posts }) {
  return (
    <Layout>
      <PostList posts={posts} />
    </Layout>
  )
}

export async function getStaticProps() {
  // Create an instance of the Contentful JavaScript SDK
  const client = require("contentful").createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
  })

  // Fetch all entries of content_type `blogPost`
  const posts = await client
    .getEntries({ content_type: "blogPost" })
    .then((response) => response.items)

  return {
    props: {
      posts,
    },
  }
}

The changes made to the pages/index.js file reference a couple of new component files, so create them next. Create a folder called components off the project root.

The first component to create is components/PostList.js. This will iterate the list of blog posts that it receives in the posts prop and display the post title, the description, the date published, and a link to read the full post.

import React from "react"
import Link from "next/link"

export default function PostList({ posts = [] }) {
  return (
    <section>
      {posts.map((post) => (
        <article key={post.sys.id}>
          <header>
            <h1>
              <Link href={`/post/${post.fields.slug}`}>
                <a>{post.fields.title}</a>
              </Link>
            </h1>
            <small>
              <p>Published: {Date(post.fields.publishedDate).toString()}</p>
            </small>
          </header>
          <p>{post.fields.description}</p>
          <p>
            <Link href={`/post/${post.fields.slug}`}>
              <a>Continue reading »</a>
            </Link>
          </p>
        </article>
      ))}
      <style jsx>{`
        h1 {
          margin: 0 0 0.75rem;
          font-size: 2.5rem;
          font-weight: 400;
        }
        h1 a {
          text-decoration: none;
        }
        p {
          line-height: 1.75rem;
        }
        article {
          margin: 2rem 0;
        }
      `}</style>
    </section>
  )
}

Side note: One thing I've learned about Contentful is that the Content Delivery API does not supply its own firstPublishedAt date in the sys object. It's only available in the Content Management API. That's why we have added the publishedDate as a field on the blog post model. It would be so much better to use the firstPublishedAt date instead of having to create our own field so we can have a single source of truth. Contentful people, if you're reading this, take note!

Next up is the Layout component which will provide a top-level wrapper for the common elements that will get reused on all pages. Create the file components/Layout.js as follows:

import React from "react"
import Head from "next/head"
import Link from "next/link"

export default function Layout({ children }) {
  return (
    <div className="container">
      <Head>
        <title>My Next.js Static Blog</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <header>
        <h1>
          <Link href="/">
            <a>Next.JS + Contentful + Vercel = ❤️</a>
          </Link>
        </h1>
      </header>
      <main>{children}</main>
      <style jsx>{`
        .container {
          max-width: 42rem;
          margin: 0 auto;
          padding: 0 0.5rem;
          display: flex;
          flex-direction: column;
          justify-content: center;
          align-items: center;
        }

        header h1 a {
          color: #000;
          text-decoration: none;
        }

        main {
          padding: 2rem 0;
          display: flex;
          flex-direction: column;
          justify-content: flex-start;
        }
      `}</style>
      <style jsx global>{`
        html,
        body {
          padding: 0;
          margin: 0;
          font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
            Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
            sans-serif;
        }

        a,
        a:visited {
          color: blue;
        }

        * {
          box-sizing: border-box;
        }
      `}</style>
    </div>
  )
}

At this point, if the test blog post has been published (if it's not, go do that), we can run a local copy of the site as it stands.

Execute the following in your terminal:

now dev

Then in a browser, open up http://localhost:3000 and there should be something like this:

Blog Post List

That's pretty great so far, isn't it? Up next is building the post detail page.

The Post Detail Page

This section is going to go over building the post detail page which utilizes Next's Dynamic Routes feature. Start by creating the file pages/post/[slug].js, which at first glance may seem strange to have the file name wrapped in square brackets, but what that's doing is levereging Next's file-based routing to name the parameter that the router is going to capture from the URL. So when a request comes in to https://myawesomeblog/post/my-awesome-post, Next's router will take my-awesome-post as the [slug] parameter and provide that to the page component.

Create the pages/post/[slug].js file as follows:

import React from "react"
import Head from "next/head"
import Layout from "../../components/Layout"
import Post from "../../components/Post"

export default function Slug({ post }) {
  return (
    <Layout>
      <Head>
        <title>{post.fields.title}My Next.js Static Blog</title>
      </Head>
      <Post post={post} />
    </Layout>
  )
}

export async function getStaticProps(context) {
  // Create an instance of the Contentful JavaScript SDK
  const client = require("contentful").createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
  })

  // Fetch all results where `fields.slug` is equal to the `slug` param
  const result = await client
    .getEntries({
      content_type: "blogPost",
      "fields.slug": context.params.slug,
    })
    .then((response) => response.items)

  // Since `slug` was set to be a unique field, we can be confident that
  // the only result in the query is the correct post.
  const post = result.pop()

  // If nothing was found, return an empty object for props, or else there would
  // be an error when Next tries to serialize an `undefined` value to JSON.
  if (!post) {
    return { props: {} }
  }

  // Return the post as props
  return {
    props: {
      post,
    },
  }
}

export async function getStaticPaths() {
  // Create an instance of the Contentful JavaScript SDK
  const client = require("contentful").createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
  })

  // Query Contentful for all blog posts in the space
  const posts = await client
    .getEntries({ content_type: "blogPost" })
    .then((response) => response.items)

  // Map the result of that query to a list of slugs.
  // This will give Next the list of all blog post pages that need to be
  // rendered at build time.
  const paths = posts.map(({ fields: { slug } }) => ({ params: { slug } }))

  return {
    paths,
    fallback: false,
  }
}

Notice the fallback parameter at the end of the getStaticPaths function. It's required that a boolean value is returned for that key. You can read the docs for a full explanation but the gist of it is that when fallback is false, a 404 will be returned if the slug URL param is not found in the list of paths. The alternative will be covered in a later post in this series.

Next, create the components/Post.js component as follows:

import React from "react"
import Markdown from "react-markdown"
import Author from "./Author"

export default function Post({ post }) {
  return (
    <article>
      <header>
        <h1>{post.fields.title}</h1>
        <small>
          <p>Published: {Date(post.fields.publishedDate).toString()}</p>
        </small>
      </header>
      <section>
        <Markdown source={post.fields.body} escapeHtml={true} />
      </section>
      <footer>
        <Author author={post.fields.author} />
      </footer>
      <style jsx>{`
        header {
          margin-bottom: 2rem;
          padding-bottom: 2rem;
          border-bottom: 1px solid #949499;
        }
        header h1 {
          font-size: 3rem;
          margin-bottom: 1rem;
        }
        /*
        The section :global() selector is necessary to target the content
        that will be rendered by the Markdown component.
        */
        section :global(h1) {
          font-size: 2.5rem;
          margin-bottom: 1rem;
        }
        section :global(h2) {
          font-size: 2rem;
          margin-bottom: 1rem;
        }
        section :global(p) {
          line-height: 1.75rem;
          margin: 2rem 0;
        }
        section :global(img) {
          max-width: 100%;
        }
        section :global(blockquote) {
          border-left: 0.5rem solid #949499;
          margin-left: 0;
          padding: 0 2rem;
          color: #646469;
        }
        section :global(li) {
          margin: 1rem 0;
          line-height: 1.5rem;
        }
        section :global(hr) {
          border: none;
          background: #949499;
          height: 1px;
        }
      `}</style>
    </article>
  )
}

The Post component references another component called Author so that one is next. Create the file components/Author.js as follows:

import React from "react"

export default function Author({ author }) {
  return (
    <div className="author">
      <img
        src={author.fields.avatar.fields.file.url}
        alt={author.fields.name}
      />
      <div className="info">
        <span className="name">{author.fields.name}</span>
        <span className="bio">{author.fields.bio}</span>
      </div>
      <style jsx>{`
        .author {
          display: flex;
          flex-direction: row;
          justify-content: flex-start;
          align-items: center;
          width: 36rem;
          margin: 1rem auto;
        }
        .info {
          display: flex;
          flex-direction: column;
          justify-content: center;
        }
        .name {
          font-weight: bold;
          font-size: 1.2rem;
        }
        .bio {
          font-size: 0.875rem;
        }
        img {
          width: 6.25rem;
          border-radius: 50%;
          margin: 1rem;
        }
      `}</style>
    </div>
  )
}

With the post detail page and all of its associated components created, it's time to see how it looks in the browser.

If everything is working correctly, clicking on the title of the blog post from the index page should bring up the post detail page looking something like this:

Post Detail

Looking fantastic, isn't it? A little to be desired as far as aesthetics but the framework has been layed out.

That's it for the code portion of this article. Now it's time to deploy the updates to Vercel.

Configuring environment variables in Vercel

Earlier, environment variables were created in the .env file for local development. But as every good developer knows, secrets should never be stored in a repository. So now is the time to tell Vercel about the environment variables required to run the application.

Log in to Vercel, click the project name from the list of projects, and then click the "Settings" tab to navigate to the project settings page. Under the "Environment Variables" section, add the CONTENTFUL_ACCESS_TOKEN and CONTENTFUL_SPACE_ID environment variables just as they were in your .env file. Do this for both Production and Preview environments.

Environment Variables

Deploy the updates

With the environment variables properly configured, the project can now be deployed to the Preview environment. In a terminal, perform the following to deploy the changes:

now

Upon successful completion, the preview URL will be displayed in the command output, as well as copied to the clipboard. Open that URL in a browser and try navigating around.

If everything is working as expected, the production environment can be updated as well. Run the following in the terminal:

now --prod

Again, upon successful completion of the deployment command, the URL to the deployment will be displayed in the command output. Open it in a browser and see your work in all its glory.

Wrapping up

That's it for this part of the series. Stay tuned for the next part in the series where we will build the ability to preview a blog post in the website before publishing it, as well as create a web hook for deploying to production when a new article is published.

The code for this tutorial is available on GitHub, broken down into stages. Master will always represent where the tutorial series is at as new parts are released.

Mike Robinson
Mike RobinsonI build apps and stuff for the web.