Devs! Try out Prismic's new onboarding and get $50 on us! šŸŽApply Now
Tech stack
Ā·10 min read

Build a Next.js Blog: A Step-by-Step Guide

When building a blog with Next.js, you may be tempted to handle the management of the content yourself in the code using methods like Markdown. However, this approach often becomes cumbersome and can quickly pose issues as the team grows and fewer technical people are onboarded to the project.

This is why using a headless CMS like Prismic is ideal for a Next.js blog because it allows for easy management of content for both technical and non-technical people. Prismic enables you to build and iterate pages quickly, which is essential for managing a blog and consistently delivering quality, unique content. This is precisely what weā€™ll explore further in this tutorial as we build the example blog below.

A GIF showing the blog we will be building.

Project Setup

To get started, weā€™ll need to create a new Prismic project, which we can do with a few easy steps:

  1. Log in to PrismicĀ orĀ create a new account.
  2. Create a new Prismic repositoryĀ using the Next.js option.
  3. Select theĀ ā€Minimal starterā€
  4. Give your repository a name, select the free plan, and clickĀ Create Repository.

Launch your starter

Then, once the project has been created in Prismic, click Get Started with Slice Machine and run the command in the dashboard to launch your starter. You will be asked if you want to run Slice Machine in the terminal. We will run this in the next step, so for now, you can type n and click Iā€™m done.

An image of the 'Launch your starter' command in Prismic.

Next, in order to view your project in development, run the commands under ā€˜Run your projectā€™ in the Prismic dashboard (making sure to swap in your repository name for <your-project-name>). When youā€™re done, you can mark this step as completed by clicking the button.

And image of the commands to run your project in Prismic.

Weā€™ll go through editing a page and creating slices and page types later in the tutorial, so donā€™t worry about those steps yet. For now, open the project in your code editor.

In your browser, you should be able to see the front end of your website locally atĀ http://localhost:3000/.

An image of the frontend of our starter project.

Setup Tailwind CSS

Once your Prismic project is set up and the repository has been cloned to your local machine, the next thing we need to configure is Tailwind CSS. We can do this in a few steps:

1. Install Tailwind CSS by running the following commands in a new tab in your terminal:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

2. Update your tailwind.config.js file to work with the src directory our project uses. It should look like this:

// ./tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
  theme: {
    extend: {},
  },
  plugins: [],
};

3. Create a styles.css file inside the app directory, and add the Tailwind CSS directives.

@tailwind base;
@tailwind components;
@tailwind utilities;

4. Import the new styles.css file in your layout.tsx file.

// ./src/app/layout.tsx

// ... other imports 
import './styles.css';

// ... rest of the file

Slice Machine Configuration

With our project initialized, and Tailwind CSS set up, we can now turn our attention to building our blog and creating content for it. We will first configure our page types and slices using Slice Machine, which we can access by going toĀ http://localhost:9999/Ā (this server was also started earlier when we ranĀ npm run devĀ to launch our project).

What are Slices and Slice Machine?

Slice: A dynamic website section. Slices are freely arranged in a document'sĀ slice zone. These are the components that will render your content from Prismic and allow content editors to compose dynamic pages quickly.

Slice Machine: Prismic's local development tool for developing with slices in your codebase.

Creating a ā€œBlog Postā€ Page Type

Once youā€™ve accessed Slice Machine, the first thing youā€™ll do is click Create and add a page type called ā€œBlog Post.ā€ This will be the page template weā€™ll be using for all of our blog post pages when we add content later on in Prismic. Since there will be many of these pages, we will select the "Reusable type."

An image of creating a Blog Post page type in Slice Machine.

After creating the new ā€œBlog Postā€ page type, youā€™ll be shown a page where you can add slices. Our starter came with one existing RichText slice that we can add to our new page type. To do so, click Select Existing, select the RichText slice, and click Add.

A GIF of adding a slice to your page type in Slice Machine.

After doing this, weā€™re going to add several fields to the ā€œStatic Zoneā€ of the page, including:

  • UID (pre-existing)
  • Title: Rich Text field with only h1 tag
  • Description: Rich Text field with only p, b, i, and link tags
  • Featured Image: Image field
  • Publication Date: Date field

Make sure to save your changes by clicking Save in the top right.

Customizing your rich text field

You can select individual tags in a Rich Text element by clicking the pencil icon on the field, selecting the necessary tags under Accept*, and then clicking Done to confirm the changes.

Your ā€œBlog Postā€ page type should now look like this.

An image of our blog post page type in Slice Machine.

Updating the ā€œPageā€ Page Type

With our new ā€œBlog Postā€ page type created and customized, letā€™s turn our attention to the other page type present in Slice Machine, called ā€œPage.ā€ We can navigate to this slice by clicking on the ā€œPage Typesā€ option from the side menu on the left. We will use this page type for our blog's home page, where weā€™ll display a hero section and our latest blog posts.

You can see we already have a UID and Title field added to the Static Zone of this page type, in addition to our RichText slice. The only thing we need to do is create a Hero slice that we can display at the top of the homepage.

To do this, click Add slices > Create new and create a slice called ā€œHero.ā€

An image of creating a Hero slice in Slice Machine.

Then, click on our Hero slice and add the following fields to the Non-Repeatable Zone:

  • Title: Rich Text field h1 tag only
  • Description: Rich Text field with only p, b, i, and link tags
  • Image: Image
An image of our Hero slice fields in Slice Machine.

Make sure to click Save when you are done.

Creating a simple ā€œNavigationā€ custom type

The final step in configuring Slice Machine is to create a simple navigation that allows us to navigate back to the homepage from our blog posts quickly.

To do this, click Custom Types from the left side menu and click Create. Add a single custom type called ā€œNavigation.ā€

An image showing creating a Navigation custom type in Slice Machine.

For this tutorial, we are going to keep our navigation very simple. First, in the Static Zone, add:

  • Menu Items: Group field

Then, inside this group field, add two other fields:

  • Label: Key Text field
  • Link: Link field
An image of our Navigation fields in Slice Machine.

Make sure to save your changes after adding your fields.

Finally, with both our page types and navigation configured, we need to push our changes to Prismic by going to the ā€œChangesā€ tab from the left menu and clicking Push Changes. This will sync your changes locally in Slice Machine with your Prismic dashboard and allow the new fields and slices to be used.

Not signed up for The Optimized Dev?

Staying on the leading edge of web development just got easier. Explore new tech with a fun, scaffolded coding challenge that lands in your inbox once a month.

Building our blog

Before heading to the Prismic dashboard and populating the new fields and slices weā€™ve created, letā€™s first jump into the code and update the layout and styling of our pages, components, and slices.

Updating Styles

The first page weā€™re going to update is the /app/layout.tsx file by adding some base styling. Below is the updated code for it.

import { PrismicPreview } from '@prismicio/next';
import { repositoryName } from '@/prismicio';
import './styles.css';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <head>
        <link
          rel="icon"
          type="image/png"
          sizes="any"
          href="https://prismic.io/favicon.ico"
        />
      </head>

      <body className="flex flex-col items-center bg-stone-50">
        <div className="bg-white max-w-7xl min-h-screen border-x border-solid border-gray-200 p-12 w-full flex flex-col gap-20 items-center text-slate-700">
          {children}
          <PrismicPreview repositoryName={repositoryName} />
        </div>
      </body>
    </html>
  );
}

Next, letā€™s create a custom component that handles the displaying of RichText elements used throughout our project.

To do this, create a new file at ./src/components/RichText.tsx and add the code below.

// ./src/components/RichText.tsx

import { RichTextField } from '@prismicio/client';
import { JSXMapSerializer, PrismicRichText, PrismicLink } from '@prismicio/react';

export const richTextComponents: JSXMapSerializer = {
  label: ({ node, children }) => {
    if (node.data.label === 'codespan') {
      return <code>{children}</code>;
    }
  },
  heading1: ({ children }) => (
    <h1 className="font-bold text-4xl">{children}</h1>
  ),
  heading2: ({ children }) => <h2 className="font-bold text-xl">{children}</h2>,
  heading3: ({ children }) => <h3 className="font-bold text-lg">{children}</h3>,
  paragraph: ({ children }) => <p>{children}</p>,
  hyperlink: ({ children, node }) => (
    <PrismicLink field={node.data} className="font-bold underline">{children}</PrismicLink>
  ),
};

interface RichTextProps {
  field: RichTextField;
}

export const RichText = ({ field }: RichTextProps) => {
  return <PrismicRichText field={field} components={richTextComponents} />;
};

This component allows us to easily render RichText elements throughout our project and have centralized styles apply to each element without us needing to import the custom styles multiple times in the project.

With that component created, weā€™re now ready to update the styling of our two slices: RichText and Hero. Below are the updated layouts and stylings for both.

// ./src/slices/RichText/index.tsx

import type { Content } from "@prismicio/client";
import { SliceComponentProps } from "@prismicio/react";
import { RichText } from "@/components/RichText";

type RichTextProps = SliceComponentProps<Content.RichTextSlice>;

export default function RichTextSlice({ slice }: RichTextProps) {
  return (
    <section className="flex flex-col gap-2">
      <RichText field={slice.primary.content} />
    </section>
  );
}
// ./src/slices/Hero/index.tsx

import { Content } from "@prismicio/client";
import { SliceComponentProps, PrismicText } from "@prismicio/react";
import { RichText } from "@/components/RichText";
import { PrismicNextImage } from "@prismicio/next";

/**
 * Props for `Hero`.
 */
export type HeroProps = SliceComponentProps<Content.HeroSlice>;

/**
 * Component for "Hero" Slices.
 */
const Hero = ({ slice }: HeroProps): JSX.Element => {
  return (
    <section
      className="flex flex-col gap-4 max-w-3xl w-full"
      data-slice-type={slice.slice_type}
      data-slice-variation={slice.variation}
    >
      <PrismicNextImage
        field={slice.primary.image}
        sizes="100vw"
        className="w-full max-w-[100px] max-h-full rounded-md object-cover"
      />
      <div className="flex flex-col gap-2">
        <h1 className="font-bold text-4xl">
          <PrismicText field={slice.primary.title} />
        </h1>
        <RichText field={slice.primary.description} />
      </div>
    </section>
  );
};

export default Hero;

Creating our Navigation component

With our slices now styled, weā€™re almost ready to look at updating our home page. But before we can do that, we must create a couple of new components. First, let's create a Navigation component to navigate between our homepage and blog posts. Create a new file called Navigation.tsx inside the components directory we created earlier and add the code below.

// ./src/components/Navigation.tsx

import { Client, Content, isFilled } from "@prismicio/client";
import { PrismicLink } from "@prismicio/react";

export const Navigation = async ({
  client,
}: {
  client: Client<Content.AllDocumentTypes>;
}): Promise<JSX.Element> => {
  const navigation = await client.getSingle("navigation");

  return (
    <nav className="font-bold text-xl self-center">
      <ul>
        {isFilled.group(navigation.data.menu_items) &&
          navigation.data.menu_items.map((item) => {
            return (
              <li key={item.label}>
                <PrismicLink field={item.link}>{item.label}</PrismicLink>
              </li>
            );
          })}
      </ul>
    </nav>
  );
};

Creating our PostCard component

The second component we will create is a PostCard component to handle the displaying of the blog posts that appear on the homepage.

To create this component, add a new file called PostCard.tsx inside the components directory. Then add in the below code, which takes in the blog post data as a prop and renders it with the styling weā€™ve added.

// ./src/components/PostCard.tsx

import { PrismicNextImage } from "@prismicio/next";
import { PrismicLink, PrismicText } from "@prismicio/react";
import { RichText } from "./RichText";
import { Content } from "@prismicio/client";

export const PostCard = ({
  post,
}: {
  post: Content.BlogPostDocument;
}): JSX.Element => {
  const { data } = post;

  return (
    <PrismicLink document={post} className="grid grid-cols-2 gap-10">
      <PrismicNextImage
        field={data.featured_image}
        sizes="100vw"
        className="w-full max-w-sm max-h-60 rounded-xl object-cover"
      />
      <div className="flex flex-col gap-3">
        <div className="flex flex-col gap-1">
          <p className="text-sm opacity-75 text-slate-700 border-b-2 w-min pb-1">
            {new Date(data?.publication_date || "").toLocaleDateString()}
          </p>
          <div className="hover:opacity-75 duration-300 ease-in-out transition-all">
            <h2 className="font-bold text-xl">
              <PrismicText field={data.title} />
            </h2>
          </div>
        </div>
        <RichText field={data.description} />
      </div>
      <div className="border-b border-solid border-gray-200 w-full col-span-2" />
    </PrismicLink>
  );
};

Updating our home page

With our new PostCard component created, weā€™re ready to start customizing our blog's home page. The home page can be found in the /src/app/page.tsx file. In this file, there will be a couple of pre-existing functions. We haven't updated the first generateMetadata(), so it can be left as is.

However, the second one Index(), handles rendering the content to the page. Below is the updated code for the entire file, including the necessary imports.

// ./src/app/page.tsx

import { Metadata } from "next";

import { SliceZone } from "@prismicio/react";
import * as prismic from "@prismicio/client";

import { createClient } from "@/prismicio";
import { components } from "@/slices";
import { PostCard } from "@/components/PostCard";
import { Navigation } from "@/components/Navigation";

/**
 * This component renders your homepage.
 *
 * Use Next's generateMetadata function to render page metadata.
 *
 * Use the SliceZone to render the content of the page.
 */

export async function generateMetadata(): Promise<Metadata> {
  const client = createClient();
  const home = await client.getByUID("page", "home");

  return {
    title: prismic.asText(home.data.title),
    description: home.data.meta_description,
    openGraph: {
      title: home.data.meta_title || undefined,
      images: [
        {
          url: home.data.meta_image.url || "",
        },
      ],
    },
  };
}

export default async function Index() {
  // The client queries content from the Prismic API
  const client = createClient();

  // Fetch the content of the home page from Prismic
  const home = await client.getByUID("page", "home");

  // Get all of the blog_post documents created on Prismic ordered by publication date
  const posts = await client.getAllByType("blog_post", {
    orderings: [
      { field: "my.blog_post.publication_date", direction: "desc" },
      { field: "document.first_publication_date", direction: "desc" },
    ],
  });

  return (
    <>
      <Navigation client={client} />

      <SliceZone slices={home.data.slices} components={components} />

      {/* Map over each of the blog posts created and display a `PostCard` for it */}
      <section className="grid grid-cols-1 gap-8 max-w-3xl w-full">
        {posts.map((post) => (
          <PostCard key={post.id} post={post} />
        ))}
      </section>

      <Navigation client={client} />
    </>
  );
}

Creating our /blog/[uid] page

With our home page now updated, letā€™s turn our attention to the new page weā€™ll need to create that will display our blog posts and their content. First, letā€™s create a new file to contain the code needed to generate and render our blog post pages.

To do this, create a new file at ./src/app/blog/[uid]/page.tsx. The file path is important because it allows us to create dynamic routes thanks to the [uid] portion. This will enable us to automatically generate new pages in Next.js whenever we create new pages in Prismic.

With the file created, letā€™s now add the code for it.

// ./src/app/blog/[uid]/page.tsx

import { Metadata } from "next";
import { notFound } from "next/navigation";

import { SliceZone } from "@prismicio/react";
import * as prismic from "@prismicio/client";

import { createClient } from "@/prismicio";
import { components } from "@/slices";
import { PrismicNextImage } from "@prismicio/next";
import { PostCard } from "@/components/PostCard";
import { RichText } from "@/components/RichText";
import { Navigation } from "@/components/Navigation";

type Params = { uid: string };

/**
 * This page renders a Prismic Document dynamically based on the URL.
 */

export async function generateMetadata({
  params,
}: {
  params: Params;
}): Promise<Metadata> {
  const client = createClient();
  const page = await client
    .getByUID("blog_post", params.uid)
    .catch(() => notFound());

  return {
    title: prismic.asText(page.data.title),
    description: page.data.meta_description,
    openGraph: {
      title: page.data.meta_title || undefined,
      images: [
        {
          url: page.data.meta_image.url || "",
        },
      ],
    },
  };
}

export default async function Page({ params }: { params: Params }) {
  const client = createClient();

  // Fetch the current blog post page being displayed by the UID of the page
  const page = await client
    .getByUID("blog_post", params.uid)
    .catch(() => notFound());

  /**
   * Fetch all of the blog posts in Prismic (max 2), excluding the current one, and ordered by publication date.
   *
   * We use this data to display our "recommended posts" section at the end of the blog post
   */
  const posts = await client.getAllByType("blog_post", {
    predicates: [prismic.filter.not("my.blog_post.uid", params.uid)],
    orderings: [
      { field: "my.blog_post.publication_date", direction: "desc" },
      { field: "document.first_publication_date", direction: "desc" },
    ],
    limit: 2,
  });

  // Destructure out the content of the current page
  const { slices, title, publication_date, description, featured_image } =
    page.data;

  return (
    <div className="flex flex-col gap-12 w-full max-w-3xl">
      <Navigation client={client} />

      {/* Display the "hero" section of the blog post */}
      <section className="flex flex-col gap-12">
        <div className="flex flex-col items-center gap-3 w-full">
          <div className="flex flex-col gap-6 items-center">
            <p className="opacity-75 border-b-2 w-min pb-1">
              {new Date(publication_date || "").toLocaleDateString()}
            </p>
            <div className="text-center">
              <RichText field={title} />
            </div>
          </div>
          <div className="text-center">
            <RichText field={description} />
          </div>
        </div>
        <PrismicNextImage
          field={featured_image}
          sizes="100vw"
          className="w-full max-w-3xl max-h-96 rounded-xl object-cover"
        />
      </section>

      {/* Display the content of the blog post */}
      <SliceZone slices={slices} components={components} />

      {/* Display the Recommended Posts section using the posts we requested earlier */}
      <h2 className="font-bold text-3xl">Recommended Posts</h2>
      <section className="grid grid-cols-1 gap-8 max-w-3xl w-full">
        {posts.map((post) => (
          <PostCard key={post.id} post={post} />
        ))}
      </section>

      <Navigation client={client} />
    </div>
  );
}

export async function generateStaticParams() {
  const client = createClient();

  /**
   * Query all Documents from the API, except the homepage.
   */
  const pages = await client.getAllByType("blog_post");

  /**
   * Define a path for every Document.
   */
  return pages.map((page) => {
    return { uid: page.uid };
  });
}

Letā€™s break down the functions in the code block above.

  • generateStaticParams()

The generateStaticParams function enables us to generate a new page in Next.js for each blog post in Prismic.

The first thing that happens is we query Prismic for all of the pages that use our custom page type blog_post. Then, we loop over the returned data and return an array of all of the pages UIDā€™s. This array then tells Next.js to create a new page for each item in it so in our case, a new page for each uid.

Youā€™ll also notice that uid is the parameter name we used in the file path a moment ago. This is important because weā€™ll use that parameter to query data from Prismic when we look at the following two functions.

  • generateMetadata()

The generateMetadata function helps create the metatags and data for the page. This is largely similar to the one you wouldā€™ve seen inside the home page a moment ago, but with the query to Prismic updated to take in the UID parameter from the route to identify which page weā€™re on.

  • Page()

The final function to review from above is the Page function, which handles the rendering of content for our blog post page. Iā€™ve added some comments to help break down what each section in the code is responsible for.

Adding our blog post route

Before we can start creating pages and adding content in Prismic, the final step is to add a route for our blog post pages. To do this, we need to add a path in ./src/prismicio.ts under the / path.

// ./src/prismicio.ts

...
{
  type: "blog_post",
  path: "/blog/:uid",
},
...

This will ensure our blog posts have the proper URL structure, for example, /blog/example-post.

Adding content in Prismic

With our blog post page created, weā€™ve finished everything we need to do in the code. Weā€™re now ready to move on to adding our content in Prismic and testing our blog!

So, to get started, revisit your Prismic dashboard from earlier, which you can get to by going to https://prismic.io/dashboard and clicking on your repository. If your dashboard tab is still open from before, just make sure you refresh the page. You should be shown two pages, ā€œHomepageā€ and ā€œAnother page.ā€ We wonā€™t need the ā€œAnother pageā€ for this tutorial, so you can select it and press the ā€œArchiveā€ button to archive it.

To create a simple navigation, from the Prismic dashboard click Create New and select ā€œNavigation.ā€ For now, let's just add a single menu item by adding:

  • Label: ā€˜Homeā€™
  • Link: Select ā€œLink to a Prismic pageā€ and choose ā€œHomepage.ā€

Our document should now look like this:

A picture of our navigation type in the Page Builder.

When youā€™re ready, click Save and then Publish in the upper right.

Specifying a document title

You may have noticed that I have given a name to my document, 'Navigation,' which appears in the top left corner next to the back button in the image above. This naming convention helps with easy searching of documents that you would like to link to.

Although the document title will eventually be auto-saved with upcoming feature improvements, adding it manually for each page when using the new Page Builder is currently recommended.

Homepage

The next thing we need to do is update the content of our ā€œHomepage.ā€ To do this, navigate back to the dashboard and click the ā€œHomepageā€ document to open it in the Prismic Page Builder.

Youā€™ll see an existing RichText slice in our document. We wonā€™t need this for our Homepage, so you can click the three dots on the slice on the left and click Delete.

An image of deleting a slice in the Page Builder.

Then, letā€™s add a Hero slice to our homepage by clicking Add Slice on the left and selecting the Hero slice. Make sure to add a title, description and image to your slice, and then save and publish your page to make the changes live.

An image of our Hero slice in the Page Builder

Configure a live preview

To view a live preview of your slices on the left as you build your pages, click the three dots in the upper right and select Live Preview Settings. Then, add http://localhost:3000/slice-simulator in the input.

Create a new blog post page

With the ā€œHomepageā€ page now updated and ready to go, we need to create some blog posts to display on the home page. To do this, go back to the Prismic dashboard and click Create new > Blog Post from the list of types presented to you.

Youā€™ll want to fill in the details for the UID, title, description, featured image, and publication date on the newly created page.

After filling in these details, youā€™ll want to add a RichText slice by clicking Add slice from the table of slices on the left and selecting RichText. Add some content youā€™d like to display as the blog post's content.

Once youā€™ve finished adding content and customizing the page, it should look similar to the below page.

A GIF of our blog post page type in the Page Builder.

Make sure to click Save and Publish to make your changes live.

Create two more blog posts

To ensure we have at least three blog posts, repeat the step above and add two more blog post types to your repository. When youā€™re done, you should have at least three blog posts in total.

In your repository, you should now have at least five documents.

An image of our documents in the Prismic dashboard.

Testing Our Blog

At this point, weā€™ve configured our slices and page types, updated the styling and layout of our blog, and added the content weā€™d like to display on the page. So, weā€™re now ready to give our blog a test by visiting http://localhost:3000/ in your browser (you may need to restart your dev server by running npm run dev).

You should then see something similar to the page below, but with the content you added in Prismic.

A GIF of our completed blog.

Closing Thoughts

And, with that, itā€™s brought us to the end of the tutorial. In this tutorial, weā€™ve looked at how to build a blog with Next.js and Prismic. If youā€™re interested in checking out the final code for this project, check out the GitHub repository here, and let us know what you think in the comments below! šŸ‘‡

Thanks for reading!

Further learning

Create a stunning 3D animated portfolio website

If you are looking to take your creativity and Next.js knowledge up a notch, try this 3D animated portfolio website tutorial with Next.js 14, Three.js, GSAP, and Prismic! In this fun step-by-step course, we will make a stylish and interactive personal portfolio site that really stands out.

Article written by

Coner Murphy

Fullstack web developer, freelancer, content creator, and indie hacker. Building SaaS products to profitability and creating content about tech & SaaS.

More posts
Coner Murphy profile picture.

Join the discussion

Hit your website goals

Websites success stories from the Prismic Community

How Arcadia is Telling a Consistent Brand Story

Read Case Study

How Evri Cut their Time to Ship

Read Case Study

How Pallyy Grew Daily Visitors from 500 to 10,000

Read Case Study