DEV Community

Lorenzo Rivosecchi
Lorenzo Rivosecchi

Posted on

Practical image rendering guide for Sanity and Next.js

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, and for the other one there is the Next.js image component.

Sanity Asset CDN

On top of storage and delivery, sanity provides a pipeline to transform the images upon request using query parameters.

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

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

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:

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

section > img {
  position: absolute;
  inset: 0;
  object-fit: cover;
  margin: 0 auto;
}
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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.

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;
   }
}
Enter fullscreen mode Exit fullscreen mode

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

<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"
/>
Enter fullscreen mode Exit fullscreen mode

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.

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>
Enter fullscreen mode Exit fullscreen mode

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.

npm i @sanity/image-url
# yarn add @sanity/image-url
# pnpm add @sanity/image-url
Enter fullscreen mode Exit fullscreen mode

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.

// 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)
}
Enter fullscreen mode Exit fullscreen mode

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

"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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

"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}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)