Skip to content

Instantly share code, notes, and snippets.

@ndrvndr
Last active October 5, 2023 08:31
Show Gist options
  • Save ndrvndr/7c61e56314a05115872b3f0c0e0f3f72 to your computer and use it in GitHub Desktop.
Save ndrvndr/7c61e56314a05115872b3f0c0e0f3f72 to your computer and use it in GitHub Desktop.
Building a Blog Using Sanity Studio V3 and Next.js 13 (App Router)
//sanity/action.ts
import { groq } from "next-sanity";
import { readClient, writeClient } from "./lib/client";
import { buildQuery } from "./utils";
interface GetResourcesParams {
query: string;
tags: string;
page: string; //pagination
}
export const getResources = async (params: GetResourcesParams) => {
// destructure the params
const { query, tags, page } = params;
try {
const resources = await readClient.fetch(
groq`${buildQuery({
type: "resource",
query,
tags,
page: parseInt(page),
})}{
_id,
title,
slug,
readingTime,
views,
releaseDate,
overview,
"image": poster.asset->url,
tags,
content,
}`
);
return resources;
} catch (error) {
console.error(error);
}
};
export async function incrementViews(blogId: string): Promise<number> {
try {
const currentViewsQuery = groq`*[_type == "resource" && _id == $blogId][0].views`;
const currentViews = await readClient.fetch<number>(currentViewsQuery, {
blogId,
});
const updatedViews = currentViews + 1;
const patchOperation = {
set: {
views: updatedViews,
},
};
await writeClient.transaction().patch(blogId, patchOperation).commit();
return updatedViews;
} catch (error) {
console.error(error);
throw error;
}
}
"use client";
import { ResourceItem } from "@/app/(root)/(home)/page";
import { incrementViews } from "@/sanity/action";
import { format } from "date-fns";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
export default function BlogCard({ resource }: { resource: ResourceItem }) {
const [localViews, setLocalViews] = useState(resource.views);
const handleCardClick = async () => {
try {
const updateViews = await incrementViews(resource._id);
setLocalViews(updateViews);
} catch (error) {
console.error(error);
}
};
return (
<li className='h-full w-full rounded-md border border-solid border-black'>
<Link href={resource.slug.current} onClick={handleCardClick}>
<div className='relative'>
<Image
src={resource.image}
alt='Photo taken from Unsplash'
width={1200}
height={480}
className='h-auto w-auto rounded-t-md'
priority={true}
/>
<div className='absolute bottom-3 right-3 flex gap-1'>
{resource.tags.map((tag) => (
<span
key={tag}
className='rounded-md bg-white px-2 py-1 text-xs '
>
{tag}
</span>
))}
</div>
</div>
<div className='p-4'>
<h1 className='font-bold md:text-lg'>{resource.title}</h1>
<div className='mt-2 flex gap-2 text-sm font-medium'>
{resource.readingTime}
<span>{localViews.toLocaleString() ?? "0"} views</span>
</div>
<p className='mb-2 mt-4 text-sm font-bold'>
{format(new Date(resource.releaseDate), "MMMM dd, yyyy")}
</p>
<p className='text-sm'>{resource.overview}</p>
</div>
</Link>
</li>
);
}
import { createClient } from "next-sanity";
import { apiVersion, dataset, projectId, token, useCdn } from "../env";
export const readClient = createClient({
apiVersion,
dataset,
projectId,
useCdn,
});
export const writeClient = createClient({
apiVersion,
dataset,
projectId,
useCdn,
token,
});
//sanity/env.ts
export const apiVersion =
process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2023-10-03";
export const dataset = assertValue(
process.env.NEXT_PUBLIC_SANITY_DATASET,
"Missing environment variable: NEXT_PUBLIC_SANITY_DATASET"
);
export const projectId = assertValue(
process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
"Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID"
);
export const token = assertValue(
process.env.NEXT_PUBLIC_SANITY_TOKEN,
"Missing environment variable: NEXT_PUBLIC_SANITY_TOKEN"
);
export const useCdn = false;
function assertValue<T>(v: T | undefined, errorMessage: string): T {
if (v === undefined) {
throw new Error(errorMessage);
}
return v;
}
//sanity/schemas/index.ts
import resource from "./resource.schema";
const schemas = [resource];
export default schemas;
app/(root)/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<nav className='max-w-7xl m-auto p-8 flex justify-between'>
<span>My Blog</span>
<ul className='flex gap-8'>
<li>Home</li>
<li>About</li>
<li>Contact</li>
</ul>
</nav>
{children}
<footer className='w-fit m-auto py-8'>
copyright &copy; Your Name 2023
</footer>
</>
);
}
app/(root)/(home)/[...slug]/page.tsx
import { getResources } from "@/sanity/action";
import { format } from "date-fns";
import Image from "next/image";
const BlockContent = require("@sanity/block-content-to-react");
export default async function BlogDetail({
params,
}: {
params: { slug: string };
}) {
const resource = await getResources({
query: params.slug,
tags: "",
page: "1",
});
return (
<main className='min-h-screen max-w-7xl m-auto p-8'>
<Image
src={resource[0].image}
alt='Photo taken from Unsplash'
width={1200}
height={480}
priority={true}
className='rounded-lg'
/>
<h1 className='mt-5 text-2xl font-bold md:text-4xl'>
{resource[0].title}
</h1>
<p className='mb-5 text-sm font-bold'>
Written on {format(new Date(resource[0].releaseDate), "MMMM dd, yyyy")}{" "}
by Andre Avindra
</p>
<div className='flex gap-4 mb-8'>
<span className='gradient__text'>{resource[0].readingTime}</span>
{resource[0].views?.toLocaleString() ?? "0"} views
</div>
<BlockContent
blocks={resource[0].content}
projectId={process.env.NEXT_PUBLIC_SANITY_PROJECT_ID}
dataset={process.env.NEXT_PUBLIC_SANITY_DATASET}
/>
</main>
);
}
//sanity/schemas/resource.schema.ts
const schema = {
name: "resource",
title: "Resource",
type: "document",
fields: [
{
name: "title",
title: "Title",
type: "string",
validation: (Rule: any) => Rule.required(),
},
{
name: "slug",
title: "Slug",
type: "slug",
options: { source: "title" },
validation: (Rule: any) => Rule.required(),
},
{
name: "readingTime",
title: "Reading Time",
type: "string",
validation: (Rule: any) => Rule.required(),
},
{
name: "views",
title: "Views",
type: "number",
initialValue: 0,
},
{
name: "releaseDate",
title: "Release Date",
type: "date",
options: {
dateFormat: "MM-DD-YYYY",
calendarTodayLabel: "Today",
},
validation: (Rule: any) => Rule.required(),
},
{
name: "overview",
title: "OverView",
type: "string",
validation: (Rule: any) => Rule.required(),
},
{
name: "poster",
title: "Poster",
type: "image",
options: {
hotspot: true,
},
validation: (Rule: any) => Rule.required(),
},
{
name: "tags",
title: "Tags",
type: "array",
of: [
{
type: "string",
},
],
validation: (Rule: any) => Rule.required().min(1),
},
{
name: "content",
title: "Content",
type: "array",
of: [
{ type: "block" },
{
type: "image",
fields: [
{
type: "text",
name: "alt",
title: "Alternative Text",
},
],
},
],
},
],
};
export default schema;
/**
* This configuration is used to for the Sanity Studio that’s mounted on the `\src\pages\studio\[[...index]].tsx` route
*/
import { visionTool } from "@sanity/vision";
import { defineConfig } from "sanity";
import { deskTool } from "sanity/desk";
import { codeInput } from "@sanity/code-input";
// Go to https://www.sanity.io/docs/api-versioning to learn how API versioning works
import { apiVersion, dataset, projectId } from "./sanity/env";
import schemas from "./sanity/schemas";
export default defineConfig({
basePath: "/studio",
projectId,
dataset,
// Add and edit the content schema in the './sanity/schema' folder
schema: { types: schemas },
plugins: [
codeInput(),
deskTool(),
// Vision is a tool that lets you query your content with GROQ in the studio
// https://www.sanity.io/docs/the-vision-plugin
visionTool({ defaultApiVersion: apiVersion }),
],
});
//sanity/utils.ts
interface BuildQueryParams {
type: string;
query: string;
tags: string;
page: number;
perPage?: number;
}
export const buildQuery = (params: BuildQueryParams) => {
const { type, query, tags, page = 1, perPage = 20 } = params;
const conditions = [`*[_type=="${type}"`];
if (query) conditions.push(`title match "*${query}*"`);
if (tags && tags !== "all") {
conditions.push(`tags == "${tags}"`);
}
//pagination
const offset = (page - 1) * perPage;
const limit = perPage;
return conditions.length > 1
? `${conditions[0]} && (${conditions
.slice(1)
.join(" && ")})][${offset}...${limit}]`
: `${conditions[0]}][${offset}...${limit}]`;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment