Next.js との連携
前回は Sanity Studio の Vision を利用して、GROQ を利用したクエリーから必要となるコンテンツの取得ができました。今回は、Next.js から同様の処理を実行して Web のコンテンツとして利用する方法を紹介します。
Next.js のプロジェクト作成
まず最初に、Next.js のプロジェクトの作成を進めていきます。コマンドとしては以下のようになります。
npx create-next-app
プロジェクトの作成は以下のように進めました。

フォルダの構成は以下のようになります。
- day-one
- apps
- studio
- web
- data
続いて Next.js で Sanity と連携するためのパッケージをインストールします。Next.js のパスに移動して以下のコマンドを実行します。
npm install next-sanity
これで準備ができました。まだパッケージをインストールしただけですが、以下のコマンドで Next.js を起動します。
npm run dev
Next.js のテンプレートのページが立ち上がりました。

Sanity と連携する
続いて Next.js にコードを追加して、Sanity からデータを取得していきます。
イベント一覧の表示
Sanity からデータを取得するためのクライアントを apps/web/src/sanity/client.ts に作成します。
import { createClient } from "next-sanity";
export const client = createClient({
projectId: "REPLACE_WITH_YOUR_PROJECT_ID",
dataset: "production",
apiVersion: "2025-07-09",
useCdn: false,
});
projectId は apps/studio/sanity.cli.ts に記載されているので、参照して書き換えてください。
これだけでも十分ですが、コンテンツの変更を素早く反映させるためのモードを追加します。
import { defineLive } from "next-sanity";
import { client } from "@/sanity/client";
export const { sanityFetch, SanityLive } = defineLive({
client: client.withConfig({ apiVersion: "vX" }),
});
これで、Sanity と連携するためのコードの準備ができました。
Next.js 側で利用できるように変更をしていきます。まずは apps/web/src/app/layout.tsx のファイルを以下のように書き換えます。
import { SanityLive } from "@/sanity/live";
import "./globals.css";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="bg-white min-h-screen">
{children}
<SanityLive />
</body>
</html>
);
}
続いてトップページを以下のように書き換えます。なお、公式サイトに記載されていませんが、Event の interface を追加、ダークモードでも文字が見えるように少し変更をしています。
import Link from "next/link";
import { defineQuery } from "next-sanity";
import { sanityFetch } from "@/sanity/live";
interface Event {
_id: string;
name: string;
slug: {
current: string;
};
date: string;
}
const EVENTS_QUERY = defineQuery(`*[
_type == "event"
&& defined(slug.current)
&& date > now()
]|order(date asc){_id, name, slug, date}`);
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 dark:bg-gray-800">
<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: Event) => (
<li
className="bg-white dark:bg-gray-600 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 dark:text-gray-300">
{new Date(event.date).toLocaleDateString()}
</p>
)}
</Link>
</li>
))}
</ul>
</main>
);
}
コードを確認していくと。
- イベントデータを取得する GROQ のクエリ
- sanityFetch を利用してデータを取得
- 取得したデータを表示
結果として、イベントの情報が表示されるようになりました。

イベントのページ作成
一覧から各ページへのリンクを作成するために、events のフォルダの作成、その下に [slug] のフォルダを作成して、page.tsx を作成します。これにより slug を利用した URL を利用できるようになります。
import { defineQuery, PortableText } from "next-sanity";
import Link from "next/link";
import { notFound } from "next/navigation";
import { sanityFetch } from "@/sanity/live";
const EVENT_QUERY = defineQuery(`*[
_type == "event" &&
slug.current == $slug
][0]{
...,
"date": coalesce(date, now()),
"doorsOpen": coalesce(doorsOpen, 0),
headline->,
venue->
}`);
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,
details,
eventType,
doorsOpen,
venue,
tickets,
} = event;
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">
{/* We'll update this image in the next lesson */}
<img
src={"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>
);
}
実際に追加したあと、トップページのイベントをクリックすると以下のようにイベントの詳細ページが表示されるようになりました。

このページで重要なコードは以下の部分になります。
const EVENT_QUERY = defineQuery(`*[
_type == "event" &&
slug.current == $slug
][0]{
...,
"date": coalesce(date, now()),
"doorsOpen": coalesce(doorsOpen, 0),
headline->,
venue->
}`);
export default async function EventPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { data: event } = await sanityFetch({
query: EVENT_QUERY,
params: await params,
});
以下のような動作になっています。
- EVENT_QUERY には $slug という形で取得した slug を利用できるようにする
- [0] ということで slug の値が一致した 1 つ目のデータを利用
- sanityFetch では query と params を渡して、$slug が [slug] の値として動作するようにしている
取得したイベントのデータをページで簡単に利用できるように以下のように定義しています。
const {
name,
date,
headline,
details,
eventType,
doorsOpen,
venue,
tickets,
} = event;
sanityFetch にクエリと値を渡せば、結果となるデータの取得が決まり、ページの中で表示する値も作成できるようになります。
アセットの表示
画像を表示するためのパッケージとして @sanity/image-url が提供されています。これを Next.js のプロジェクトに追加します。
npm install @sanity/image-url
続いて Next.js で外部のドメインの画像を利用する際には、設定にドメインを登録する必要があります。remotePatterns で追加することになります。設定は以下の通りです。
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "cdn.sanity.io" },
{ protocol: "https", hostname: "placehold.co" },
],
},
};
export default nextConfig;
続いて画像を表示するためのコードを追加します。
import imageUrlBuilder from "@sanity/image-url";
import type { SanityImageSource } from "@sanity/image-url/lib/types/types";
import { client } from "./client";
const builder = imageUrlBuilder(client);
export function urlFor(source: SanityImageSource) {
return builder.image(source);
}
上記で追加した apps/web/src/sanity/image.ts を利用して、イベントのページ apps/web/src/app/events/[slug]/page.tsx のコードを更新します。
import { defineQuery, PortableText } from "next-sanity";
import Link from "next/link";
import { notFound } from "next/navigation";
import Image from "next/image";
import { sanityFetch } from "@/sanity/live";
import { urlFor } from "@/sanity/image";
const EVENT_QUERY = defineQuery(`*[
_type == "event" &&
slug.current == $slug
][0]{
...,
"date": coalesce(date, now()),
"doorsOpen": coalesce(doorsOpen, 0),
headline->,
venue->
}`);
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,
details,
eventType,
doorsOpen,
venue,
tickets,
} = event;
const eventDate = new Date(date).toDateString();
const eventTime = new Date(date).toLocaleTimeString();
const doorsOpenTime = new Date(
new Date(date).getTime() - doorsOpen * 60000
).toLocaleTimeString();
const imageUrl = headline?.photo
? urlFor(headline.photo)
.height(310)
.width(550)
.quality(80)
.auto("format")
.url()
: "https://placehold.co/550x310/png";
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={imageUrl}
alt={headline?.name ?? "Headliner"}
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>
);
}
これで、イベントに画像が設定されている場合はその画像を、そうではない場合は Placeholder の画像を表示するようにしました。

イベントによっては画像が含まれていないアーティストもいるため、一覧から画像が指定されているアーティストのイベントを選択してください。
まとめ
今回は Next.js のプロジェクトの作成、そのプロジェクトに Sanity と連携するためのパッケージをインストールしたあと、パッケージで提供している機能を利用して、かつ GROQ を利用して Web ページを作成しました。また、画像に関する情報も併せて取得してページに表示することができました。
ここまでのコードは以下のリポジトリ / ブランチで参照できます。