How to Create a Next.js Blog with Sanity as a Headless CMS

Published on Sun Feb 18 2024
Two logos side-by-side: Next.js (black circle) and Sanity (red square).

Why Next.js and Sanity?

Next.js

Server-side rendering (SSR) and static site generation (SSG): Delivers lightning-fast performance for a seamless user experience and enhanced SEO.

Automatic code splitting and routing: Ensures optimal page load times and seamless navigation.

Built-in image optimisation: Reduces image sizes without compromising quality for faster loading.

Sanity

Headless approach: Separates content management from presentation, empowering content creators and developers.

Flexible data modelling: Provides the freedom to structure your content as you need it, without limitations.

Content previews and revisions: Streamlines the content creation process with live previews and version control.

Powerful querying: Leveraging GROQ, enables you to fetch and transform data precisely how you need it.

Prerequisites

  1. JavaScript/TypeScript: You should have a good understanding of JavaScript or TypeScript, as both Next.js and Sanity use these languages extensively.
  2. React: Next.js is built on top of React, so a strong understanding of React is essential for building components and managing the state of your Next.js application.
  3. React Server Components: By default, Next.js uses Server Components. React Server Components allow you to write UI that can be rendered and optionally cached on the server.
  4. Next.js: Familiarize yourself with the Next.js framework, including concepts such as server-side rendering (SSR), static site generation (SSG), routing, and data fetching.
  5. Tailwind CSS: Although optional, Tailwind CSS is often used for styling in Next.js projects. Familiarize yourself with Tailwind CSS classes and utility-first CSS principles if you choose to use them.

Step-by-Step Guide

Step 1: Set Up a Next.js Project

To create a new Next.js project, you can use the `create-next-app` command.

npx create-next-app next-sanity-starter


For this tutorial, we will be using the default options.

✔ Would you like to use TypeScript?
> Yes
✔ Would you like to use ESLint? 
> Yes
✔ Would you like to use Tailwind CSS? 
> Yes
✔ Would you like to use `src/` directory?
> Yes
✔ Would you like to use App Router? (recommended)
> Yes
✔ Would you like to customize the default import alias (@/*)?
> Yes

Navigate to your project directory.

cd next-sanity-starter

Step 2: Set Up Sanity Studio

In this tutorial, we will embed our Sanity studio in our Next.js project by setting up the Sanity studio in the same repository.

npx sanity init

You will receive a login prompt for your Sanity account. Select your provider and proceed.


Upon logging in, you will be prompted to create a new project or select an existing one. Let's create a new project and give it a name.

Login successful
Good stuff, you're now authenticated. You'll need a project to keep your
datasets and collaborators safe and snug.
✔ Fetching existing projects
? Select project to use (Use arrow keys)
❯ Create new project 
? Your project name: next-sanity-starter

Once you have created a new project, you will need to configure it. This involves specifying the necessary settings and options required to get your project up and running.

Your content will be stored in a dataset that can be public or private, depending on
whether you want to query your content with or without authentication.
The default dataset configuration has a public dataset named "production".
Use the default dataset configuration? 
> Yes

For this tutorial, we will use a public dataset. If you want to learn more about private datasets, read this guide.

Would you like to add configuration files for a Sanity project in this Next.js folder?
> Yes

This will create a `sanity.cli.ts` and a `sanity.config.ts file in your root directory. More on these later.

Do you want to use TypeScript?
> Yes

We will use TypeScript as the default language, you can select No for JavaScript.

Would you like an embedded Sanity Studio? 
> Yes

This allows us to add an embedded Sanity Studio to our Next.js project.

Would you like to use the Next.js app directory for routes?
> Yes

Select yes since we are using the Next.js app directory.

What route do you want to use for the Studio?
> /studio

This is the path where we can access our Sanity Studio. You can set this as anything relevant, eg. `/cms`.

Select project template to use
> Blog (schema)

You can either use a blank template and generate the schema for your blog, for this tutorial, we will use the `Blog` schema.

Would you like to add the project ID and dataset to your .env file?
> Yes

This generates the following files:

  1. `src/app/studio/[[...index]]/page.tsx`: This component is responsible for rendering the embedded Sanity Studio.
  2. `sanity/lib/client.ts`: Sanity generates a client utility that can fetch documents from Sanity.
  3. `sanity/lib/image.ts`: When you fetch Sanity documents, images are sent as a reference, you can use this utility to get the image URL from the provided reference.
  4. `sanity/schemaTypes/**`: These files define your content model for your Sanity documents. Since we selected the blog schema, Sanity generates Post, Author, and Category schemas. You can learn more about customising the Sanity documents schema here.

Step 3: Populate Content

Before you can access the Sanity Studio from your local server, you need to add localhost:3000 as a CORS origin to your Sanity project. To do this,

  1. Login to https://www.sanity.io/manage.
  2. Select your project.
  3. Select the API tab.
  4. Scroll down to CORS origins and select Add CORS origin.
  5. Insert http://localhost:3000 in the Origin and check the Credentials checkbox.

Now we are ready to run the embedded Sanity Studio. Run the `npm run dev` command and navigate to http://localhost:3000/studio to access your newly created Sanity Studio. Login to your Sanity account and you will see the following interface.

Sanity Studio Embedded Dashboard

Sanity Studio Embedded Dashboard

Go ahead and fill your write your first blog post!

Step 4: Fetch Data from Sanity in Next.js

Home Page

Since we are using the Next.js app directory, we can create async server components and fetch data from Sanity directly in our React component. To learn more about server components, click here. But before we fetch our data, let’s add some bare minimum styling.

Open `src/app/globals.css` and replace the code with the below.

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

Open the tailwind.config.ts file and replace the code with the one below.

import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      container: {
        center: true,
        padding: "1rem",
        screens: {
          lg: "768px",
          xl: "768px",
          "2xl": "768px",
        },
      },
    },
  },
  plugins: [require("@tailwindcss/typography")],
};
export default config;

The `@tailwindcss/typography` plugin adds a set of prose classes that add sensible typographic styles to content blocks. Install the package using the following command.

npm i -D @tailwindcss/typography

Create a new file `src/app/(blog)/layout.tsx`. Next.js shares the layout across all the pages, however, we do not want our Sanity to have the website’s layout. This can be achieved by using route groups. All the pages inside the `(blog)` folder will use the layout inside the `(blog)` folder instead of the root layout. Learn more about route groups.

// src/app/(blog)/layout.tsx

import Link from "next/link";
import { ReactNode } from "react";

export default function BlogLayout({ children }: { children: ReactNode }) {
  return (
    <div className="container">
      <nav className="py-12">
        <ul>
          <li>
            <Link href="/">Home</Link>
          </li>
        </ul>
      </nav>
      {children}
    </div>
  );
}

Now, open the `app/src/(blog)/page.tsx` file and add the following code.

// src/app/(blog)/page.tsx

import { groq } from "next-sanity";
import { client } from "../../sanity/lib/client";
import Link from "next/link";

const postsQuery = groq`*[_type == "post"] {
  _id,
  title,
  slug
}
`;

interface Post {
  _id: string;
  title: string;
  slug: {
    current: string;
  };
}

export default async function BlogListing() {
  const posts = await client.fetch<Post[]>(postsQuery);

  return (
    <ul className="flex flex-col gap-4">
      {posts.map((post) => (
        <li key={post._id}>
          <Link href={`/post/${post.slug.current}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  );
}

Let’s review what’s happening in this file.

`postsQuery`: Sanity uses its querying language GROQ to query data from its datasets. In this query, we are fetching all documents with the _type equal to “post” and selecting only the id, title and slug fields from the dataset.

Blog Listing Page with Two List Items

Blog Listing Page

Blog Post

Let’s create a blog post page where we can display our blog posts. Before we start writing our code for the page, we need to do a couple of configurations.

First, since Sanity uses PortableText to store block content data we need to use the `@portabletext/react` package to context the PortableText into HTML. Install the package using the below command.

npm i @portabletext/react

Second, to protect your application from malicious users, Next.js requires you to configure external domains to use external images. Paste the following code in your `next.config.js` file.

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "cdn.sanity.io",
      },
    ],
  },
};

export default nextConfig;

Create a `src/app/(blog)/post/[slug]/page.tsx` file. The square braces specify the dynamic route parameter and will be passed as a prop to the server component. Add the following lines of code to the file.

import { groq } from "next-sanity";
import { client } from "../../../../../sanity/lib/client";
import { Post } from "../../page";
import Image from "next/image";
import { urlForImage } from "../../../../../sanity/lib/image";
import { PortableText } from "@portabletext/react";

const postQuery = groq`*[_type == "post" && slug.current == $slug] [0] {
    title,
    mainImage,
    body
}
`;

interface PageParams {
  slug: string;
}

export default async function BlogPost({
  params: { slug },
}: {
  params: PageParams;
}) {
  const post = await client.fetch<Post>(postQuery, { slug });

  return (
    <article className="prose">
      <h1>{post.title}</h1>
      <Image
        src={urlForImage(post.mainImage)}
        width={300}
        height={200}
        alt={post.title}
      />
      <PortableText value={post.body} />
    </article>
  );
}

Let’s break down what’s happening here. The `postQuery` is pretty much the same as the query used to get all the posts from Sanity. However, here we need to get the post using its slug, hence the comparison `slug.current == $slug` . Furthermore, every result from a Sanity query is an array, and since we require only the first post, the query includes `[0]` to get the first object from the result. To keep the tutorial simple, I have only fetched the title, main image, and body of the post, however, in a complex application, you may need to fetch and display author, categories, tags, comments, etc.

In the server component, the slug of the post is passed inside the params object and is passed as the second parameter to the fetch function. Please note that the field name in the second argument of the fetch function must match the argument required by the GROQ query.

The `prose` utility is used to add sensible typographic styles to content blocks and we have used the `PortableText` component from the `@portabletext/react` to convert Portable Text to HTML.

Blog Post

Blog Post

Conclusion

Creating a Next.js blog with Sanity as a headless CMS offers a powerful combination of performance, flexibility, and ease of use. With Next.js providing server-side rendering and automatic code splitting, along with built-in image optimization, and Sanity offering a headless approach with flexible data modelling and powerful querying capabilities, developers and content creators have the tools they need to build and manage dynamic and engaging blog content efficiently.