Sanity
Learn (Beta)
Course

Day One with Sanity Studio

Lesson
8

Display content in a Next.js front end

You've now crafted a content creation experience and learned how to query from the Content Lake. All that's left to do is distribute that content to the world.

Log in to mark your progress for each Lesson and Task

In this exercise, you'll create a new Next.js application and query for Sanity content. Next.js is a React framework for building full-stack web applications. You'll use it in this exercise because of how simply you can get started – but the concepts here could work with any framework or front end library.

See "clean starter" templates available for Astro, Remix and more

The command below installs a predefined bare bones template with some sensible defaults, and Tailwind CSS is installed.

Create a new directory and run the following command to create this new Next.js application.
npx create-next-app@latest day-one-with-sanity-nextjs --tailwind --ts --app --src-dir --eslint --import-alias "@/*"

Go into the day-one-with-sanity-nextjs directory and install these additional dependencies to query and render Sanity content.

Install the Sanity dependencies
# In the day-one-with-sanity-nextjs directory
npm install next-sanity @sanity/image-url @portabletext/react
  • next-sanity is a collection of utilities specifically tuned for Next.js when integrating with Sanity
  • @sanity/image-url contains helper functions to take image data from Sanity and create a URL
  • @portabletext/react is a React Component for rendering Portable Text with default components and the option to extend them for your own block content.

Note: This exercise focuses on the basics for getting started. For projects going into production, there are more factors to consider: caching and revalidation, live previews, and other performance enhancements that require a bit more setup and consideration.

The next-sanity documentation contains more details for preparing Sanity and Next.js for production
Start the development server
# In the day-one-with-sanity-nextjs directory
npm run dev
Open http://localhost:3000 in your browser

You should now see the default page for new Next.js applications, just like this:

Before you continue, the default Next.js template comes with a lot of cruft in the globals.css file, reset it to just the following:

./src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

Also because you'll load images from external URL's using the image component Next.js supplies, those domains will need to be listed in the Next.js config

./next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ["cdn.sanity.io", "via.placeholder.com"],
},
};
export default nextConfig;

To fetch content from Sanity, you'll need a configured Sanity Client. In the code snippet below, you'll need to modify the projectId value to the one in your Studio's sanity.config.ts

Create a new file for the Sanity client with your projectId
./src/sanity/client.ts
import "server-only";
import { createClient, type QueryParams } from "next-sanity";
export const client = createClient({
projectId: "REPLACE_WITH_YOUR_PROJECT_ID",
dataset: "production",
apiVersion: "2024-01-01",
useCdn: false,
});
export async function sanityFetch<QueryResponse>({
query,
params = {},
tags,
}: {
query: string;
params?: QueryParams;
tags?: string[];
}) {
return client.fetch<QueryResponse>(query, params, {
next: {
revalidate: process.env.NODE_ENV === 'development' ? 30 : 3600,
tags,
},
});
}

Because Next.js will heavily cache all requests by default – such as those made by Sanity Client – this file also exports a sanityFetch helper function to perform queries with customized caching settings. In development it will only cache content for 30 seconds, otherwise it will cache for an hour.

See the next-sanity package's section on cache revalidation for more details.

Next.js uses "routes" for loading and displaying data. This home page is the root index route with the filename page.tsx.

It's currently showing static content; let's replace that with content fetched from your Sanity project.

  • Next.js uses React Server Components. These components render only server-side, and this is where the data fetching happens.
  • Notice the GROQ query looking for all event-type documents that have a slug.
Update the home page route
./src/app/page.tsx
import Link from "next/link";
import { SanityDocument } from "next-sanity";
import { sanityFetch } from "@/sanity/client";
const EVENTS_QUERY = `*[_type == "event"]{_id, name, slug, date}|order(date desc)`;
export default async function IndexPage() {
const events = await sanityFetch<SanityDocument[]>({query: EVENTS_QUERY});
return (
<main className="flex bg-gray-100 min-h-screen flex-col p-24 gap-12">
<h1 className="text-4xl font-bold tracking-tighter">
Events
</h1>
<ul className="grid grid-cols-1 gap-12 lg:grid-cols-2">
{events.map((event) => (
<li
className="bg-white p-4 rounded-lg"
key={event._id}
>
<Link
className="hover:underline"
href={`/events/${event.slug.current}`}
>
<h2 className="text-xl font-semibold">{event?.name}</h2>
<p className="text-gray-500">
{new Date(event?.date).toLocaleDateString()}
</p>
</Link>
</li>
))}
</ul>
</main>
);
}

Your home page should now look mostly the same but with published documents from your Studio.

Create another route to display each individual event. The query on this page will look for any event with a matching slug from the one used to load the page.

Create a route for individual event pages by adding a folder called events with another folder called [slug] within it
In the [slug] folder, add a new file called page.tsx
./src/app/events/[slug]/page.tsx
import { PortableText } from "@portabletext/react";
import { SanityDocument } from "next-sanity";
import imageUrlBuilder from "@sanity/image-url";
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
import { client, sanityFetch } from "@/sanity/client";
import Link from "next/link";
import Image from "next/image";
const EVENT_QUERY = `*[
_type == "event" &&
slug.current == $slug
][0]{
...,
headline->,
venue->
}`;
const { projectId, dataset } = client.config();
export const urlFor = (source: SanityImageSource) =>
projectId && dataset
? imageUrlBuilder({ projectId, dataset }).image(source)
: null;
export default async function EventPage({
params,
}: {
params: { slug: string };
}) {
const event = await sanityFetch<SanityDocument>({
query: EVENT_QUERY,
params,
});
const {
name,
date,
headline,
image,
details,
eventType,
doorsOpen,
venue,
tickets,
} = event;
const eventImageUrl = image
? urlFor(image)?.width(550).height(310).url()
: null;
const eventDate = new Date(date).toDateString();
const eventTime = new Date(date).toLocaleTimeString();
const doorsOpenTime = new Date(
new Date(date).getTime() + doorsOpen * 60000
).toLocaleTimeString();
return (
<main className="container mx-auto grid gap-12 p-12">
<div className="mb-4">
<Link href="/">← Back to events</Link>
</div>
<div className="grid items-top gap-12 sm:grid-cols-2">
<Image
src={eventImageUrl || "https://via.placeholder.com/550x310"}
alt={name || "Event"}
className="mx-auto aspect-video overflow-hidden rounded-xl object-cover object-center sm:w-full"
height="310"
width="550"
/>
<div className="flex flex-col justify-center space-y-4">
<div className="space-y-4">
{eventType ? (
<div className="inline-block rounded-lg bg-gray-100 px-3 py-1 text-sm dark:bg-gray-800 capitalize">
{eventType.replace("-", " ")}
</div>
) : null}
{name ? (
<h1 className="text-4xl font-bold tracking-tighter mb-8">
{name}
</h1>
) : null}
{headline?.name ? (
<dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base">
<dd className="font-semibold">Artist</dd>
<dt>{headline?.name}</dt>
</dl>
) : null}
<dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base">
<dd className="font-semibold">Date</dd>
<div>
{eventDate && <dt>{eventDate}</dt>}
{eventTime && <dt>{eventTime}</dt>}
</div>
</dl>
{doorsOpenTime ? (
<dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base">
<dd className="font-semibold">Doors Open</dd>
<div className="grid gap-1">
<dt>Doors Open</dt>
<dt>{doorsOpenTime}</dt>
</div>
</dl>
) : null}
{venue?.name ? (
<dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base">
<div className="flex items-start">
<dd className="font-semibold">Venue</dd>
</div>
<div className="grid gap-1">
<dt>{venue.name}</dt>
</div>
</dl>
) : null}
</div>
{details && details.length > 0 && (
<div className="prose max-w-none">
<PortableText value={details} />
</div>
)}
{tickets && (
<a
className="flex items-center justify-center rounded-md bg-blue-500 p-4 text-white"
href={tickets}
>
Buy Tickets
</a>
)}
</div>
</div>
</main>
);
}

A few things of note in the code example above:

  • The urlFor function sets up the automatic image transformation handling that you get with Sanity out of the box. In production, you would move this function to the sanity folder so you could import it into any template.
  • The brackets in the [slug] folder name tell Next.js that it should make this part of the URL (localhost:3000/events/the-event-slug) dynamic, that is, available inside of the params property for the page template function. We can then use this information to query the correct document from Sanity ($slug).
  • Some deeply nested items use optional chaining (?.) to only render an attribute if its parent exists. This is especially important when working with live preview where draft documents cannot be guaranteed to have values – even those you have required validation rules on.

You should now be able to view the list of Events on the home page, click any one, and be shown its full details.

Tooling is available to render Sanity content types into the front end of your choice.

Now, you've successfully displayed content from Sanity into a Next.js front end. The next move would be to set up Visual Editing within the Presentation tool, so your authors can see and edit their content in context inside Sanity Studio.

See the documentation's overview page for Visual Editing Tooling.

There are step-by-step guides available to set up Visual Editing and Presentation within popular frameworks.