How to setup Sanity CMS with Next.js & TailwindCSS
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
Website: https://web3forms.com/ CMS Admin: https://username.someothercms.com/dashboard/manage
With Sanity CMS
Website: https://web3forms.com/
CMS Admin: https://web3forms.com/studio/
# or let sanity host it
https://web3forms.sanity.studio/
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.
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
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.
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.
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.
const STUDIO_REWRITE = {
source: "/studio/:path*",
destination:
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"
}
}
If you are doing the step above, you must allow CORS origin from the Sanity Project Settings.
Go to: https://manage.sanity.io/projects/{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.
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.
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.
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_SANITY_PROJECT_ID=YOUR_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.
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.
/lib/config.js
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.
*
* https://nextjs.org/docs/basic-features/environment-variables
**/
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",
};
/lib/sanity.js
import {
createClient,
createPortableTextComponent,
createImageUrlBuilder,
createPreviewSubscriptionHook
} 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
// (https://github.com/sanity-io/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;
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",
description:
"Write a short pararaph of this post (For SEO Purposes)",
title: "Excerpt",
rows: 5,
type: "text",
validation: Rule =>
Rule.max(160).error(
"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: "author.name",
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.
post,
author,
category,
])
});
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!
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.
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.
Protip
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.
Now we also need to hide the extra view using the deskStructure.
deskStructure.js
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(
listItem.getId()
);
// 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(
S.editor()
.schemaType("siteconfig")
.documentId("siteconfig")
),
// Add a visual divider (optional)
S.divider(),
S.documentTypeListItem("page").title("Pages"),
S.divider(),
...S.documentTypeListItems().filter(hiddenDocTypes)
]);
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.
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.
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.
jsconfig.json
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.
index.js
import Head from "next/head";
import { useRouter } from "next/router";
import client, {
getClient,
usePreviewSubscription,
PortableText,
} 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 &&
posts.map((post) => (
<article>
<h3 className="text-lg"> {post.title} </h3>
<p className="mt-3">{post.excerpt}</p>
</article>
))}
</>
);
}
const query = groq`
*[_type == "post"] | order(_createdAt desc) {
...,
author->,
categories[]->
}
`;
export async function getStaticProps({ params, preview = false }) {
const post = await getClient(preview).fetch(query);
return {
props: {
postdata: post,
preview,
},
revalidate: 10,
};
}
Explanation
So, here's what we did in the above file.
- We imported the required functions from the sanity library.
- Imported next/router for detecting
?preview
so that we can live preview without publishing. - Fetched
postdata
and used preview subscription when the user is authenticated - Looped the data to render it on the frontend
- Groq query to fetch posts. (More details on the docs)
- 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.