How to Integrate Sanity with Next.js for Advanced Content Management

This post shows exactly how to render markdown for sanity cms into nextjs application using tailwind CSS ,react, code blocks etc.

Blog post By Prithviraj - Published at 12/23/2022, 4:02:39 AM

Next.js and Sanity are two of the most popular tools in the web development world. Together, they provide a seamless, efficient and powerful solution for building complex web applications with advanced content management capabilities.

In this article, we will explore how to integrate Sanity with Next.js to create a dynamic, scalable and easily maintainable web application.

Setting up a Sanity Studio

To get started, you'll need to set up a Sanity Studio. Sanity provides a free and easy-to-use platform for managing your content, making it ideal for web developers of all skill levels.

To create a Sanity Studio, simply navigate to the Sanity website, create an account, and select the "Start a new project" option. From there, you'll be able to choose from a variety of templates, customize your studio to fit your needs, and start managing your content.

Create markdown schema

In your studio add this markdown plugin:

yarn add sanity-plugin-markdown

then create a file mdBlog.js in the schemas directory and paste this:

export default {
    type: "document",
    name: "Content",
    title:"Blog Posts",
    fields: [
        { title: "Title", name: "title", type: "string" },{
        type: "markdown",
        title:"WhatYouWant",
        description: "Markdown Content supported with Image uploading",
        name: "bio",
        validation: (Rule) => Rule.required(),
      },
      {
        name: 'slug',
        title: 'Slug',
        type: 'slug',
        validation: (Rule) => Rule.required(),
        options: {
          source: 'title',
          maxLength: 96,
        },
      },
      {
        name:"IntroImage",
        title:"Image",
        type:"image"
      },
      {
        name: 'description',
        title: 'Description',
        type: 'string',
        validation: (Rule) => Rule.required(),
      },
    ]
  }

then add the schema in the schema file:

import mardownblog from "./mdblog"

export default [
  mardownblog,
]

then add the plugin in `sanity.config.js`:

// sanity.config.js
import { defineConfig } from "sanity";
import { deskTool } from "sanity/desk";
import schemas from "./schemas/schema";
import { visionTool } from "@sanity/vision";
import { markdownSchema } from "sanity-plugin-markdown";

export default defineConfig({
  title: "blog",
  projectId: "your id",
  dataset: "your dataset",
  plugins: [deskTool(),visionTool(),markdownSchema()],
  schema: {
    types: schemas,
  },
});
Note: This plugin is for sanity studio v3

Connect sanity with Nextjs

To do this we will be using the `next-sanity` package. like this:

yarn add next-sanity

then create a folder lib and create a file `sanity.js` and paste this:

import { createClient } from "next-sanity";
import createImageUrlBuilder from "@sanity/image-url";

export const config = {

  projectId: "your project id",
  dataset: "your name of dataset",
  apiVersion: "v1",
  token:
    process.env.token ||
    "your api token",
  useCdn: false,
  ignoreBrowserTokenWarning: true,
};

export const sanityClient = createClient(config);

export const urlFor = (source) => createImageUrlBuilder(config).image(source);

The Frontend part

Now for the frontend part, we will be using NextJs 13. It's still in beta and not recommended to use but still, I wanna show you a demo of what NextJs 13 brings. Let's begin

npx create-next-app@latest --experimental-app
# or
yarn create next-app --experimental-app
# or
pnpm create next-app --experimental-app

the `next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
  },
};

module.exports = nextConfig;

Create a head file app/head.js with a title and meta viewport tags to the file:

export default function Head() {
  return (
    <>
      <title>Blog</title>
      <title>Stoic</title>
      <link rel="icon" href="/image (2).svg" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta name="keywords" content={`blog ,coding blog`} />
      <meta
        name="description"
        content="Check out this Blog for programming content and about developer trends."
      />
    </>
  );
}

Create a root layout app/layout.js with the required <html> and <body> tags:


import './globals.css'
import { Inter } from '@next/font/google';
const inter = Inter({ subsets: ['latin'] })

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">

      <head />
      <body className={`${inter.className} sm:relative`}>{children}</body>
    </html>
  )
}

then in the app/page.tsx this:

import { getSanityContent } from "../lib/sanityclient";
import { DestinationCard } from "../components/main";

export default async function Index() {
  const data = await getSanityContent();
  return (
    <div>
      <div className="bg-gray-100 grid lg:grid-cols-2 2xl:grid-cols-5">
        <div
          className="px-8 py-1 max-w-md mx-auto sm:max-w-xl lg:px-12
         lg:py-24 lg:max-w-full xl:mr-0 2xl:col-span-2"
        >
          <div className="xl:max-w-xl">
            <img
              className="mt-3 rounded-lg shadow-xl sm:mt-8 sm:h-64 sm:w-full
               sm:object-cover object-center lg:hidden"
              src="/beach-work.webp"
              alt="Woman workcationing on the beach"
            />
            <h1
              className="mt-2 text-2xl font-headline tracking-tight 
            font-semibold text-gray-900 sm:mt-8 sm:text-4xl 
            lg:text-3xl xl:text-4xl"
            >
              You can work from anywhere.
              <br className="hidden lg:inline" />{" "}
              <span className="text-brand">Take advantage of it.</span>
            </h1>
            <p className="mt-2 text-gray-700 sm:mt-4 sm:text-xl">
              As a software engineer, I have a passion for all things related to
              code and technology, and I want to share my knowledge with the
              world. Whether you're a beginner looking to learn the basics or an
              experienced developer looking for new insights and tips, I hope
              you'll find something valuable here. I'll be covering a wide range
              of topics, from front-end web development to back-end server-side
              programming, and everything in between. Thank you for visiting,
              and I look forward to sharing my knowledge with you!
            </p>
            <div className="mt-4 space-x-1 sm:mt-6">
              <a
                className="inline-block px-5 py-3 rounded-lg transform transition bg-gray-600 hover:bg-gray-500 hover:-translate-y-0.5 focus:ring-black focus:ring-opacity-50 focus:outline-none focus:ring focus:ring-offset-2 active:bg-slate-200 uppercase tracking-wider font-semibold text-sm text-white shadow-lg sm:text-base"
                href="/"
              >
                Enjoy This Shit
              </a>
            </div>
          </div>
        </div>
        <div className="hidden relative lg:block 2xl:col-span-3">
          <img
            className="absolute inset-0 w-full h-full object-cover object-center"
            src="/beach-work.webp"
            alt="Woman workcationing on the beach"
          />
        </div>
      </div>

      <div className="max-w-md sm:max-w-xl lg:max-w-6xl mx-auto px-8 lg:px-12 py-8">
        <h2 className="text-xl text-gray-900">Popular Articles</h2>
        <p className="mt-2 text-gray-600">
          {" "}
          A series of newest blog post increase the scale of your consiousness
        </p>
        <div className="mt-6 grid gap-5 lg:grid-cols-1  xl:grid-cols-2">
          {data.map((post: any) => (
            <DestinationCard key={post._id} {...post} />
          ))}
        </div>
      </div>
    </div>
  );
}

then create a folder `[slug]` and inside it create a page.tsx file

import dynamic from "next/dynamic";
const MarkComponent = dynamic(() => import("../../../components/Markdown"), {
  ssr: false,
});
import { getSanityContentBySLug, getSanityContent } from "../../../lib/sanityclient";

export const revalidate = 15;

export default async function Page({ params }: any) {
  const content = await getStaticContent(params);

  return (
    <div className="prose font-[0.8rem] py-16 px-6 sm:px-8 sm:prose-sm md:prose-base mx-auto lg:prose-lg  hover:prose-a:text-blue-500">
      <MarkComponent children={content} />
    </div>
  );
}
async function getStaticContent(params: any) {
  // console.log(params.slug);

  const data = await getSanityContentBySLug(params.slug);
  // console.log(data);

  const mdxSource = data[0].bio;
  const content = mdxSource;

  //   console.log(data);

  return content;
}
export async function generateStaticParams() {
  const data = await getSanityContent();

  return data.map((post: any) => ({
    slug: post.slug.current,
  }));
}

then finally add these files to the components directory

`main.tsx`

"use client";
import Link from "next/link";
import Image from "next/image";
import { useRef } from "react";

import { urlFor } from "../lib/sanity";

export function Codeblock({ children, className }: any) {
  const preRef = useRef<HTMLDivElement>(null);
  function copy() {
    const content = preRef.current?.textContent ?? "";
    navigator.clipboard.writeText(content);
  }
  return (
    <div className="grid  relative">
      <div className="flex justify-end items-center">
        <button
          onClick={copy}
          data-tooltip-target="button-pills-example-copy-clipboard-tooltip"
          data-tooltip-placement="bottom"
          type="button"
          data-copy-state="copy"
          className="absolute  px-3  -right-5 py-2 top-0 text-xs font-medium text-gray-600 bg-gray-100 border-gray-200 dark:border-gray-600 dark:text-gray-400 dark:bg-gray-800 hover:text-blue-700 dark:hover:text-white copy-to-clipboard-button"
        >
          <svg
            className="w-4 h-4 "
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
            ></path>
          </svg>{" "}
          <span className="copy-text "></span>
        </button>
      </div>

      <code ref={preRef} lang={className}>
        {children}
      </code>
    </div>
  );
}

export function DestinationCard(post: any) {
  // console.log(destination.slug.current);

  return (
    <div className="flex items-center rounded-lg bg-white shadow-lg overflow-hidden ">
      <Image
        className=" h-24 w-24 sm:h-32 sm:w-32 flex-shrink-0 shadow-2xl p-3 border-t-2 border-b-2 border-l-2"
        alt="MarkdownBlog"
        loading="lazy"
        width={96}
        height={96}
        src={
          post.IntroImage
            ? urlFor(post.IntroImage).width(96).height(96).url()!
            : "https://avatars.githubusercontent.com/u/87182486?s=40&v=4"
        }
      />

      <div className="px-5 py-3 flex flex-col ">
        <h3 className="sm:text-lg text-base  inline font-semibold text-gray-800">
          {post.title}
        </h3>
        <span className="truncate ">{`${post.description}`} </span>

        <div className="mt-4 ">
          <Link
            href={`/posts/${post.slug.current}`}
            className="text-gray-900 hover:text-gray-500 font-semibold text-base"
          >
            Explore {} more
          </Link>
        </div>
      </div>
    </div>
  );
}

Markdown.tsx

import Image from "next/image";
import Link from "next/link";
import ReactMarkdown from "react-markdown";
import { Codeblock } from "./main";

export const components = {
  a: (a: { href: string; children: any }) => {
    return (
      <Link href={`${a.href}`} target="_blank">
        {a.children?.toString()}{" "}
      </Link>
    );
  },
  img: (img: { src: any; alt: any }) => {
    return (
      <div className="relative mx-auto h-auto w-full">
        <Image
          className="object-fill"
          priority
          src={img.src}
          alt={img.alt}
          width={800}
          height={400}
        />
      </div>
    );
  },
  code: ({ children, node, className }: any) => {
    return <Codeblock children={children} className={className} />;
  },
};
export default function Reactmarkdown({ children }: any) {
  return <ReactMarkdown children={children} components={components as any} />;
}

and finally the sanityclient.ts

import { sanityClient } from "./sanity";

export async function getSanityContent() {

  const query = `*[_type == 'Content'  ] { _id, IntroImage,bio,title,description, slug{
    current
  } }`;

  const posts = await sanityClient.fetch(query);

  return posts;
}
export async function getSanityContentBySLug(slug: String) {
 
  const query = `*[_type == 'Content'  &&  slug.current==$slug] { bio,slug{current}} `;
  const result = await sanityClient.fetch(query,{
    slug:slug,
  });
  return result;
}

the tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
    './app/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      typography: (theme) => ({
        DEFAULT: {
          css: {
            color: theme('colors.gray.700'),
            a: {
              color: theme('colors.primary.500'),
              '&:hover': {
                color: `${theme('colors.primary.600')} !important`,
              },
              code: { color: theme('colors.primary.400') },
            },
            h1: {
              fontWeight: '700',
              letterSpacing: theme('letterSpacing.tight'),
              color: theme('colors.gray.900'),
            },
            h2: {
              fontWeight: '700',
              letterSpacing: theme('letterSpacing.tight'),
              color: theme('colors.gray.900'),
            },
            h3: {
              fontWeight: '600',
              color: theme('colors.gray.900'),
            },
            'h4,h5,h6': {
              color: theme('colors.gray.900'),
            },
            pre: {
              backgroundColor: theme('colors.gray.800'),
            },
            code: {
              color: theme('colors.cyan.500'),
              backgroundColor: theme('colors.gray.100'),
              paddingLeft: '4px',
              paddingRight: '4px',
              paddingTop: '2px',
              paddingBottom: '2px',
              borderRadius: '0.25rem',
            },
            'code::before': {
              content: 'none',
            },
            'code::after': {
              content: 'none',
            },
            details: {
              backgroundColor: theme('colors.gray.900'),
              paddingLeft: '4px',
              paddingRight: '4px',
              paddingTop: '2px',
              paddingBottom: '2px',
              borderRadius: '0.25rem',
            },
            hr: { borderColor: theme('colors.gray.200') },
            'ol li::marker': {
              fontWeight: '600',
              color: theme('colors.gray.900'),
            },
            'ul li::marker': {
              backgroundColor: theme('colors.gray.900'),
            },
            strong: { color: theme('colors.gray.600') },
            blockquote: {
              color: theme('colors.gray.900'),
              borderLeftColor: theme('colors.gray.500'),
            },
          },
        },
        dark: {
          css: {
            color: theme('colors.gray.300'),
            a: {
              color: theme('colors.primary.500'),
              '&:hover': {
                color: `${theme('colors.primary.400')} !important`,
              },
              code: { color: theme('colors.primary.400') },
            },
            h1: {
              fontWeight: '700',
              letterSpacing: theme('letterSpacing.tight'),
              color: theme('colors.gray.100'),
            },
            h2: {
              fontWeight: '700',
              letterSpacing: theme('letterSpacing.tight'),
              color: theme('colors.gray.100'),
            },
            h3: {
              fontWeight: '600',
              color: theme('colors.gray.100'),
            },
            'h4,h5,h6': {
              color: theme('colors.gray.100'),
            },
            pre: {
              backgroundColor: theme('colors.gray.800'),
            },
            code: {
              backgroundColor: theme('colors.gray.800'),
            },
            details: {
              backgroundColor: theme('colors.gray.800'),
            },
            hr: { borderColor: theme('colors.gray.700') },
            'ol li::marker': {
              fontWeight: '600',
              color: theme('colors.gray.400'),
            },
            'ul li::marker': {
              backgroundColor: theme('colors.gray.400'),
            },
            strong: { color: theme('colors.gray.100') },
            thead: {
              th: {
                color: theme('colors.gray.100'),
              },
            },
            tbody: {
              tr: {
                borderBottomColor: theme('colors.gray.700'),
              },
            },
            blockquote: {
              color: theme('colors.gray.100'),
              borderLeftColor: theme('colors.gray.700'),
            },
          },
        },
      }),
    },
  },
  plugins: [
    require('@tailwindcss/typography'),
    // ...
  ],
}

Hope you all this guide helped to get your projects done.