Looking at two ways of setting up Sanity CMS with React applications.
Sanity is a simple CMS, easy to set up and navigate. There are many others, which have different use cases.
Vercel provide a blog starter kit, which enables you to select a CMS to set it up with.
Click here to view the template.
npx create-next-app --example blog-starter blog-starter-app
Clone the repo and run the following command in the root of the project directory.
npx vercel link
Download environment variables to connect Next.js and Sanity.
npx vercel env pull
Run Next.js locally (in dev mode).
npm install && npm run dev
Blog can be viewed on: http://localhost:3000 CMS can be viewed on: http://localhost:3000/studio
Deploying to production:
git add .
git commit
git push
You can deploy using Vercel CLI too:
npx vercel --prod
npm i -g @sanity/cli
sanity init
$ Select project to use: Create new project
$ Informal name for your project: example-blog
$ Name of your first data set: production
$ Output path: ~/Sites/my-blog/studio
$ Select project: template Blog (schema)
sanity start
npm install next react react-dom
Ensure your package.json looks as follows:
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
}
const Index = () => {
return (
<div>
<p>Example Blog</p>
</div>
);
};
export default Index;
// client.js
import sanityClient from "@sanity/client";
export default sanityClient({
projectId: "my-id", // found in sanity.json
dataset: "production",
useCdn: true, // set to false if you want to ensure fresh data
});
Create a post file within the pages folder.
import { useRouter } from "next/router";
const Post = () => {
const router = useRouter();
return (
<article>
<h1>{router.query.slug}</h1>
</article>
);
};
export default Post;
If you navigate to localhost:3000/post?slug=something
you should be able to see something
written as the header.
import client from "../../client";
const Post = ({ post }) => {
return (
<article>
<h1>{post?.slug?.current}</h1>
</article>
);
};
export async function getStaticPaths() {
const paths = await client.fetch(
`*[_type == "post" && defined(slug.current)][].slug.current`
);
return {
paths: paths.map((slug) => ({ params: { slug } })),
fallback: true,
};
}
export async function getStaticProps(context) {
const { slug = "" } = context.params;
const post = await client.fetch(
`
*[_type == "post" && slug.current == $slug][0]
`,
{ slug }
);
return {
props: {
post,
},
};
}
export default Post;
Reminder: getStaticProps
and getStaticPaths
work only in files in the pages folder and are used for routing.
"author": {
"_ref": "fdbf38ad-8ac5-4568-8184-1db8eede5d54",
"_type": "reference"
}
// [slug].js
import groq from "groq";
import client from "../../client";
const Post = ({ post }) => {
const { title = "Missing title", name = "Missing name", categories } = post;
return (
<article>
<h1>{title}</h1>
<span>By {name}</span>
{categories && (
<ul>
Posted in
{categories.map((category) => (
<li key={category}>{category}</li>
))}
</ul>
)}
</article>
);
};
const query = groq`*[_type == "post" && slug.current == $slug][0]{
title,
"name": author->name,
"categories": categories[]->title
}`;
export async function getStaticPaths() {
const paths = await client.fetch(
groq`*[_type == "post" && defined(slug.current)][].slug.current`
);
return {
paths: paths.map((slug) => ({ params: { slug } })),
fallback: true,
};
}
export async function getStaticProps(context) {
const { slug = "" } = context.params;
const post = await client.fetch(query, { slug });
return {
props: {
post,
},
};
}
export default Post;
import groq from "groq";
import imageUrlBuilder from "@sanity/image-url";
import { PortableText } from "@portabletext/react";
import client from "../../client";
function urlFor(source) {
return imageUrlBuilder(client).image(source);
}
const ptComponents = {
types: {
image: ({ value }) => {
if (!value?.asset?._ref) {
return null;
}
return (
<img
alt={value.alt || " "}
loading="lazy"
src={urlFor(value).width(320).height(240).fit("max").auto("format")}
/>
);
},
},
};
const Post = ({ post }) => {
const {
title = "Missing title",
name = "Missing name",
categories,
authorImage,
body = [],
} = post;
return (
<article>
<h1>{title}</h1>
<span>By {name}</span>
{categories && (
<ul>
Posted in
{categories.map((category) => (
<li key={category}>{category}</li>
))}
</ul>
)}
{authorImage && (
<div>
<img
src={urlFor(authorImage).width(50).url()}
alt={`${name}'s picture`}
/>
</div>
)}
<PortableText value={body} components={ptComponents} />
</article>
);
};
const query = groq`*[_type == "post" && slug.current == $slug][0]{
title,
"name": author->name,
"categories": categories[]->title,
"authorImage": author->image,
body
}`;
export async function getStaticPaths() {
const paths = await client.fetch(
groq`*[_type == "post" && defined(slug.current)][].slug.current`
);
return {
paths: paths.map((slug) => ({ params: { slug } })),
fallback: true,
};
}
export async function getStaticProps(context) {
const { slug = "" } = context.params;
const post = await client.fetch(query, { slug });
return {
props: {
post,
},
};
}
export default Post;
import Link from "next/link";
import groq from "groq";
import client from "../client";
const Index = ({ posts }) => {
return (
<div>
<h1>Welcome to a blog!</h1>
{posts.length > 0 &&
posts.map(
({ _id, title = "", slug = "", publishedAt = "" }) =>
slug && (
<li key={_id}>
<Link href="/post/[slug]" as={`/post/${slug.current}`}>
<a>{title}</a>
</Link>{" "}
({new Date(publishedAt).toDateString()})
</li>
)
)}
</div>
);
};
export async function getStaticProps() {
const posts = await client.fetch(groq`
*[_type == "post" && publishedAt < now()] | order(publishedAt desc)
`);
return {
props: {
posts,
},
};
}
export default Index;