Create a simple blog app using SanityCMS, GROQ, ISR & TypeScript and Tailwind CSS

Setup

npx craete-next-app --example with-tailwindcss projectname
cd projectname
  • run this yet
npm install -g @sanity/cli

this command allows me to have cli tools which means i can run stuff inside the terminal

  • this command will let me initialize a sanity studio inside this diretcory
sanity init --coupon sonny2022
  • when asks to choose project templates: choose blog (schema) to shortcut the process

  • after this you can see your project name in your sanity projects

  • you'll see that the index and app are in txs format - typescript and not javascript, the browser doesn't actually understand that but next js prepares all that behind the scenes and this is called transpiing when i save the file it will be automatically translated to javascript

npm run dev

using Link from next js will prefetch the page by default, which means it will make it super fast because its already fitched

cd sanityproject

this runs up our local studio

sanity start

after creating posts and authors we want to pull them into our project

  • in the vision of the sanity add a query of all needed data that you want to fetch
*[_type == "post"]{
  _id,
  title,
  slug,
  author -> {
  name,
  image,
}
}
npm install next-sanity

to connect everything together go to package.json public file and add:

npm install @sanity/image-url

create sanity.js file same level as package.json

add these imports:

import {
    createCurrentUserHook,
    createClient,
} from 'next-sanity'; 

import createImageUrlBuilder from '@sanity/image-url'
export const config = {
    // sanity id is found on sanity.json in the sanity project
    dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
    projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
    apiVersion: "2021-03-25",
    useCdn: process.env.NODE_ENV === "production",
}

add this to sanity file:

// to fetch data
export const sanityClient = createClient(config)


// to extract the image urls
export const urlFor = (source) => createImageUtlBuilder(config).image(source)

create .env.local file and create variables of NEXT_PUBLIC_SANITY_DATASET, you will find this in your sanity.json file in the sanity project folder as dataset and the other one NEXT_PUBLIC_SANITY_PROJECT_ID as projectid

make sure you start these names by NEXT_PUBLIC, that will enhance the security of your data

start with server side render at the home page that means its going to be rendered by request of anyone who visits the home page

go to home page or any page you want to make the SSR in add a new function : import the sanityClient from sanity.js

export const getServerSideProps = async () => {
  const query = `*[_type == "post"]{
    _id,
    title,
    slug,
    author -> {
    name,
    image,
  },
  mainImage,
  slug
  }`;

  const posts = await sanityClient.fetch(query);
};

return props in the same function

return {
    props: {
      posts,
    },
  }

and pass it in the main function with defining it as interface

interface Props {
  posts: [Post];
}


function Hero({posts}: Props)
  • create a new file called typings.d.tx:
// definistion typescript file

export interface Post {
  _id: string;
  _createdAt: string;
  title: string;
  author: {
    name: string;
    image: string;
  };
  mainImage: {
    asset: {
      url: string;
    };
  };

  description: string;
  slug: {
    current: string;
  };
  body: [object];
}

now we can make a map for the posts and use it in the jsx

after this we need to create a dynamic post page -> for post or blog details, create a folder in the pages directory (post) and inside it create [slug].tsx component.

we are going to use a special function called get static paths, this is going to allow next js to know which routes it should pre build


import { sanityClient, urlFor } from "../../sanity";
import { Post } from "../../typings";

function Post() {
  return <div></div>;
}

export default Post;

export const getStaticPaths = async () => {
  const query = `*[_type == "post"]{
        _id,
        slug,
      }`;

  const posts = await sanityClient.fetch(query);

  const paths = posts.map((post: Post) => ({
    params: {
      slug: post.slug.current,
    },
  }));
  return {
    paths,
    fallback: "blocking",
  };
};
// we need to provide the props as array
// for every single post i want to return an object
// the first is a params and the second one matches the fle name (slug)

// the structure will be an array of objects each have a slug
  • in the same page, add this function for getting the data from each post
export const getStaticProps: GetStaticProps = async ({params}) => {
    const query = `
    *[_type == "post" && slug.current == $slug][0]{
        _id,
        _createdAt,
        title,
        author => {
        name,
        image,
      },
      description,
      mainImage,
        slug,
      body
      }`
      const post = await sanityClient.fetch(query, {
        slug: params?.slug,
      });

      if (!post) {
        return {
            notFound: true
        } 
      }

      return {
        props: {
            post,
        }
      }
}

now a new page is generated for each post that I have. but until now it won't take changes, so we have to do the following to regenerate the page every 60 seconds:

just add revalidate: 60 with the props

install this and import it in the page so we can render the body field

npm install react-portable-text

continue editing the blog article and create serializers for the body


import { GetStaticProps } from "next";
import { sanityClient, urlFor } from "../../sanity";
import { Post } from "../../typings";
import Header from '../../components/Header'
import PortableText from 'react-portable-text'

interface Props {
    post: Post;
}

function Post({ post }: Props) {
    return <div>
        <Header />

        <img src={urlFor(post.mainImage).url()!} className="object-cover w-full h-40"/>
        <article className="max-w-4xl p-5 mx-auto ">
            <h1 className="mt-10 mb-3 text-3xl">{post.title}</h1>
            <h2 className="mb-2 text-xl font-light text-gray-500">{post.description}</h2>

            <div className="flex items-center space-x-2">
                <img src={urlFor(post.author.image).url()!} 
                className="w-10 h-10 rounded-full"/>
                <p className="text-sm font-light text-gray-500">Blog post by {" "} {post.author.name} - Published at {" "}
                {new Date(post._createdAt).toLocaleString()}</p>

            </div>

            <div>

                {/* this is how we render the body */}
            <PortableText
            className="h-screen p-3 mt-10 tracking-wider "
            dataset={process.env.NEXT_PUBLIC_SANITY_DATASET!}
            projectId={process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!}
            content={post.body}
            serializers={{
                h1: (props: any) => (
                    <h1 className="my-5 text-2xl " {...props}/>
                ),
                h2: (props: any) => (
                    <h1 className="my-5 text-xl " {...props}/>
                ),
                p: (props: any) => (
                    <p className="my-5 " {...props}/>
                ),
                li: ({childern} : any) => (
                    <li className="ml-4 list-disc">{childern}</li>
                ),
                link: ({href, children}: any) => (
                    <a href={href} className="text-blue-900 hover:underline" >{children}</a>
                ),
            }}
            />
            </div>
        </article>

    </div>;
}

export default Post;


// this function is to find existing posts
export const getStaticPaths = async () => {
  const query = `*[_type == "post"]{
        _id,
        slug,
      }`;

  const posts = await sanityClient.fetch(query);

  const paths = posts.map((post: Post) => ({
    params: {
      slug: post.slug.current,
    },
  }));
  return {
    paths,
    fallback: "blocking",
  };
};
// we need to provide the props as array
// for every single post i want to return an object
// the first is a params and the second one matches the fle name (slug)

// the structure will be an array of objects each have a slug



// this function is for getting the data from each post
export const getStaticProps: GetStaticProps = async ({params}) => {
    const query = `
    *[_type == "post" && slug.current == $slug][0]{
        _id,
        _createdAt,
        title,
        author -> {
        name,
        image,
      },
      description,
      mainImage,
        slug,
      body
      }`
      const post = await sanityClient.fetch(query, {
        slug: params?.slug,
      });

      if (!post) {
        return {
            notFound: true
        } 
      }

      return {
        props: {
            post,
        },
        revalidate: 60, // after 60 seconds it will update the page (for catching updates)
      }
}

now after this is finished we will start making the API form that handles commenting on the blog post:

in the same page add the form jsx

<form className="flex flex-col max-w-2xl p-5 mx-auto my-10" onSubmit={handleSubmit(onSubmit)}>
        <h3 className="text-2xl">Leave a comment below!</h3>
        <hr className="py-3 mt-2" />

        <input type="hidden" {...register("_id")} name="_id" value={post._id} />

        <label className="mb-5 ">
          <input
            {...register("name", { required: true })}
            className="w-full px-3 py-1 border rounded shadow-sm focus:outline-blue-200 "
            placeholder="Name*"
            type="text"
          />
        </label>

        <label className="mb-5 ">
          <input
            {...register("email", { required: true })}
            className="w-full px-3 py-1 border rounded shadow-sm focus:outline-blue-200"
            placeholder="Email*"
            type="email"
          />
        </label>

        <label className="mb-5 ">
          <textarea
            {...register("comment", { required: true })}
            className="w-full px-3 py-10 border rounded shadow-sm focus:outline-blue-200"
            placeholder="Comment..."
          />
        </label>

        <div className="flex flex-col p-5">
          {errors.name && (
            <p className="text-sm text-red-400">Name field is required!</p>
          )}
          {errors.email && (
            <p className="text-sm text-red-400">Email field is required!</p>
          )}
          {errors.comment && (
            <p className="text-sm text-red-400">Comment field is required!</p>
          )}
        </div>

        <button className="p-3 text-white duration-200 ease-in-out bg-black border rounded-full cursor-pointer hover:bg-white hover:text-black hover:border-black" type="submit">Send</button>
      </form>

you have to install:

npm install react-hook-form

and import the following hooks to handle the form

import { useForm, SubmitHandler } from "react-hook-form";
  • define the interface of which data we want to be renderd:
interface IFormInput {
  _id: string;
  name: string;
  email: string;
  comment: string;
}

for the id field, we will create a hidden input for that and then pass the register to the rest of inputs. note: if we have a non required input we can add a question mark before its name in the interface ?name: string ..

function Post({ post }: Props) {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<IFormInput>();

start to create an api route:

  // special submit handler
  const onSubmit: SubmitHandler<IFormInput> = async(data)=>{
    await fetch('/api/createComment', {
        method: 'POST',
        body: JSON.stringify(data),
    }).then((res)=>{
        console.log(res)
    }).catch((err)=>{
        console.log(err)
    })
  • create a tsx file in the api folder with the same name in the url

copy the content of the hello.tsx file inside the createcomment but remove the stuff, then copy the config function from sanity.js

install sanityClient

npm install @sanity/client

import it this way:

import sanityClient from "@sanity/client";
  • go get the sanity token from the sanity dashboard in the browser, from API go to add token, enter a name and choose editor then copy the token

  • this api file should look like this:

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
import sanityClient from "@sanity/client";

export const config = {
    dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
    projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
    useCdn: process.env.NODE_ENV === "production",
    token: process.env.SANITY_API_TOKEN,
  };

  const client = sanityClient(config)

export default async function createComment(
  req: NextApiRequest,
  res: NextApiResponse
) {
  res.status(200).json({ name: 'John Doe' })
}

now restart the localhost server and check for updates

  • create a new schema for the comment
const { _id, name, email, comment } = JSON.parse(req.body);

  try {
    await client.create({
      _type: "comment",
      post: {
        _type: "reference",
        _ref: _id,
      },
      name,
      email,
      comment,
    });
  } catch (err) {
    return res.status(500).json({ message: "Comment not sent", err });
  }
  console.log("submitted");
  return res.status(200).json({ message: "Comment sent!" });
}

after this we need to update the sanity schema new file in the schema : comment.js

export default {
  name: "comment",
  type: "document",
  title: "comment",
  fields: [
    {
      name: "name",
      type: "string",
    },
    {
      title: "Approved",
      name: "approved",
      type: "boolean",
      description: "comments won't show on the site without approval",
    },
    {
      name: "email",
      type: "string",
    },
    {
      name: "comment",
      type: "text",
    },
    {
      name: "post",
      type: "reference",
      to: [{ type: "post" }],
    },
  ],
};

go to schema.js file and import the new schema:

import comment from './comment'

here and in the types section

you can now see it in the desk of sanity in the browser with all our specified fields

now you can create a state variable with false initial value to track the sent messages, you can make ma condition to render the form only when its false else render a message saying comment was sent, we can change the value of the state whenever we press the button in the onsubmit function.

add this comment field to the query

'comments': *[
  _type == 'comment' &&
  post._ref == ^._id ,
  approved == true
],

to check if the post reference equals the id and make sure you approve the comments before from the comment schema

go back to the typings.b.ts and add the ( comments: Comment[];) then add another export


export interface Comment {
  approved: boolean;
  comment: string;
  email: string;
  name: string;
  post: {
    _ref: string;
    _type: string;
  };
  _createdAt: string;
  _id: string;
  _rev: string;
  _type: string;
  _updatedAt: string;
}

Deploying

  • cd into the sanity folder

  • run sanity deploy

  • give it a name

the deployed url of the sanity project
https://blogit.sanity.studio/desk


create a github repo and initialize it there then push it to guithub

--> deployed link
[https://blogit-8asp55ov9-dialaabulkhail.vercel.app/](https://blogit-8asp55ov9-dialaabulkhail.vercel.app/)