Practical image rendering guide for Sanity and Next.js
Published: 2023-06-12 09:44:41

Presenting images that look good and load fast is essential to provide a good user experience. Fortunately in 2023 we have a lot of services and APIs that help achieve that goal, but understanding how to use them can be difficult.

In this post I'll explain how to render images in a Next.js + Sanity website in hope that the lessons learned can be transferred to similar technologies.

---

## The Toolbox

Presenting images requires a method to serve them and one render them. For the first bit we can use the [Sanity Asset CDN](https://www.sanity.io/docs/asset-cdn), and for the other one there is the [Next.js image component](https://nextjs.org/docs/api-reference/next/image).

### Sanity Asset CDN

On top of storage and delivery, sanity provides a pipeline to transform the images upon request using [query parameters](https://www.sanity.io/docs/image-urls#BhPyF4m0).

We can use the `w` and `h` parameters to resize the image to the dimensions we need. This flexibility allows us to upload a high definition image and have the frontend request a properly sized version using the [srcset](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images) attribute.

```jsx
<img
  alt="Logo"
  src={IMG_URL} 
  srcset={`
    ${IMG_URL}?=w=72 72w,
    ${IMG_URL}?=w=144 144w
  `}
  width={72}
  height={72}
/>
```

In this example we render a logo icon using a 72x72 image from Sanity. The source set includes a url for the size we need plus one with doubled resolution. The browser will use the second one on devices with high resolution screens.

---

Another case where this API is useful is with background images. Let's imagine a hero image that covers the entire viewport.

Here are the styles:

```css
section {
  position: relative;
  width: 100vw;
  height: 100vh;
  display: grid;
  place-content: center;
}

section > img {
  position: absolute;
  inset: 0;
  object-fit: cover;
  margin: 0 auto;
}
```

Since the image will adapt to the dimensions of the viewport, we need to include a lot of sources, starting from `400px`. Note that the same considerations about high quality displays apply here as well, therefore if we stop at `2000px`, the image won't look sharp on high resolutions screens from `1000px` wide onwards.

```tsx
<section>
   <h2>Welcome!</h2>
   <img alt=""
      src={IMG_URL}
      srcSet={`
        ${IMG_URL}?w=400 400w,
        ${IMG_URL}?w=600 600w,
        ${IMG_URL}?w=800 800w,
        ${IMG_URL}?w=1000 1000w,
        ${IMG_URL}?w=1200 1200w,
        ${IMG_URL}?w=1400 1400w,
        ${IMG_URL}?w=1600 1600w,
        ${IMG_URL}?w=1800 1800w,
        ${IMG_URL}?w=2000 2000w
      `}
   />
</section>
```

As last example, let's consider another type of hero, where the image and the heading are stacked. On mobile the image will be 100% of the screen, but on desktop it will cover just half of it.

```css
section {
  position: relative;
  width: 100vw;
  height: 100vh;
  display: grid;
}

section > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (min-width: 1024px) {
  section {
     grid-template-columns: 1fr 1fr;
  }
  section > h2 {
     width: 50%;
     height: 100%;
     display: grid;
     place-content: center;
   }
}
```

To render the image with the right dimensions we can keep the source set as it is, and add the `sizes` attribute.

```jsx
<img alt=""
  src={IMG_URL}
  srcSet={`
    ${IMG_URL}?w=400 400w,
    ${IMG_URL}?w=600 600w,
    ${IMG_URL}?w=800 800w,
    ${IMG_URL}?w=1000 1000w,
    ${IMG_URL}?w=1200 1200w,
    ${IMG_URL}?w=1400 1400w,
    ${IMG_URL}?w=1600 1600w,
    ${IMG_URL}?w=1800 1800w,
    ${IMG_URL}?w=2000 2000w
  `}
  sizes="(min-width: 1024px) 50vw, 100vw"
/>
```

The browser will consider `100vw` to be the image size until the media query matches, then it will consider it to be `50vw`.

## Next.js Image component

The Next.js image component is a wrapper around the HTML img tag that provides useful features like:

- Auto generating srcsets
- Static image generation
- Lazy loading
- Blurred placeholders
- Sensible defaults
- Automatic preload

To render an image, simply import the component from `next/image` and pass props to it.

```tsx
import Image from "next/image";

// Image with static dimensions
<Image src={IMG_URL} alt="" width={72} height={72} />

// Responsive image
<section>
  <h1>Welcome!</h2>
  <Image src={IMG_URL} alt="" fill />
</section>
```

The resulting html will be similar to the examples from before with one notable exception:

Instead of using the Sanity CDN, Next.js will download and cache the images internally and serve them from `/_next/images`.

This is fine if you are deploying on Vercel because they will serve the generated images from a CDN, but if you are deploying on a node server located in a single data center, images will load slowly.

If images take too long to load you can fix it by disabling static image generation via the `unoptimized` prop, but then you would have to write the `srcset` manually.

In this case a better option would be to use the `loader` prop to tell the image component that it should fetch the images from the Sanity CDN instead of the Next.js image cache.

## Using the Image component with the Sanity CDN

Start by installing the image url package.

```shell
npm i @sanity/image-url
# yarn add @sanity/image-url
# pnpm add @sanity/image-url
```

We will use this library top facilitate the creation of different image urls that take advantage of the image transformation functionality offered by Sanity.

Before using the library we need to feed our sanity client instance to it.

```typescript
// lib/sanity.ts

import { createClient } from '@sanity/client';
import imageUrlBuilder from '@sanity/image-url';
import { SanityImageSource } from "@sanity/image-url/lib/types/types";


export const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset-name',
  useCdn: true, // not important
  apiVersion: '2023-06-12'
})

const builder = imageUrlBuilder(client);

export function urlForImage(source: SanityImageSource) {
  return builder.image(source)
}
```

Now, let's create a React component to render the section from before using `next/image` in combination with the Sanity CDN:

```tsx
"use client";

import { urlForImage } from "../lib/sanity";
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
import Image from "next/image";

type Props = {
  image: SanityImageSource;
};

export default function MySection({ image }: Props) {
  return (
    <section>
      <h2>Welcome!</h2>
      <Image
        src="Doesn't matter"
        alt=""
        fill
        loader={({ width, quality = 100 }) =>
       urlForImage(image).width(width).quality(quality).url()
        }
      />
    </section>
  );
}
```

This implementation gives us the benefits of the image components with the convenience and lower cost of the Sanity CDN.

We can elaborate our approach further by creating a reusable SanityImage component:

```tsx
"use client";

import { urlForImage } from "@/sanity/lib/image";
import { SanityImageSource } from "@sanity/image-url/lib/types/types";
import Image, { ImageProps } from "next/image";

type Props = Omit<ImageProps, "src"> & {
  src: SanityImageSource;
};

export default function SanityImage({ src, alt, ...props }: Props) {
  return (
    <Image
      src="Doesn't matter"
      alt={alt}
      loader={({ width, quality = 100 }) =>
        urlForImage(src).width(width).quality(quality).url()
      }
      {...props}
    />
  );
}
```