Build a Static Blog with Next.js and Contentful — Part Two: Next.js
Published:
Updated:
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:
- Register for an account at Vercel
- Install Node.js 12.x
- Install Vercel CLI and link with your Vercel account
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.
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.
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:
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:
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.
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.