CoursesContent-driven web application foundationsDisplaying images
Track
Work-ready Next.js

Content-driven web application foundations

Log in to watch a video walkthrough of this lesson
Video thumbnail

Sanity stores your image assets, learn how both the Sanity CDN and Next.js's Image component help optimize rendering them.

Log in to mark your progress for each Lesson and Task

For most web applications, the majority of data sent over the network will be for assets – such as images and videos. Your end users want your application to load as fast as possible. It's also well-known that faster-loading sites directly improve conversion rates.

Optimizing web applications for performance is an intense topic. This lesson aims to give essential guidance for serving images using utilities provided by Sanity and Next.js.

See Optimising Largest Contentful Paint on CSS Wizardry for an example of how much further this topic goes.

This is the last time we'll remind you to create a branch when working on new features. We trust you'll get in the habit from now on.

Create a new local branch before continuing.
git checkout -b add-images

Assets uploaded to the Content Lake are available on the Sanity CDN to render on your front end. Parameters can be added to an image URL to determine its size, cropping, file type, and more.

See Presenting Images in the documentation for more details

When you upload an image to the Content Lake, an additional document is created to represent that asset, along with details of its metadata and more.

Uploading an image from within a document creates a reference to that asset document.

Upload an image to the "Main image" field of a post document and publish

Now you can query for a single post-type document with an image and return just the image field.

Run the query below in Vision in your Sanity Studio
*[_type == "post" && defined(mainImage)][0]{
mainImage
}

The response should contain a _ref inside the asset attribute.

There may also be crop information and an alt string field.

{
"mainImage": {
"_type": "image",
"asset": {
"_ref": "image-a9302e7a5555e209623897eeec703c39499db23e-5785x3857-jpg",
"_type": "reference"
}
}
}

The image field schema type is similar to the object type in that it can have additional fields. One is already configured for you for "alternative text."

"Alt" text is used as a fallback when the image is not yet loaded and helps describe the image for screen readers. It is an essential addition to the accessibility of your web application.

See the MDN documentation for more information about the alt attribute.
Your current schema type setup stores the alt text in this document. The popular Media browser plugin writes alt text to the asset document.

Including alt text for images is important enough that your Studio schema should enforce it as a requirement. Use a custom validation rule to require the alt field when the asset field has a value.

Update the post-type schema to add a validation rule to the alt text field
src/sanity/schemaTypes/postType.ts
defineField({
name: 'alt',
type: 'string',
title: 'Alternative text',
validation: rule => rule.custom((value, context) => {
const parent = context?.parent as {asset?: {_ref?: string}}
return !value && parent?.asset?._ref ? 'Alt text is required when an image is present' : true
}),
})

Using the GROQ operator to resolve a reference (->) you can return everything from the asset attribute.

Run the query below in Vision to return the referenced asset document
*[_type == "post" && defined(mainImage)][0]{
mainImage {
...,
asset->
}
}

You should now have a much larger response, and within it, a url attribute with the full path to the original image.

This is useful, however:

  • It would be slow for end-users and wasteful of bandwidth to serve a full-size image for every request.
  • Because Sanity image URLs follow a strict convention, the @sanity/image-url package allows you to create image URL's without resolving references – using just the project ID, dataset and asset ID.
@sanity/asset-utils is another handy library for working with Sanity Assets using just their ID

Next, you'll update the front-end to display the "main image" with a dynamically generated URL.

Images served from Sanity's CDN can be resized and delivered in different qualities and formats, all by appending specific parameters to the URL. Serving images closer to the size they are viewed and in the most efficient format is the best way to reduce bandwidth and loading times.

When you ran sanity init with the Next.js template, a file was created for you with the image builder preconfigured with your project ID and dataset name:

src/sanity/lib/image.ts
import createImageUrlBuilder from '@sanity/image-url'
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
import { dataset, projectId } from '../env'
// https://www.sanity.io/docs/image-url
const builder = createImageUrlBuilder({ projectId, dataset })
export const urlFor = (source: SanityImageSource) => {
return builder.image(source)
}

This urlFor function will accept a Sanity image – as a full asset document, or even just the ID as a string – and return a method for you to generate a full URL.

Update your individual post route to render an image if it exists:
src/app/posts/[slug]/page.tsx
import Link from "next/link";
import { client } from "@/sanity/lib/client";
import { POST_QUERY } from "@/sanity/lib/queries";
import { urlFor } from "@/sanity/lib/image";
type PostIndexProps = { params: { slug: string } };
const options = { next: { revalidate: 60 } };
export default async function Page({ params }: PostIndexProps) {
const post = await client.fetch(POST_QUERY, params, options);
return (
<main className="container mx-auto grid grid-cols-1 gap-6 py-12">
{post?.mainImage ?
<img
className="w-full aspect-[800/300]"
src={urlFor(post.mainImage).width(800).height(300).quality(80).auto('format').url()}
alt={post?.mainImage?.alt || ''}
width="800"
height="300"
/> : null}
<h1 className="text-4xl font-bold text-balance">{post?.title}</h1>
<hr />
<Link href="/posts">&larr; Return to index</Link>
</main>
);
}

The post to which you uploaded an image and published the changes should now render the image from Sanity.

Take note of the methods passed along to urlFor, which created a unique URL to the image which was cropped to 800x300 pixels, a quality of 80%, in the most efficient file format that the current browser can display, and finally returned a string to the complete URL.

See @sanity/image-url for the full list of available methods and their uses.

If you inspect the URL of the image, you should see a result like this:

https://cdn.sanity.io/images/mml9n8hq/production/a9302e7a5555e209623897eeec703c39499db23e-5785x3857.jpg?rect=0,845,5785,2169&w=800&h=300&q=80&auto=format

While the front end is set to determine the size of the image, your content creators may want to draw focus to a specific region. In other CMSes, this typically means uploading several versions of the same image at different crop sizes. With Sanity, you can store the crop and focal intentions as data.

Within your Sanity Studio post schema type, the "main image" field contains an option of hotspot set to true.

defineField({
name: 'mainImage',
type: 'image',
options: {
hotspot: true,
},
// ... other settings
}),

This enables the crop and hotspot tool inside Sanity Studio, allowing creators to set the bounds of the image that should be displayed and, when cropped, which area it should focus on.

Because the crop and hotspot values were returned from the GROQ query for the asset, they were sent along when creating the URL.

Update the image in the document with a crop area and focal point and publish the document.

Now, your image should look somewhat different on the front end, with its best efforts made to utilize the crop area and focal point.

You've now successfully uploaded, editorialized, and displayed an image from Sanity on the front end that is performant and accessible. However, your IDE may be showing a warning that the use of the <img> element may produce sub-par performance. There is some logic to this, as the rendering of an image can be further enhanced for performance than what you currently have.

Next.js prefers you use their Image component, we can switch to that now.

Fast image rendering is crucial to fast web applications, so any improvements that can be made in this area are beneficial. Next.js ships an Image component for this reason.

See Vercel's documentation for more about the Next.js Image component and optimization

The Next.js documentation mentions that you'll need to update the Next.js config to accept the Sanity CDN URL to use the Image component with remote images.

Update nextjs.config.mjs to include the Sanity CDN URL
nextjs.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.sanity.io",
},
],
},
};
export default nextConfig;

Now update your individual post route to use the imported Next.js component <Image /> instead of the HTML <img>. It is important to note that this component requires a specified height and width.

Update the post route to use Image from next/image:
src/app/(frontend)/posts/[slug]/page.tsx
import Link from "next/link";
import Image from "next/image";
import { client } from "@/sanity/lib/client";
import { POST_QUERY } from "@/sanity/lib/queries";
import { urlFor } from "@/sanity/lib/image";
type PostIndexProps = { params: { slug: string } };
const options = { next: { revalidate: 60 } };
export default async function Page({ params }: PostIndexProps) {
const post = await client.fetch(POST_QUERY, params, options);
return (
<main className="container mx-auto grid grid-cols-1 gap-6 py-12">
{post?.mainImage ?
<Image
className="w-full aspect-[800/400]"
src={urlFor(post.mainImage).width(800).height(400).quality(80).auto('format').url()}
alt={post?.mainImage?.alt || ''}
width="800"
height="400"
/> : null}
<h1 className="text-4xl font-bold text-balance">{post?.title}</h1>
<hr />
<Link href="/posts">&larr; Return to index</Link>
</main>
);
}

Once the page reloads, open the web inspector and look at the generated <img> element. You will notice it has several more attributes than before. The loading and decoding attributes, in particular, are subtle performance improvements.

<img
alt="Chocolate layer cake"
loading="lazy"
width="800"
height="300"
decoding="async"
data-nimg="1"
class="w-full aspect-[800/300]"
style="color:transparent"
srcset="/_next/image?url=https..."
>

You're now retrieving images from Sanity's CDN with the best possible front end performance thanks to the Next.js Image component.

The next building block of web applications to render is rich text and block content. Let's get acquainted with Portable Text in the next lesson.

Courses in the "Work-ready Next.js" track