Build a blog with Next.js, Tailwind CSS, and Contentful

Looking to understand how to create a project from scratch with Next.js, Tailwind, and Contentful? This tutorial to build a minimal blog explains it all.
Published
September 28, 2023
Category

Guides

In this tutorial, we’re going to explore how we can build a basic blog with the following technology:

  • Next.js as main React framework, with support for static rendering

  • Tailwind as CSS framework

  • Contentful for creating and managing the content

You can find all the relevant materials on a GitHub repo. But before diving into the actual process, let’s have a quick overview of the topics we are going to cover.

Static rendering

One of the powerful features provided by Next.js is that we can write server components. This allows us to use static rendering, which means that all the HTML pages will be pre-rendered at build time. The front-facing site will not send any requests to the backend (in this case, Contentful). The content API will be queried only when we run the next build command and generate the static files.

Tailwind CSS

Tailwind is, as per definition, a “utility-first CSS framework.” What makes it different compared to other CSS frameworks is that it doesn’t impose any design specifications. Class names are predefined and each utility class is, in most cases, doing one thing and one thing only. 

Let’s say I want to apply some very basic styling to an H1 element. With a vanilla CSS approach, I would do something like this:

<h1 class="myCustomClass">This is a sample text</h1>

And then, in the CSS file:

In Tailwind, to obtain the exact same result, we would use this syntax:

<h1 class="text-4xl text-center text-green-500 font-bold">This is a sample text</h1>

As we can observe, we used a specific Tailwind class for each one of the CSS definitions (you can find the full list in the official documentation).

Is this really an improvement?

This was the first question that came to my mind the first time I stumbled across Tailwind and I still think it’s a valid point. At first glance, it might look like we’re writing inline styles, which goes against the concept of separation of concerns. Also, the HTML markup looks already quite messy even with the very basic styling of the example, therefore we can expect it to be way more polluted once we apply some more advanced styling.

However, this approach brings several benefits as well:

  • No class names. Having to choose names for classes in a consistent way can be a difficult task, especially on projects with multiple developers. Here we don’t have to worry about this.

  • Responsiveness out of the box. Tailwind comes with mechanisms for creating responsive websites. We just need to use prefixes like sm:, md: in the utility classes to target the most common viewport breakpoints, without having to write media queries.

  • CSS bundle is optimized. The build process of Tailwind includes only the classes that are actually used in the project, making sure that we end up with the smallest possible CSS file (especially when this is combined with minification and network compression).

Project setup

Now that we have an overview of what we want to build, let’s dive into the actual project creation.

The good news is that, since March 2023, the --Tailwind flag is officially part of the create-next-app CLI. We can therefore bootstrap a Next.js project with Tailwind in less than a minute.

Let’s open the terminal and run:

npx create-next-app

When prompted, let’s answer the questions as follows:

After the installation, we can go to our newly created directory (simple-blog-demo), run npm run dev and open the browser on http://localhost:3000/ to see what we have so far.

If everything worked as expected we should see this:

Our project with Next.js and Tailwind is ready. Let’s now do some preparation work for the content part.

Our project with Next.js and Tailwind is ready. Let’s now do some preparation work for the content part.

Set up content in Contentful

As mentioned earlier, the blog content will be entirely managed by Contentful.

We can create a new account, or use an existing one, and then click on “Content model” on the top menu.

If we don’t have any pre-existing content types, we just have to click on “Design your content model.” Otherwise, we can just click on “Add Content Type” in the top right.

If we don’t have any pre-existing content types, we just have to click on “Design your content model.” Otherwise, we can just click on “Add Content Type” in the top right.

We will be asked to choose a name, let’s go with “Blog.” We can also add a description if we want, but it’s not necessary for our demo. 

Name

Field ID

Field Type

Notes

Title

title

Text (Short text, exact search)

“This field represents the Entry title“ should be checked in the field properties

Slug

slug

Text (Short text, exact search)

In Appearance, make sure that Slug is selected. In Validation, check the “Unique Field” checkbox.

Date

date

Date and Time

Content

content

Rich Text

Now that we have the content model, it’s time to define our fields. Let’s click on “Add field” and select the type of field we need to create. I created a quick recap of the fields that we will need and their type. Please keep in mind that Field ID will be auto-filled when typing the name, so we will probably not need to type it at all.

Now that our content model is ready, we can create our blog posts. In my case, I created some dummy articles using generative AI just for the sake of the demo.

Now that our content model is ready, we can create our blog posts. In my case, I created some dummy articles using generative AI just for the sake of the demo.

API keys

Before we jump back into the code, we need to do one more thing in Contentful: create an API key. This will be used by our project in order to retrieve our content.

Let's go to Settings → API keys → Add API key. Once this is done, we need to take note of these credentials:

  • Space ID

  • Content Delivery API – access token

That’s all we needed to do on the Contentful dashboard, therefore, it’s time to go back to our project.

Install the Contentful client

Let’s go back to our terminal and run:

npm i contentful

Now that the Contentful library is installed, we can go to src/app/page.tsx, a file that was created during the installation process and that we will use as the blog listing page.

We will find some existing code here, all related to the demo page that we saw earlier. We can clean that up and just replace the whole main content with a temporary  “blog listing goes here” text.

We can also remove the existing import from next/image since we are not going to use that.

The whole content of page.tsx should now look like this:

Configure the client

Now we want to set up the connection with Contentful, in order to retrieve our blog data.

To do that, we need to import the library and then set up the client with the credentials that we stored earlier. 

Create a file named .env.local at the root of the project and put these credentials in the file like so:

SPACE_ID = ""

ACCESS_TOKEN = ""

Let’s add this on top of the file:

We can now create the utility for fetching the data. It will be outside of the Home functional component.

As you can see, we used “blog” as the content type. This is the name that we defined earlier when we created the content model on Contentful.

Now we need to invoke the newly created function. And since our function is asynchronous, the Home component needs to be asynchronous as well.

What before was:

export default function Home() {

....

will be updated to:

export default async function Home() {

...

Now that Home is asynchronous, we can invoke the getBlogEntries method, just before the return statement. We also want to make sure that it’s working, so let’s add a temporary console log there:

const blogEntries = await getBlogEntries();

console.log("Home -> blogEntries", blogEntries)

Time to check if it all works. Keep in mind that data fetching is not happening in the browser but in the Next.js local development server. As mentioned at the beginning of the article we are, in fact, taking full advantage of the Static Rendering capabilities of Next.js and pre-rendering this page at build time. Therefore, our console log is not going to be shown in the browser console but in the terminal console.

Keep in mind that data fetching is not happening in the browser but in the Next.js local development server.

What about the return type?

A careful Typescript developer will have noticed that we haven’t defined the return type of getBlogEntries. If we want to use the full potential of types and minimize the risk of potential mistakes we should declare our expected return type. 

First of all, we need to install a new dev dependency: Since we are using Rich Text for the content field, we also need to install the related types in our project:

npm install -D @contentful/rich-text-types

Let’s create a new file called types.ts inside src/app/ and then define the types for our data structure:

Now we can use BlogQueryResult as the return type for getBlogEntries. Let’s keep in mind that this function is asynchronous, therefore the return type will be a Promise:

const getBlogEntries = async ():Promise<BlogQueryResult> => {

...

The type BlogQueryResult needs to be imported from types.ts. Our IDE is likely going to add the import automatically but, if not, we need to add this on top of the file:

import { BlogQueryResult } from "./types";

Outputting the content

The connection to Contentful works and we have typed our custom data structure. It’s time to output this data into the page and define the basic markup, without any styling for now.

Where we previously put the <p>blog listing goes here</p> placeholder is where we will do our actual iteration of content. 

To do that we will use the .map function, like this:

{blogEntries.items.map((singlePost) => {

  ....

})}

Now, inside the map, let’s extract the fields that we need:

const { slug, title, date } = singlePost.fields;

Finally, we will output the preview of our blog post:

A couple of notes on what we did here:

  • We are using slug as the key. React requires us to specify a unique key prop when we use JSX elements inside a loop, and slug is a unique identifier of the blog post (as defined in our content model as well)

  • We are using the Link component from Next.js which will be needed to navigate between routes. We are pointing to a route that doesn’t exist yet (/articles/{{slug}}, we will fix that in a bit.

  • Since date is of type Date we are converting it to a readable format (like, for example, “August 17, 2023”) using toLocaleDateString. Potentially, we can move this functionality to an external utility file but to keep it simple let’s leave it here for now.

Our page.tsx should now look like this:

We didn’t apply any custom styling so the page looks a bit messed up. It’s time to make it look nice.

Styling the listing page with Tailwind

The reason why posts are distributed in a strange way on the page is because of these two Tailwind classes that are assigned to the <main> element:

  • items-center: equals to align-items: center in plain CSS. 

  • justify-between: equals to justify-content: space-between in plain CSS.

Since the flex direction of the element is column (see flex-col class) this means that the content is horizontally centered and vertically equally distributed on the page.

Let’s get rid of those two classes, and leave all the other ones:

<main className="flex min-h-screen flex-col p-24">

The content will now be aligned on the top left and we can proceed with styling the individual elements.

If you use Visual Studio Code, it’s highly recommended to install the Tailwind CSS IntelliSense extension. This helps with autocompleting and syntax highlighting.

The blog entries are now all collapsed, without margins. We could solve that by adding a margin (top or bottom) to the wrapper. But, by doing so, we will have an unnecessary margin on the first (or last) item. Instead, since we are working with flex, the ideal solution is to use the row-gap property. We will therefore use the gap-y-8 class from Tailwind, which equals to row-gap: 2rem in plain CSS.

<main className="flex min-h-screen flex-col p-24 gap-y-8">

Let’s now go to the <h2> element, which is used for the post title. We want to make this text bold and bigger:

<h2 className="font-extrabold text-xl">{title}</h2>

  • font-extrabold: equals to font-weight: 800;

  • text-xl: equals to font-size: 1.25rem; line-height: 1.75rem;

It’s already looking way better, but we want to add an extra touch to it: we want the title to change the color when hovered.

It’s already looking way better, but we want to add an extra touch to it: we want the title to change the color when hovered. If you check our DOM structure you will probably notice a problem there: the Link component (which will render an <a> tag in the page) is wrapping two elements: the h2 title and the span. We ideally want the title to change color when the whole block is hovered.

In standard CSS, we would use a child selector. Something like:

But how do we do that in Tailwind? This is actually quite simple. We just need to add a group class to the Link component:

<Link className="group" href={`/articles/${slug}`}>

  ...

And then, in the h2 definition, we will add this class:

group-hover:text-blue-500

In this way, we are telling our title element to change color when its parent element is hovered over.

We also want a smooth hover transition, therefore, let's add the transition-colors class as well.

The h2 element should now look like this:

Current state, including hover effect.

Creation of the article page

We successfully created our Blog Listing page. Now it’s time to create the article page. As we saw earlier, when using the Link component, we want the page to be available at this path:

/articles/{article slug}

In order to do this, we need to use a feature of Next.js called Dynamic Routes.

Let’s create a new file at this location: src/app/articles/[slug]/page.tsx. Notice the square brackets on the folder name, this creates a dynamic route.  Our component will receive a prop called slug which will contain the dynamic part of the route.

Let’s now create a very basic article page:

And now let’s navigate to http://localhost:3000/articles/this-is-a-test

As we can see, slug is received as part of the props, and is therefore based on the route. In this specific case it will be: “this-is-a-test.”

However, since this is a server component, we have an issue: as long as we are in development mode, every route (regardless if the slug exists or not) will work. However, at build time, Next.js will not know which routes should be created, and therefore none of these pages will be generated at build time.

generateStaticParams

This is where generateStaticParams comes into play. This Next.js function, used in combination with dynamic routes, allows us to statically generate routes at build time:

We can test if this works as intended by running npm run build and then checking the content of the build folder.

We can test if this works as intended by running npm run build and then checking the content of the build folder.

Back to the project, we now need a function for fetching the content of a single blog post, based on the slug.

Now that we have fetchBlogPost, we can use it before our return statement.

const article = await fetchBlogPost(slug);

const { title, date, content } = article.fields;

And we can now replace the content of the h1 tag, to show the actual article title:

<h1>{title}</h1>

We obviously also want to render the content. Since we used the Rich Text format, this data is not received as a string but as a structured object, but Contentful provides a utility for transforming this data structure into actual JSX.

Let’s install it in the project:

npm install @contentful/rich-text-react-renderer

And import it at the beginning of the file:

import { documentToReactComponents } from '@contentful/rich-text-react-renderer';

We can now use this to render the article content, right below the h1 title:

{ documentToReactComponents(content) }

Some final touches

The h1 element for our article title is currently unstyled, so it doesn’t stand out from the rest of the article.

Let’s add some Tailwind classes to make it look bigger:

<h1 className="font-extrabold text-3xl mb-2">{title}</h1>

We are not rendering the post date at the moment. Let’s do that, like we did on the listing page:

The article content doesn’t have any formatting, since Tailwind is resetting all the browser default styles. 

We can’t use inline styles here, because the html elements (h2, p, etc.) are rendered by documentToReactComponents. However, Tailwind also allows us to target the children of an element with a special &> selector. Let’s wrap our content in a div and use this syntax:

As you can see, we are targeting the children and assigning a margin-bottom to all p elements and making all the h2 elements bold.

Finally, let’s have a look at our article page:

Finally let’s have a look at our article page.

Time to deploy!

First, let’s push our code into a new repository on GitHub. Once this is done, we can head over to https://vercel.com/new (If you don’t have an account, you can sign up with GitHub).

You will be asked to install the GitHub application, in order to be able to choose our repository. Once this is done, we should be able to see the repository in the dropdown:

You will be asked to install the GitHub application, in order to be able to choose our repository.

After the import is done, all we have to do is to click Deploy in the Configure Project panel.

After the import is done, all we have to do is to click Deploy in the Configure Project panel.

If everything worked as expected, we should see a message saying “Congratulations! You just deployed a new Project to Vercel.

We can navigate to the Vercel preview URL and see our demo blog in action:

We can navigate to the Vercel preview URL and see our demo blog in action.

One more thing…

Our blog is now deployed and it’s entirely statically generated at build time, giving us the best possible page performance. But what happens if we edit some content or create a new article?

With the current implementation, we will need to go to the Vercel dashboard and trigger a re-deploy of the application. 

A new build will then fetch the content from Contentful API again and use the updated content in the new deploy. However, there’s a simple procedure to automate this process so that we don’t have to worry about manual deploys.

Setting up a webhook

In the Vercel dashboard, let’s go to Settings → Git.

Now let’s scroll to Deploy Hooks. Here we can create a new hook and give it a custom name. We also need to specify a branch ("main” in my case).

When the hook is created, we will get a link. Let’s copy it, we will need it in a bit.

Let’s go back to the Contentful dashboard. Then Settings → Webhooks.

Here we can set up a new webhook connection. Contentful provides a webhook template for Vercel (in the right sidebar). Let’s find it and then click “Add”.

The modal will ask for the Vercel deploy hook URL. Let’s paste the link that we previously copied.

The modal will ask for the Vercel deploy hook URL. Let’s paste the link that we previously copied.

The automated deploy webhook is now created. From this moment on, any published change on Contentful will automatically trigger a new deploy on Vercel. We can also check the history of webhook calls in the Contentful dashboard, by going to Settings → Webhooks

Conclusion

Our minimal blog is now live and functional. If you made it here, you now have a solid understanding of how to create a project from scratch with Next.js, Tailwind, and Contentful. 

My recommendation: In order to extend your knowledge and confidence with these frameworks, is to start our own project (not necessarily a blog, it can be anything) and use this tutorial as a foundation. Thanks for reading!

Start building

Use your favorite tech stack, language, and framework of your choice.

About the author

Don't miss the latest

Get updates in your inbox
Discover new insights from the Contentful developer community each month.
add-circle arrow-right remove style-two-pin-marker subtract-circle remove