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.
Project Setup
To get started, weāll need to create a new Prismic project, which we can do with a few easy steps:
- Log in to PrismicĀ orĀ create a new account.
- Create a new Prismic repositoryĀ using the Next.js option.
- Select theĀ āMinimal starterā
- 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.
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.
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/
.
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."
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.
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.
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.ā
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
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.ā
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
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.
Navigation
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:
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.
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.
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.
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.
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.
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.