Update Schema Content using Latest Sanity Migration Tool
let's create a simple migration using latest sanity migration cli tool.
Go to Update Schema Content using Latest Sanity Migration ToolThere are many Headless CMS out there, but Sanity CMS is a perfect choice when working with a Next.js & TailwindCSS Project.
This guide contains code examples for an older version of Sanity Studio (v2), which is deprecated.
Learn how to migrate to the new Studio v3 →In this article, I will show you how to setup Sanity CMS with Next.js & Tailwind. Please note I'm using Sanity Studio v2 for this guide. I'll create a v3 guide soon.
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.
If you are using Vercel, you can configure studio path as /studio
on production, you have to configure some steps. Create vercel.json
and add the following code.
{
"version": 2,
"rewrites": [{
"source": "/studio/(.*)",
"destination": "/studio/index.html"
}]
}
So while in development you can use http://localhost:3333
to open the studio and on your production website, you can use yourwebsite.com/studio
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 did the above step, 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 eg: yourwebsite.com
& Enable the "Allow Credentials" checkbox.
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 few plugins which is called next-sanity
and nex-sanity-image
These plugins are needed so that we can call the API easily and to render images proeprly.
npm install next-sanity
npm install next-sanity-image
# or
yarn add next-sanity
yarn add next-sanity-image
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 Image from "next/image";
import {
createClient,
createPreviewSubscriptionHook
} from "next-sanity";
import createImageUrlBuilder from "@sanity/image-url";
import { PortableText as PortableTextComponent } from "@portabletext/react";
import { config } from "./config";
import GetImage from "@utils/getImage";
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);
// Barebones lazy-loaded image component
const ImageComponent = ({ value }) => {
// const {width, height} = getImageDimensions(value)
return (
<Image
{...GetImage(value)}
blurDataURL={GetImage(value).blurDataURL}
objectFit="cover"
sizes="(max-width: 800px) 100vw, 800px"
alt={value.alt || " "}
placeholder="blur"
loading="lazy"
/>
);
};
const components = {
types: {
image: ImageComponent,
code: props => (
<pre data-language={props.node.language}>
<code>{props.node.code}</code>
</pre>
)
},
marks: {
center: props => (
<div className="text-center">{props.children}</div>
),
highlight: props => (
<span className="font-bold text-brand-primary">
{props.children}
</span>
),
link: props => (
<a href={props?.value?.href} target="_blank" rel="noopener">
{props.children}
</a>
)
}
};
// Set up Portable Text serialization
export const PortableText = props => (
<PortableTextComponent components={components} {...props} />
);
export const client = createClient(config);
export const previewClient = createClient({
...config,
useCdn: false
});
export const getClient = usePreview =>
usePreview ? previewClient : client;
export default client;
/utils/getImage.js
import client from "@lib/sanity";
import { useNextSanityImage } from "next-sanity-image";
export default function GetImage(image, CustomImageBuilder = null) {
const imageProps = useNextSanityImage(client, image, {
imageBuilder: CustomImageBuilder
});
if (!image || !image.asset) {
return null;
}
return imageProps;
}
jsconfig.json
Optional. Used for calling paths as @lib
/ @utils
anywhere in our project instead of ../../lib
.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@lib/*": [ "./lib/*"],
"@utils/*": ["./utils/*"],
}
}
}
Now, open the /studio/schemas/
folder and add a schema. See Sanity Docs
First, create a post.js
file inside /schemas
folder. The path can be anywhere but make sure you linked them properly in the schema.js
file (see below).
/schemas/post.js
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 what a schema can look like. There are a lot of customization available. Be sure to check the Official Docs.
In the above, you can see I have referenced author & category, so let's create them as well.
/schemas/author.js
export default {
name: 'author',
title: 'Author',
type: 'document',
fields: [
{
name: 'name',
title: 'Name',
type: 'string',
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'name',
maxLength: 96,
},
},
{
name: 'image',
title: 'Image',
type: 'image',
options: {
hotspot: true,
},
},
{
name: 'bio',
title: 'Bio',
type: 'array',
type: 'blockContent',
},
],
preview: {
select: {
title: 'name',
media: 'image',
},
},
}
/schemas/category.js
export default {
name: "category",
title: "Category",
type: "document",
fields: [
{
name: "title",
title: "Title",
type: "string"
},
{
name: "slug",
title: "Slug",
type: "slug",
options: {
source: "title",
maxLength: 96
},
validation: Rule => Rule.required()
},
{
name: "description",
title: "Description",
type: "text"
}
]
};
/schemas/blockContent.js
/**
* This is the schema definition for the rich text fields used for
* for this blog studio. When you import it in schemas.js it can be
* reused in other parts of the studio with:
* {
* name: 'someName',
* title: 'Some title',
* type: 'blockContent'
* }
*/
export default {
title: "Block Content",
name: "blockContent",
type: "array",
of: [
{
title: "Block",
type: "block",
// Styles let you set what your user can mark up blocks with. These
// correspond with HTML tags, but you can set any title or value
// you want and decide how you want to deal with it where you want to
// use your content.
styles: [
{ title: "Normal", value: "normal" },
// {title: 'H1', value: 'h1'},
{ title: "H2", value: "h2" },
{ title: "H3", value: "h3" },
{ title: "H4", value: "h4" },
{ title: "Quote", value: "blockquote" }
],
lists: [{ title: "Bullet", value: "bullet" }],
// Marks let you mark up inline text in the block editor.
marks: {
// Decorators usually describe a single property – e.g. a typographic
// preference or highlighting by editors.
decorators: [
{ title: "Strong", value: "strong" },
{ title: "Emphasis", value: "em" }
],
// Annotations can be any object structure – e.g. a link or a footnote.
annotations: [
{
title: "URL",
name: "link",
type: "object",
fields: [
{
title: "URL",
name: "href",
type: "url"
}
]
}
]
}
},
// You can add additional types here. Note that you can't use
// primitive types such as 'string' and 'number' in the same array
// as a block type.
{
type: "image",
options: { hotspot: true }
}
]
};
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";
import blockContent from "./blockContent";
// 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,
blockContent
])
});
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.
Now, add the siteconfig to the /schemas/schema.js
as well.
// other schema content ...
import siteconfig from "./siteConfig";
export default createSchema({
// ...
types: schemaTypes.concat([
// ...,
siteconfig
])
});
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 =>
!["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.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 a 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 powerful and works well with Sanity as they are the creator of both.
Here's a sample page to fetch the data.
index.js
import Head from "next/head";
import { useRouter } from "next/router";
import { getClient, usePreviewSubscription } from "@lib/sanity";
import { groq } from "next-sanity";
const query = groq`
*[_type == "post"] | order(_createdAt desc) {
...,
author->,
categories[]->
}
`;
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>
))}
</>
);
}
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.
?preview
so that we can live preview without publishing.postdata
and used preview subscription when the user is authenticated🥂🥳 Yaaaayyy!!! Now refresh your browser and see your data.
Let's create an individual page using Next.js & Sanity.
/post/[slug.js]
import Image from "next/image";
import { useRouter } from "next/router";
import client, {
getClient,
usePreviewSubscription,
PortableText,
} from "@lib/sanity";
import ErrorPage from "next/error";
import GetImage from "@utils/getImage";
const singlequery = groq`
*[_type == "post" && slug.current == $slug][0] {
...,
author->,
categories[]->,
}
`;
const pathquery = groq`
*[_type == "post"] {
'slug': slug.current,
}
`;
export default function Post(props) {
const { postdata, preview } = props;
const router = useRouter();
const { slug } = router.query;
const { data: post } = usePreviewSubscription(singlequery, {
params: { slug: slug },
initialData: postdata,
enabled: preview || router.query.preview !== undefined,
});
if (!router.isFallback && !post?.slug) {
return <ErrorPage statusCode={404} />;
}
return (
<>
{post && (
<article className="max-w-screen-md mx-auto ">
<h1 className="mt-2 mb-3 text-3xl font-semibold tracking-tight text-center lg:leading-snug text-brand-primary lg:text-4xl dark:text-white">
{post.title}
</h1>
<p className="text-gray-800 dark:text-gray-400">
{post.author.name}
</p>
<div className="relative z-0 max-w-screen-lg mx-auto overflow-hidden lg:rounded-lg aspect-video">
{post?.mainImage && (
<Image
{...GetImage(post?.mainImage)}
placeholder="blur"
/>
)}
</div>
<div className="mx-auto my-3 prose prose-base dark:prose-invert prose-a:text-blue-500">
{post.body && <PortableText value={post.body} />}
</div>
</article>
)}
</>
);
}
export async function getStaticProps({ params, preview = false }) {
//console.log(params);
const post = await getClient(preview).fetch(singlequery, {
slug: params.slug,
});
return {
props: {
postdata: { ...post },
preview,
},
revalidate: 10,
};
}
export async function getStaticPaths() {
const allPosts = await client.fetch(pathquery);
return {
paths:
allPosts?.map((page) => ({
params: {
slug: page.slug,
},
})) || [],
fallback: true,
};
}
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 on Twitter
Sanity Composable Content Cloud is the headless CMS that gives you (and your team) a content backend to drive websites and applications with modern tooling. It offers a real-time editing environment for content creators that’s easy to configure but designed to be customized with JavaScript and React when needed. With the hosted document store, you query content freely and easily integrate with any framework or data source to distribute and enrich content.
Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.
let's create a simple migration using latest sanity migration cli tool.
Go to Update Schema Content using Latest Sanity Migration Tool