next-sanity icon indicating copy to clipboard operation
next-sanity copied to clipboard

Supply Image component using next/image

Open alexbchr opened this issue 4 years ago • 1 comments

It would be great to have an Image component built-in the library, using next/image behind the scenes. It should be possible to use Sanity with it as since [email protected], there is a new loader prop (see next/image documentation).

At least, if not supplying a component leveraging next/image, it would be great to have a code example on how to implement one in the docs or in an example project.

I started developing one myself as a small POC (supports both crop and hotspot features if layout !== 'fill'):

import React, { useCallback } from 'react'
import NextImage, { ImageProps as NextImageProps, ImageLoader } from 'next/image'
import { urlFor } from '../../../clients/sanity'
import { SanityAsset } from '../../../utils/sanity'

// Needed to mostly copy ImageProps from next/image as otherwise Typescript types were all messy
export type ImageProps = Omit<
  JSX.IntrinsicElements['img'],
  'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' | 'style'
> & {
  src: {
    alt?: string
    asset: SanityAsset & { url?: string }
  }
  quality?: number | string
  priority?: boolean
  loading?: NextImageProps['loading']
  unoptimized?: boolean
  objectFit?: NextImageProps['objectFit']
  objectPosition?: NextImageProps['objectPosition']
} & (
    | {
        width?: never
        height?: never
        /** @deprecated Use `layout="fill"` instead */
        unsized: true
      }
    | {
        width?: never
        height?: never
        layout: 'fill'
      }
    | {
        width: number | string
        height: number | string
        layout?: 'fixed' | 'intrinsic' | 'responsive'
      }
  )

/**
 * Basic component displaying an Image coming from Sanity using next/image.
 *
 * Supports Crop/Hotspot if height and width are specified (if using a `layout` prop different of "fill")
 *
 * @see https://nextjs.org/docs/api-reference/next/image
 */
export const Image: React.FC<ImageProps> = ({ src, ...props }) => {
  if (!src?.asset?._ref && !src?.asset?.url) {
    console.warn('No Reference passed to image. Make sure the image is correctly set.')
    return null
  }

  // Loader for Sanity. Unfortunately, as we need crop/hotspot properties here, along with the height provided, 
  // We need to have this function defined in the component.
  const sanityLoader = useCallback<ImageLoader>(
    ({ width, quality = 75 }) => {
      const renderWidthInt = _getInt(props.width)
      const renderHeightInt = _getInt(props.height)
      const imageRatio =
        renderWidthInt && renderHeightInt ? renderWidthInt / renderHeightInt : undefined

      let urlBuilder = urlFor(src)
        .auto('format') // Load webp if supported by browser
        .fit('max') // Don't scale up images of lower resolutions
        .width(width)
        .quality(quality)

      if (renderHeightInt && imageRatio) {
        urlBuilder = urlBuilder.height(width / imageRatio)
      }

      return urlBuilder.url() || ''
    },
    [src, props.width, props.height],
  )

  return (
    <NextImage
      {...props}
      alt={src.alt || ''}
      src={src.asset._ref || src.asset.url || ''}
      loader={sanityLoader}
    />
  )
}

const _getInt = (x: string | number | undefined): number | undefined => {
  if (typeof x === 'number') {
    return x
  }
  if (typeof x === 'string') {
    return parseInt(x, 10)
  }
  return undefined
}

alexbchr avatar Jan 14 '21 15:01 alexbchr

Having this this SanityImage included in this toolkit could be really usefull! Or at least expose a next/image loader to build the image path given the image constraint.

If this is something wanted I could start a PR to implement it.

armandabric avatar Apr 15 '21 14:04 armandabric

next-sanity-image provides support for Sanity images with Next. It could be used as a basis for implementing the component here.

In the meantime you can use that library directly if you're looking for support via next/image.

gpoole avatar Mar 07 '23 00:03 gpoole

Hey, like @gpoole mentions there's the excellent next-sanity-image package by @lorenzodejong that provides this functionality :D In fact, we use it on www.sanity.io 🎉

@armandabric I'm sure they'll give you a warm welcome if you have ideas for further improvements 😌✨

stipsan avatar Mar 22 '23 13:03 stipsan

@stipsan pretty awesome to hear you guys are using it on Sanity.io!

I've actually released version 6.0.0 recently which improves compatibility with the most recent versions of @sanity/client and next. This should in turn also improve compatibility with next-sanity.

Let me know if you run into anything with the setup, i'd be happy to help you out and/or provide further improvements on the library.

lorenzodejong avatar Mar 22 '23 20:03 lorenzodejong

Thanks @lorenzodejong, we'll be sure to let you know 😌 And it goes for you as well. If you want help to get in our automated tooling. Like, if you want to use our semantic-release so that you don't have to make manual GitHub release notes and CHANGELOG.md updates anymore. Or @sanity/pkg-utils so you don't have to deal with rollup.config.ts files or how to setup pkg.exports we'd be happy to send you a PR 😄

stipsan avatar Mar 24 '23 15:03 stipsan

@stipsan that would actually be great! The most recent releases were almost always due to a major dependency update, either on the @sanity/client side or the next side. I was already thinking about setting up a release pipeline, however it might be nice if we can align it further with Sanity's semantic release setup.

lorenzodejong avatar Mar 27 '23 15:03 lorenzodejong

Would be happy to help! Our automation setup is pretty sweet, removes a whole lot of churn and it's fun to see robots do all the heavy lifting :D

stipsan avatar Apr 03 '23 15:04 stipsan