sanity-io / next-sanity

Sanity.io toolkit for Next.js
https://www.sanity.io/
MIT License
786 stars 94 forks source link

next-sanity/image does not appear to be fully using hotspot #1647

Open JamesSingleton opened 3 months ago

JamesSingleton commented 3 months ago

Is your feature request related to a problem? Please describe. I had originally been using sanity-image which was good. However, after the livestream with Knut and Lee, I wanted to try using next/image with a loader for Sanity images. Digging through this repo I found that there was next-sanity/image. So I went to implement it as pretty much a drop in replacement for sanity-image. However, I noticed that sometimes it used the hotspot and sometimes it didn't

Describe the solution you'd like It would be nice if I didn't have to crop and it could just use the hotspot.

Describe alternatives you've considered Well as described above, I used sanity-image.

Additional context

An example where I needed to crop in order to get a hotspot

Before cropping my image BUT still have the hotspot Screenshot 2024-08-11 at 3 19 18 PM

image

After cropping the image with same hotspot Screenshot 2024-08-11 at 3 19 55 PM

Screenshot 2024-08-11 at 3 18 25 PM

An example where I didn't need to crop in order to get a hotspot

Screenshot 2024-08-11 at 3 25 26 PM

Image in the network tab

Screenshot 2024-08-11 at 3 25 37 PM

Display in a card element Screenshot 2024-08-11 at 3 26 09 PM

How I am using the component

// components/image.tsx
import createImageUrlBuilder from '@sanity/image-url'
import { Image as SanityImage, type ImageProps } from 'next-sanity/image'

import { projectId, dataset } from '@/lib/sanity.api'

const imageBuilder = createImageUrlBuilder({
  projectId,
  dataset,
})

export const urlForImage = (source: Parameters<(typeof imageBuilder)['image']>[0]) =>
  imageBuilder.image(source)

export function Image(
  props: Omit<ImageProps, 'src' | 'alt'> & {
    src: {
      _key?: string | null
      _type?: 'image' | string
      asset: {
        _type: 'reference'
        _ref: string
      }
      crop: {
        top: number
        bottom: number
        left: number
        right: number
      } | null
      hotspot: {
        x: number
        y: number
        height: number
        width: number
      } | null
      caption?: string | undefined
    }
    alt?: string
  },
) {
  const { src, ...rest } = props
  const imageBuilder = urlForImage(props.src)
  if (props.width) {
    imageBuilder.width(typeof props.width === 'string' ? parseInt(props.width, 10) : props.width)
  }
  if (props.height) {
    imageBuilder.height(
      typeof props.height === 'string' ? parseInt(props.height, 10) : props.height,
    )
  }

  return (
    <SanityImage
      alt={typeof src.caption === 'string' ? src.caption : ''}
      {...rest}
      src={imageBuilder.url()}
    />
  )
}
JamesSingleton commented 3 months ago

I guess this technically could have been a bug report as well but 🤷🏼‍♂️

coreyward commented 3 months ago

Any library using @sanity/image-url will be inaccurate so long as they ignore this 3.5 year old issue that I documented extensively and even built an entire interactive playground to demonstrate. Sanity doesn't seem to have any interest in fixing it. That is half the reason for why sanity-image exists and uses its own, leaner, more accurate and reliable URL builder.

JamesSingleton commented 3 months ago

@coreyward does sanity-image expose a loader that can be used with next/image? I’m mainly doing some experimenting after the livestream with Lee and Knut.

coreyward commented 3 months ago

A loader is just a function that generates the URLs for your image. Since sanity-image exports both buildSrc and buildSrcSet functions that handle the vast majority of the optimizations performed by the library you can use them in a loader to produce more accurate, optimized image URLs.

This said, I don't believe Next provides you any way to pass the the hotspot and crop values to your loader. They don't even let you pass in the height of the image. It's a hyper-constrained, ridiculously convoluted system that treats devs with kid gloves, all for an exorbitant fee.