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!