Using Sanity CMS with Next.js App Router 1
Have been using this for a while, really handy with clients who are looking for a custom solution with control over the content of the website (blogs, posts, team members, others...).
Setup is straight forward, but writing the configs, schemas and queries does become time consuming for each new project (given how often I tend to use it with clients). Currently planning to create a template repo with next.js, sanity, shadcn-ui, eslist, prettier & husky for future usage, would make things a bit easier. (Hoping to finish this by next week!)
The steps are straightforward to
- Update your
next.config.ts
file to allow images from Sanity.
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.sanity.io',
},
],
},
experimental: {
taint: true,
},
// ...other config settings
};
export default nextConfig;
- Create a new Sanity project. (will probably have to include a guide on how to create a new project on an already existing configuration)
npx sanity@latest init --env --create-project "<PROJECT-NAME-HERE>" --dataset production
> Would you like to add configuration files for a Sanity project in this Next.js folder?
Yes
> Do you want to use TypeScript?
Yes
> Would you like an embedded Sanity Studio?
Yes
> Would you like to use the Next.js app directory for routes?
Yes
> What route do you want to use for the Studio?
/studio
> Select project template to use
Blog (schema)
> Would you like to add the project ID and dataset to your .env file?
Yes
- Check if
.env
file is crated and rename it to.env.local
# ./.env.local
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"
- Create a fetch function to fetch documents from Sanity in
sanity/lib/fetch.ts
// ./sanity/lib/fetch.ts
import type { ClientPerspective, QueryParams } from 'next-sanity';
import { draftMode } from 'next/headers';
import { client } from './client';
// import { token } from "./token";
/**
* Used to fetch data in Server Components, it has built in support for handling Draft Mode and perspectives.
* When using the "published" perspective then time-based revalidation is used, set to match the time-to-live on Sanity's API CDN (60 seconds)
* and will also fetch from the CDN.
* When using the "previewDrafts" perspective then the data is fetched from the live API and isn't cached, it will also fetch draft content that isn't published yet.
*/
export async function sanityFetch<QueryResponse>({
query,
params = {},
perspective = draftMode().isEnabled ? 'previewDrafts' : 'published',
/**
* Stega embedded Content Source Maps are used by Visual Editing by both the Sanity Presentation Tool and Vercel Visual Editing.
* The Sanity Presentation Tool will enable Draft Mode when loading up the live preview, and we use it as a signal for when to embed source maps.
* When outside of the Sanity Studio we also support the Vercel Toolbar Visual Editing feature, which is only enabled in production when it's a Vercel Preview Deployment.
*/
stega = perspective === 'previewDrafts' ||
process.env.VERCEL_ENV === 'preview',
}: {
query: string;
params?: QueryParams;
perspective?: Omit<ClientPerspective, 'raw'>;
stega?: boolean;
}) {
if (perspective === 'previewDrafts') {
return client.fetch<QueryResponse>(query, params, {
stega,
perspective: 'previewDrafts',
// The token is required to fetch draft content
// token,
// The `previewDrafts` perspective isn't available on the API CDN
useCdn: false,
// And we can't cache the responses as it would slow down the live preview experience
next: { revalidate: 0 },
});
}
return client.fetch<QueryResponse>(query, params, {
stega,
perspective: 'published',
// The `published` perspective is available on the API CDN
useCdn: true,
// Only enable Stega in production if it's a Vercel Preview Deployment, as the Vercel Toolbar supports Visual Editing
// When using the `published` perspective we use time-based revalidation to match the time-to-live on Sanity's API CDN (60 seconds)
next: { revalidate: 60 },
});
}
- Update the queries that will be passed into the fetch function.
// ./sanity/lib/queries.ts
import { groq } from 'next-sanity';
export const POSTS_QUERY = groq`*[_type == "post" && defined(slug)]`;
export const POST_QUERY = groq`*[_type == "post" && slug.current == $slug][0]`;
- If your main purpose of using Sanity is for Blog Posts, then make sure to use blockQuote type with the relevant packages to show clear distinction between various heading levels and text formats.
pnpm install -D @tailwindcss/typography # using prose for applying heading and text distincions
pnpm install @portabletext/react # sort out sanity blocks (h1, h2, p, u) to appropriate tags to apply prose class with
// ./tailwind.config.ts
module.exports = {
// ...other settings
plugins: [require('@tailwindcss/typography')],
};
Example of how to use all together:
// ./components/Post.tsx
import Image from "next/image"
import { PortableText } from "@portabletext/react"
import imageUrlBuilder from "@sanity/image-url"
import { SanityDocument } from "next-sanity"
import { dataset, projectId } from "@/sanity/env"
const urlFor = (source: any) =>
imageUrlBuilder({ projectId, dataset }).image(source)
export default function Post({ post }: { post: SanityDocument }) {
const { title, mainImage, body } = post
return (
<main className="container mx-auto prose prose-lg p-4">
{title ? <h1>{title}</h1> : null}
{mainImage ? (
<Image
className="float-left m-0 w-1/3 mr-4 rounded-lg"
src={urlFor(mainImage).width(300).height(300).quality(80).url()}
width={300}
height={300}
alt={mainImage.alt || ""}
/>
) : null}
{body ? <PortableText value={body} /> : null}
</main>
)
}
Also make sure to create a type annotation for your BlogPost that reflects your Sanity Schema for the same.
Footnotes
-
Setting up Sanity CMS with App Router - https://www.sanity.io/guides/nextjs-app-router-live-preview ↩