umma.dev

Setting Up Sanity CMS

Looking at two ways of setting up Sanity CMS with React applications.

Why Sanity?

Sanity is a simple CMS, easy to set up and navigate. There are many others, which have different use cases.

Quick Set Up with Next.js Blog Template

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

Set Up

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

Set Up From Scratch

Sanity Set Up

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

Next.js Set Up

npm install next react react-dom

Ensure your package.json looks as follows:

{
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }
}

Index.js

const Index = () => {
  return (
    <div>
      <p>Example Blog</p>
    </div>
  );
};

export default Index;

Dynamic Page Template

// 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.

Template for Index.js and Dynamic Routing

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.

Adding Authors and Categories

"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;

Adding Rich Text and Author Image

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;

Adding Blog Posts to Home Page

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;