Rendering assets

To simplify the creation of image URLs, we also ship a number of helpful functions and methods in the @sanity/image-url
package.
@sanity/image-url
# in apps/webpnpm add @sanity/image-url
Next.js includes an Image
component for the performant rendering of images. Before you can use images from Sanity's CDN URL they'll need to be added to the Next.js configuration.
Image
component in the Next.js documentation.next.config.ts
to include the URLs of Sanity's CDN and a placeholder service.import type { NextConfig } from "next";
const nextConfig: NextConfig = { images: { remotePatterns: [ { protocol: "https", hostname: "cdn.sanity.io" }, { protocol: "https", hostname: "placehold.co" }, ], },};
export default nextConfig;
Create a helper function that is specific to your project ID and dataset which takes information about an image asset in Sanity (such as its ID) and return a complete URL with settings such as quality and size.
urlFor
helper functionimport 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);}
You can now update the individual page route to use Image component from Next.js as well as this urlFor
function to turn a Sanity image asset ID into a complete URL.
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="/" className="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white" > ← Back to events </Link> </div> <div className="grid items-top gap-12 sm:grid-cols-2"> <Image src={imageUrl} 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 dark:bg-gray-800 px-3 py-1 text-sm text-gray-700 dark:text-gray-300 capitalize"> {eventType.replace("-", " ")} </div> ) : null} {name ? ( <h1 className="text-4xl font-bold tracking-tighter mb-8 text-gray-900 dark:text-white"> {name} </h1> ) : null} {headline?.name ? ( <dl className="grid grid-cols-2 gap-1 text-sm font-medium sm:gap-2 lg:text-base text-gray-700 dark:text-gray-300"> <dd className="font-semibold text-gray-900 dark:text-white"> 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 text-gray-700 dark:text-gray-300"> <dd className="font-semibold text-gray-900 dark:text-white"> 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 text-gray-700 dark:text-gray-300"> <dd className="font-semibold text-gray-900 dark:text-white"> 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 text-gray-700 dark:text-gray-300"> <div className="flex items-start"> <dd className="font-semibold text-gray-900 dark:text-white"> 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 prose-gray dark:prose-invert"> <PortableText value={details} /> </div> )} {tickets && ( <a className="flex items-center justify-center rounded-md bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 p-4 text-white transition-colors" href={tickets} > Buy Tickets </a> )} </div> </div> </main> );}
You can now open any event with a headline
artist with a photo
field, and see the image rendered at the appropriate size.
hotspot: true
on the image
field schema to enable crop and hotspot. When this data is passed into the same URL builder function it is added to the image URL parameters.You're now querying and rendering text and image content from Sanity into your Next.js application—but your code has some annoying red squiggly lines because of TypeScript. Let's fix that next.