CoursesDay One with Sanity StudioDisplay content in a Next.js front end
Certification
Sanity developer certification
Track
Sanity developer essentials

Day One with Sanity Studio

Lesson
8

Display content in a Next.js front end

Log in to watch a video walkthrough of this lesson
Log in
Video thumbnail
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 lesson, 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 lesson 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.

Outside of your Studio directory, 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 "@/*" --turbopack
cd day-one-with-sanity-nextjs

You should now have your Studio and Next.js app in two separate, adjacent folders:

/day-one-with-sanity -> contains a Sanity Studio
/day-one-with-sanity-nextjs -> contains a Next.js app
Install the Sanity dependencies inside the /day-one-with-sanity-nextjs directory
npm install next-sanity @sanity/image-url --legacy-peer-deps
  • 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 (installed as part of next-sanity) is a React Component for rendering Portable Text with default components and the option to extend them for your own block content.

Note: This lesson 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. Including steps to embed the Sanity Studio directly into the Next.js application!
Run the following inside the /day-one-with-sanity-nextjs directory to start the development server
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.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "cdn.sanity.io" },
{ protocol: "https", hostname: "placehold.co" },
],
},
};
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 { createClient } from "next-sanity";
export const client = createClient({
projectId: "REPLACE_WITH_YOUR_PROJECT_ID",
dataset: "production",
apiVersion: "2024-11-01",
useCdn: false,
});

Sanity Client allows you to interact with many parts of your Sanity project. In many frameworks, this is all you'd use. In Next.js it's simple to do much better.

Let's make the application "Live by Default" with a little extra configuration.

Create a new file to create live fetching utilities
src/sanity/live.ts
import { defineLive } from "next-sanity";
import { client } from "@/sanity/client";
export const { sanityFetch, SanityLive } = defineLive({
client: client.withConfig({ apiVersion: "vX" }),
});
Update the root layout to include the SanityLive component
src/app/layout.tsx
// ...other imports
import { SanityLive } from "@/sanity/live";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="bg-white min-h-screen">
{children}
<SanityLive />
</body>
</html>
);
}

Next.js uses React Server Components, typically used as 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.

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 { defineQuery } from "next-sanity";
import { sanityFetch } from "@/sanity/live";
const EVENTS_QUERY = defineQuery(`*[
_type == "event"
&& defined(slug.current)
]{_id, name, slug, date}|order(date desc)`);
export default async function IndexPage() {
const { data: events } = await sanityFetch({ 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>
{event?.date && (
<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 named events with another folder named [slug] within it
Create a new file in the [slug] folder named page.tsx
src/app/events/[slug]/page.tsx
import { client } from "@/sanity/client";
import { sanityFetch } from "@/sanity/live";
import imageUrlBuilder from "@sanity/image-url";
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
import { defineQuery, PortableText } from "next-sanity";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
const EVENT_QUERY = defineQuery(`*[
_type == "event" &&
slug.current == $slug
][0]{
...,
"date": coalesce(date, now()),
"doorsOpen": coalesce(doorsOpen, 0),
headline->,
venue->
}`);
const { projectId, dataset } = client.config();
const urlFor = (source: SanityImageSource) =>
projectId && dataset
? imageUrlBuilder({ projectId, dataset }).image(source)
: null;
export default async function EventPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { data: event } = await sanityFetch({
query: EVENT_QUERY,
params: await params,
});
if (!event) {
notFound();
}
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://placehold.co/550x310/png"}
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.

Thanks to Sanity TypeGen, both the schema types in your Studio and the GROQ queries in your server components above can be fully typed. First you'll export the Studio schema, and then configure TypeGen to look through the Next.js application for any uses of the defineQuery helper function.

Run the following in your Studio directory to extract the Studio schema
npx sanity@latest schema extract

You should now have a schema.json file in the root of your Studio directory.

Create a new file in your Studio directory for TypeGen to use this extract schema and search the Next.js project for queries
sanity-typegen.json
{
"path": "../day-one-with-sanity-nextjs/src/**/*.{ts,tsx,js,jsx}",
"schema": "./schema.json",
"generates": "../day-one-with-sanity-nextjs/src/sanity/types.ts"
}
Run the following to generate the types.ts file
npx sanity@latest typegen generate

In the terminal you should see a message similar to the following:

✅ Generated TypeScript types for 14 schema types and 2 GROQ queries in 2 files into: ../day-one-with-sanity-nextjs/src/sanity/types.ts

And you should also have a new file day-one-with-sanity-nextjs/src/sanity/types.ts file inside your Next.js project.

All the "redlines" from TypeScript in your route files should now be gone.

Now, you've successfully displayed content from Sanity into a Next.js front end. But the implementation is very simple.

If you'd like to go deeper, we have a dedicated track of courses about building Next.js applications with Sanity.

See courses in the Work-ready Next.js track for more.
You have 13 uncompleted tasks in this lesson
0 of 13