Last active
October 5, 2023 08:31
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//sanity/schemas/index.ts | |
import resource from "./resource.schema"; | |
const schemas = [resource]; | |
export default schemas; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 © Your Name 2023 | |
</footer> | |
</> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 }), | |
], | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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