Using Sanity as Headless CMS to Next.js Pages Router

Instead of using WordPress to create my portfolio and/or blog, I decided to build it in Next.js because I am an epic developer. Also because I wanted to learn more about what a website is made of; how it is built. By the same vein, it is why I chose to integrate a headless CMS to power the site, even though I could (and did) simply put all the pictures and markdown files in the /public directory.

This article is going to be a brief overview of how I setup and integrate Sanity as my headless CMS to my portfolio site.

1. Setting Up Sanity

After creating an account with Sanity, the next step is embedding Sanity Studio into my Next.js project. I started a new Sanity project with:

// portfolio is the name of the Sanity project
npx sanity@latest init --env --create-project "portfolio" --dataset production

It asked a bunch of prompts that I just answered, most importantly the option to embed Sanity Studio into the project in a new directory, /sanity.

After the initialisation, Sanity created a .env file, which we need to rename to .env.local - just a Next.js thing so we can use process.env. to access it.

Navigating to http://[localhost:3000](http://localhost:3000)/studio will be met with a pop up, saying that localhost:3000 should be added to CORS origin. Following the link and adding the link to the CORS settings of my Sanity project, navigating back to the Studio, and voila! My very own Sanity Studio is setup.

After following the Day One course in Sanity, I found myself creating a my first Sanity schema.

2. Configuring Schemas

Schemas are used to structure data in Sanity. I think of it as something similar to a JSON object, with extra meta-tags on them. The only schema I have is a projectType.

To initialise a custom schema, I created a new directory inside /sanity called schemaTypes. Created a projectType.ts in /sanity/schemaTypes and built the schema.

import { defineField, defineType } from "sanity";

export const projectType = defineType({
    name: "project",
    title: "Project",
    type: "document",
    fields: [
        defineField({
            name: "title",
            type: "string",
            validation: (rule) => rule.required(),
        }),
        defineField({
            name: "slug",
            type: "slug",
            options: { source: "title" },
            validation: (rule) => rule.required(),
        }),
        defineField({
            name: "description",
            type: "string",
            validation: (rule) => rule.required(),
        }),
        defineField({
            name: "thumbnail",
            type: "image",
            validation: (rule) => rule.required(),
        }),
        // Sanity doesn't support markdown out of the box
        defineField({
            name: "markdownContent",
            type: "markdown",
        }),
    ],
});

In defineField(), validation is a function that returns a rule - in this case the fields are required for data entry. However, it is to note that markdown is not supported out of the box. Sanity uses PortableText specification to store their data. In order to store markdown (because all my content was created in markdown), I had to install a plugin from Sanity.

Simply run this command to install the plugin (and its peer dependency)

npm install --save sanity-plugin-markdown easymde@2

Afterwards, add the plugin to the sanity.config.ts and the type “markdown” is available in defineField().

export default defineConfig({
    ...,
    plugins: [
        markdownSchema(),
        ...
        ,
    ],
});

Complete the new schema setup by adding it to schema.ts

import { type SchemaTypeDefinition } from "sanity";
import { projectType } from "./schemaTypes/projectType";

export const schema: { types: SchemaTypeDefinition[] } = {
    types: [projectType],
};

After schemas are created, they can be viewed by opening the Sanity Studio, in this case http://localhost:3000/studio. Data can then be inserted into Sanity. Next up, let’s see how I use the data from Sanity to create the UI.

3. Populating the page with Sanity data

In client.ts, we can use a helper function recommended by Sanity.

// helper function that only caches for 30s in dev. 
// in production, caches for an hour.
export async function sanityFetch<QueryResponse>({
    query,
    params = {},
    tags,
}: {
    query: string;
    params?: QueryParams;
    tags?: string[];
}) {
    return client.fetch<QueryResponse>(query, params, {
        next: {
            revalidate: process.env.NODE_ENV === "development" ? 30 : 3600,
            tags,
        },
    });
}

I used this helper function to fetch data from Sanity from inside of getStaticProps() of a page. This ensures that the page is compiled at build-time, and a snappier experience when navigating through the website.

Other than the props of the data itself, I have put in revalidate: 10 in the return object. This ensures that any update or changes to the content in Sanity is reflected in the webpage itself. Next will revalidate the cache every 10 seconds to check if the content of the page is up-to-date. If it is, the next time the page is requested, Next will serve the most updated page.

export async function getStaticProps() {
    const GET_PROJECTS_QUERY = groq`*[_type=='project']{title, slug, description, thumbnail, markdownContent}`;
    const projects = await sanityFetch<GET_PROJECTS_QUERYResult>({
        query: GET_PROJECTS_QUERY,
    });

    return {
        props: {
            projects,
        },
		    revalidate: 10,
    };
}

You might notice that the sanityFetch() function uses a type. This type can be generated with sanity typegen generate using the Sanity CLI. I am not covering it in this article, though I highly recommend checking it out yourselves here.

Now, we need to take the projects prop in the page component itself, and parse the data.


interface Props {
    projects: Project[];
}

export default function Home({ projects }: Props) {
    return (
        <>
         ...
		    <div className='projects-grid'>
              {/* List of Projects */}
              {projects.map((project) => (
                  <ProjectItemCard
                      project={project}
                      key={project.title}
                  />
              ))}
          </div>
          ...
        </>
    );
}

4. Wrapping-Up

So that’s how I used Sanity to back my portfolio! This project entry will be sent to Sanity, and if this is live, Sanity has fully integrated into my website. My experience with Sanity has been great - everything is quite straightforward, and the documentation is ample. Then again, my current website does not require a lot of data manipulation or complex groq queries. Be sure to check my Github for more insight!