Introducing GROQ-powered Webhooks
August 12, 2021

How to setup Sanity CMS with Next.js & TailwindCSS

By Surjith S M

There are many Headless CMS out there, but Sanity CMS is a perfect choice when working with a Next.js & TailwindCSS Project.

In this article, we will deep dive into the setup. First, here are a few reasons why I choose Sanity CMS as my Headless CMS (and you should too).

First, the schema & the UI is completely controlled by the code so that we can customize it in any way we like. There are no restrictions. They are using react for their UI, so we can customize it as we like. In the case of other CMS, we have to stick with their UI & only use whatever option they provide.

Second, The Admin UI can be completely hosted anywhere we like and it will communicate with the main server in real-time. While using other CMS, we have to open their website and log in to access our website content. But in Sanity, we can host the Admin in our server itself.

See an example on how it might looks like:

Other CMS


CMS Admin:

With Sanity CMS


CMS Admin:

# or let sanity host it

How cool is that? When doing projects for clients, this will be a good feature to convince them.

The third reason is its beautiful design. I have played around with some other CMS, when the design is good, the pricing keeps us away. But I liked the UI Design of Sanity along with a better Pricing, This is of course a personal opinion.

So, without wasting much time, let's dive in.

Setting up Next.js & Tailwind

This is pretty straightforward, also there are many tutorials available. So I won't get deep, but I have also made a starter template that you can use to save time.

Next.js & TailwindCSS Starter Template

The first step is to install Next.js with their bootstrap template called "Create Next App". If you want an in-depth tutorial, visit: Next.js Docs

npx create-next-app 

# or 

yarn create next-app

Now we can install TailwindCSS. This is also easy. Follow the steps below or check out the official docs here: Install TailwindCSS with Next.js

npm install tailwindcss postcss autoprefixer 

# or 

yarn add tailwindcss postcss autoprefixer

Now Generate your Configuration file.

npx tailwindcss init -p

This will create a minimal tailwind.config.js file and postcss.config.js at the root of your project. Make sure you add purge settings to remove unused classes from the production build.

Now add TailwindCSS file eg: /styles/tailwind.css

@tailwind base; 

@tailwind components; 

@tailwind utilities;

Then you can include the CSS in pages/_app.js

That's it! Done! now run the following command to see if everything working. You should be able to see it live on http://localhost:3000

Setting up Sanity CMS

The first step is to install Sanity CLI globally. use the following command to do that.

npm install -g @sanity/cli

Now, go to the root folder of your created next.js app, and run the following command.

sanity init

The above command will walk you through some steps to create / login to an account, creating a project, set up the dataset, generate the files, etc.

Sanity CLI

The only thing to consider is when it asks to choose a folder name, make sure it's in the root folder of next.js and name it as something like studio or admin

Now, the folder will create at the root of Next.js project.

Setup Admin Path

To set up the admin path as /studio or /admin as I mentioned in the intro, you have to configure some steps. Go to next.config.js (or create one) and add the following code.

  source: "/studio/:path*",
    process.env.NODE_ENV === "development"
      ? "http://localhost:3333/studio/:path*"
      : "/studio/index.html",

module.exports = {
  rewrites: () => [STUDIO_REWRITE],

You may change the word studio to admin if you like. This uses the Next.js rewrite function so that we don't need to browse separate URLs.

Also, make sure you update the basepath in studio/sanity.json so that the dependencies resolve correctly.

   "project": {
    "name": "Your Sanity Project",
    "basePath": "/studio"

Setup CORS

If you are doing the step above, you must allow CORS origin from the Sanity Project Settings.

Go to:{project_id}/settings/api

Project ID can be found in /studio/sanity.json

Now, click on ADD ORIGIN button on the page and add your URL & Enable the "Allow Credentials" checkbox.

Allow CORS

Since I run Next.js on port 3000 on localhost, I'm using that URL, You can also add the Production URL in the same way.

Configure Development Server

The next step is to set up both Sanity & Next.js dev server. Open your package.json and change your scripts like this.

  "scripts": {
  "dev": "next dev",
  "prebuild": "echo 'Building Sanity to public/studio' && cd studio && yarn && npx @sanity/cli build ../public/studio -y && echo 'Done'",
  "build": "next build",
  "start": "next start",
  "sanity": "cd studio && sanity start",
  "lint": "next lint"

Now, open two terminals in your code editor and try running `yarn dev` and `yarn sanity` to run both servers.

The prebuild step will ensure the Sanity Studio will build before building the Next.js while pushing to production.

Add .env for Sanity

You have to add a .env.local file to add the project ID. Use the following text and replace YOUR_PROJECT_ID with your actual project ID.


NEXT_PUBLIC_ is required by Next.js. Do not remove it. If you have to use this project ID in Sanity Studio, you have to create .env file instead.

Setup Next Sanity Plugin

Now, we need to install one last plugin which is called next-sanity. This plugin is needed so that we can call the API easily. The plugin will handle the rest.

npm install next-sanity 

# or 

yarn add next-sanity

Now, create two files called config.js and sanity.js in /lib folder in the root of our project. These will be communicating with the plugin. (Code taken from the next-sanity repo). No changes need in the below file, Just copy-paste and save.


export const config = {
   * Find your project ID and dataset in `sanity.json` in your studio project.
   * These are considered “public”, but you can use environment variables
   * if you want differ between local dev and production.
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
  apiVersion: "2021-08-11", // or today's date for latest
   * Set useCdn to `false` if your application require the freshest possible
   * data always (potentially slightly slower and a bit more expensive).
   * Authenticated request (like preview) will always bypass the CDN
  useCdn: process.env.NODE_ENV === "production",


import {
} from "next-sanity";
import ReactTooltip from "react-tooltip";

import { config } from "./config";
if (!config.projectId) { throw Error( "The Project ID is not set. Check your environment variables." ); } export const urlFor = source => createImageUrlBuilder(config).image(source); export const imageBuilder = source => createImageUrlBuilder(config).image(source); export const usePreviewSubscription = createPreviewSubscriptionHook(config); // Set up Portable Text serialization export const PortableText = createPortableTextComponent({ ...config, // Serializers passed to @sanity/block-content-to-react // ( serializers: { types: { code: props => ( <pre data-language={props.node.language}> <code>{props.node.code}</code> </pre> ) }, } }); export const client = createClient(config); export const previewClient = createClient({ ...config, useCdn: false }); export const getClient = usePreview => usePreview ? previewClient : client; export default client;

Creating the Schema

Now, open the /studio/schemas/schema.js file and add a sample schema. More details about this can be found on Sanity Docs

First, create a post.js file. It can be anywhere but make sure you linked it properly in the schema.js file.

import { HiOutlineDocumentAdd } from "react-icons/hi";

export default {
  name: "post",
  title: "Posts",
  icon: HiOutlineDocumentAdd,
  type: "document",
  fields: [
      name: "title",
      title: "Title",
      type: "string",
      validation: Rule => Rule.required()
      name: "slug",
      title: "Slug",
      type: "slug",
      validation: Rule => Rule.required(),
      options: {
        source: "title",
        maxLength: 96
      name: "excerpt",
        "Write a short pararaph of this post (For SEO Purposes)",
      title: "Excerpt",
      rows: 5,
      type: "text",
      validation: Rule =>
          "SEO descriptions are usually better when its below 160"
      name: "body",
      title: "Body",
      type: "blockContent",
      validation: Rule => Rule.required()
      name: "author",
      title: "Author",
      type: "reference",
      to: { type: "author" },
      validation: Rule => Rule.required()
      name: "mainImage",
      title: "Main image",
      type: "image",
      fields: [
          name: "alt",
          type: "string",
          title: "Alternative text",
          description: "Important for SEO and accessiblity.",
          options: {
            isHighlighted: true
      options: {
        hotspot: true
      name: "categories",
      title: "Categories",
      type: "array",
      of: [{ type: "reference", to: { type: "category" } }],
      validation: Rule => Rule.required()
      name: "publishedAt",
      title: "Published at",
      type: "datetime"

  preview: {
    select: {
      title: "title",
      author: "",
      media: "mainImage"
    prepare(selection) {
      const { author } = selection;
      return Object.assign({}, selection, {
        subtitle: author && `by ${author}`

The above page gives you an idea of how a schema can look like. There are lot of customization available. Be sure to check the Official Docs.

Now open the schemas/schema.js file and make sure to import the post and add it to the schemaTypes.concat Array.

// First, we must import the schema creator
import createSchema from "part:@sanity/base/schema-creator";

// Then import schema types from any plugins that might expose them
import schemaTypes from "all:part:@sanity/base/schema-type";

// We import object and document schemas
import post from "./post";
import author from "./author";
import category from "./category";
// Then we give our schema to the builder and provide the result to Sanity export default createSchema({ // We name our schema name: "default", // Then proceed to concatenate our document type // to the ones provided by any plugins that are installed types: schemaTypes.concat([ // The following are document types which will appear // in the studio.
]) });

Don't fret, we only need to import the file and add schema type inside the types, the rest of the code is already there for us. Pretty neat!

Making it Singleton (one-off)

Posts are good for articles, but sometimes you need to make a page for Site Settings or any other pages where you don't need an array. Instead, you only want that setting to appear once.

Sanity doesn't support that by default, but they provide some options to achieve what we want. Psst.. That's the flexibility of this platform.

Skip this step if you don't plan to use a singleton.

1. Create the Schema

export default {
  name: "siteconfig",
  type: "document",
  title: "Site Settings",
__experimental_actions: [
/* "create", "delete", */ "update", "publish"
], fields: [ { name: "title", type: "string", title: "Site title" } // other fields // ... ] }

Did you notice the __experimental_actions part? This is where we disable the "create" and "delete" actions so that that particular file can only be created once.


Make sure to enable `create` first and add a new document and publish it. Now come back and disable it. Otherwise, you won't be able to create any files.

2. Hide it from the UI

Now we also need to hide the extra view using the deskStructure.


import S from "@sanity/desk-tool/structure-builder";
import { HiOutlineCog } from "react-icons/hi";

// Add Schema type to hidden
const hiddenDocTypes = listItem =>
!["page", "siteconfig",].includes(
// Render a custom UI to view siteconfig & pages // and showing other items except mentioed in the hiddendoctypes export default () => S.list() .title("Content Manager") .items([ S.listItem() .title("Site config") .icon(HiOutlineCog) .child(
), // Add a visual divider (optional) S.divider(), S.documentTypeListItem("page").title("Pages"), S.divider(),

Read more about Structure Builder on Sanity Docs

Now, we need to add the path to deskStructure in sanity.json. Open the file and add the following lines.

   "parts": [{
      "name": "part:@sanity/base/schema",
      "path": "./schemas/schema"
      "name": "part:@sanity/desk-tool/structure",
      "path": "./deskStructure.js"

That's it, we are good to go now.

Adding Content in Sanity CMS

It's time to add content to our Sanity Database. Run the following commands to start Next.js & Sanity.

# Terminal 1

yarn dev

# Terminal 2

yarn sanity

Then open. http://localhost:3000 and http://localhost:3333

🥳 Our Sanity Studio is live (if followed the steps correctly), Now login to your sanity account (I prefer Github Login). Once logged in, click on our newly created schema and publish it.

Getting the data in Next.js

Now comes the final part, getting sanity content inside our next.js page. for that, there are few steps.

First, you need to know the query language called groq. That's what sanity is using by default. Also, they do provide an option for graphql. if you want, you can use that as well. But GROQ is so much powerfull and work well with Sanity as they are the creator of both.


Optional. Used for calling paths as @lib anywhere in our project instead of ../../lib.

  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@lib/*": [ "./lib/*"]

Here's a sample page to fetch the data.


import Head from "next/head";
import { useRouter } from "next/router";
import client, {
} from "@lib/sanity";

import { groq } from "next-sanity";

export default function Post(props) {
  const { postdata, preview } = props;

  const router = useRouter();

  const { data: posts } = usePreviewSubscription(query, {
    initialData: postdata,
    enabled: preview || router.query.preview !== undefined,
  return (
      {posts && => (
            <h3 className="text-lg"> {post.title} </h3>
            <p className="mt-3">{post.excerpt}</p>

const query = groq`
*[_type == "post"] | order(_createdAt desc) {

export async function getStaticProps({ params, preview = false }) {
  const post = await getClient(preview).fetch(query);

  return {
    props: {
      postdata: post,
    revalidate: 10,


So, here's what we did in the above file.

  1. We imported the required functions from the sanity library.
  2. Imported next/router for detecting ?preview so that we can live preview without publishing.
  3. Fetched postdata and used preview subscription when the user is authenticated
  4. Looped the data to render it on the frontend
  5. Groq query to fetch posts. (More details on the docs)
  6. getStaticProps function to return the data from Sanity.

🥂🥳 Yaaaayyy!!! Now refresh your browser and see your data.


That's it. Now you got a working Next.js project with Sanity and TailwindCSS enabled. Hope this tutorial helps you get started. Don't forget to check out the Sanity Docs for more information & help. They also have some nice starter templates.

If you have any questions or feedback, let me know.

Follow me on Twitter