TracksMastering Content OperationsCoursesDay One Content OperationsRendering assets
Day One Content Operations
Markdown Version

Rendering assets

Log in to watch a video walkthrough of this lesson
Log in
Video thumbnail
Sanity's powerful image asset CDN allows you to deliver images at the appropriate size by setting a few query parameters on a URL.
Log in to mark your progress for each Lesson and Task
Read Presenting Images in the documentation for more details

To simplify the creation of image URLs, we also ship a number of helpful functions and methods in the @sanity/image-url package.

Run the following to install @sanity/image-url
# in apps/web
pnpm 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.

Read more about the Image component in the Next.js documentation.
Update 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.

Create a new file to export a urlFor helper function
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);
}

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.

Set 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.

You have 3 uncompleted tasks in this lesson
0 of 3