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.
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.
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
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);
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.