Sanity CDN images not loading in Safari (production only) with next/image

5 replies
Last updated: Oct 18, 2022
I’m having loading issues on Safari as there seems to be a problem with recognizing the source link to the sanity content lake (in production only, localhost works fine). Below is the
<img/>
rendered (by next/image), check src:Any idea what’s causing the loading problem here? Why doesn’t Safari get the served image as an image file?


<img
  alt="Alt 1"
  src="/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Febplvzey%2Fproduction%2F89d3576237a2b8840362eb811207e9d336137ed5-1200x630.jpg%3Fq%3D100&w=1920&q=100"
  decoding="async"
  data-nimg="responsive"
  style="position: absolute; inset: 0px; box-sizing: border-box; padding: 0px; border: none; margin: auto; display: block; width: 0px; height: 0px; min-width: 100%; max-width: 100%; min-height: 100%; max-height: 100%;"
  sizes="100vw"
  srcset="/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Febplvzey%2Fproduction%2F89d3576237a2b8840362eb811207e9d336137ed5-1200x630.jpg%3Fq%3D100&w=360&q=100 360w,
  /_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Febplvzey%2Fproduction%2F89d3576237a2b8840362eb811207e9d336137ed5-1200x630.jpg%3Fq%3D100&w=768&q=100 768w,
  /_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Febplvzey%2Fproduction%2F89d3576237a2b8840362eb811207e9d336137ed5-1200x630.jpg%3Fq%3D100&w=1024&q=100 1024w,
  /_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Febplvzey%2Fproduction%2F89d3576237a2b8840362eb811207e9d336137ed5-1200x630.jpg%3Fq%3D100&w=1440&q=100 1440w,
  /_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Febplvzey%2Fproduction%2F89d3576237a2b8840362eb811207e9d336137ed5-1200x630.jpg%3Fq%3D100&w=1920&q=100 1920w"
>

AI Update

This is most likely a Next.js configuration issue where cdn.sanity.io hasn't been properly added to your allowed image domains. The fact that it works on localhost but fails in production on Safari is a strong indicator of missing Next.js image optimization configuration.

When you use next/image with external image sources like Sanity's CDN, Next.js requires explicit configuration to allow those domains. Without this configuration, Next.js won't optimize the images properly, and browsers (particularly Safari) may fail to render them.

The primary solution is to configure your next.config.js:

For Next.js 12.3+ (recommended):

module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.sanity.io',
      },
    ],
  },
}

For older Next.js versions:

module.exports = {
  images: {
    domains: ['cdn.sanity.io'],
  },
}

After adding this configuration, you'll need to rebuild and redeploy your application.

Why this affects Safari specifically:

Safari tends to be more strict about content types and security policies than other browsers. When Next.js can't properly optimize the image due to missing domain configuration, it may serve the image in a way that Safari's security policies reject, while Chrome or Firefox might be more lenient. The localhost environment often bypasses some of these optimization checks, which is why you see different behavior between development and production.

Additional troubleshooting steps:

  1. Verify the image is accessible - Test the direct Sanity CDN URL (without Next.js's /_next/image wrapper) in Safari: https://cdn.sanity.io/images/ebplvzey/production/89d3576237a2b8840362eb811207e9d336137ed5-1200x630.jpg. If this loads fine, it confirms the issue is with Next.js configuration, not Sanity's CDN.

  2. Check your browser console - Look for any specific error messages in Safari's developer console that might provide more details about why the image isn't loading.

  3. Use Sanity's image URL builder - If you're not already using it, the @sanity/image-url helper library makes it easier to construct properly formatted Sanity image URLs. According to Sanity's Image Pipeline documentation, these URLs are designed to work seamlessly with modern frameworks like Next.js.

The remotePatterns (or domains) configuration is the standard requirement for using external image sources with Next.js Image component, and adding this should resolve your Safari loading issue in production.

Show original thread
5 replies
Hello
user B
, must be frustrating!Could you share your nextJS component/page where the image appears in?
Is this issue only happening in Safari or in other browsers as well?
Is it only this one image or all images you load from sanity? Has there been any errors during build (and are you using SSR or SSG?)
Hi
user J
thank you for checking back on this!1. This issue is only occurring on Safari 15.6.1 (15613.3.9.1.16, 15613), Version 16.0 (17614.1.25.9.10, 17614) is working fine.
2. Happening with all images coming from Sanity via this component, and all using SSG (hosted on Netlify). A curious detail: for a brief moment on load the blur placeholder appears.
👀I created a custom component for dealign with images, which utilizes
next/image
and `@sanity/image-url`:
import React from 'react'
import Image from 'next/image'
import { inRange } from '../../lib/helpers'
import { urlFor } from '../../lib/sanity'

export default function Figure({ behavior, item }) {
  let media = {
    key: item?._key,
    type: item?._type,
    image: item?.image ?? item,
    video: item?.video ? {
      url: item?.video,
      caption: item?.caption
    } : {}
  }

  // Image
  const iWidth = media?.image?.asset?.metadata?.dimensions?.width
  const iHeight = media?.image?.asset?.metadata?.dimensions?.height

  // Crop
  const cWidth = Math.round(media?.image?.asset?.metadata?.dimensions?.width * (media?.image?.crop ? (1 - (media?.image?.crop?.left + media?.image?.crop?.right)) : 1))
  const cHeight = Math.round(media?.image?.asset?.metadata?.dimensions?.height * (media?.image?.crop ? (1 - (media?.image?.crop?.top + media?.image?.crop?.bottom)) : 1))
  const cX1 = Math.round(media?.image?.asset?.metadata?.dimensions?.width * (media?.image?.crop ? media?.image?.crop?.left : 0))
  const cX2 = cX1 + cWidth
  const cY1 = Math.round(media?.image?.asset?.metadata?.dimensions?.height * (media?.image?.crop ? media?.image?.crop?.top : 0))
  const cY2 = cY1 + cHeight

  // Hotspot
  const hIX = Math.round(media?.image?.hotspot?.x * iWidth)
  const hIY = Math.round(media?.image?.hotspot?.y * iHeight)
  const hCX = (hIX - cX1) / (cX2 - cX1)
  const hCY = (hIY - cY1) / (cY2 - cY1)

  return item ? (
    <figure>
      <div>
        <Image
          src={
            urlFor(media?.image)
              .rect(
                Math.round(media?.image?.asset?.metadata?.dimensions?.width * (media?.image?.crop ? media?.image?.crop?.left : 0)),
                Math.round(media?.image?.asset?.metadata?.dimensions?.height * (media?.image?.crop ? media?.image?.crop?.top : 0)),
                Math.round(media?.image?.asset?.metadata?.dimensions?.width * (media?.image?.crop ? (1 - (media?.image?.crop?.left + media?.image?.crop?.right)) : 1)),
                Math.round(media?.image?.asset?.metadata?.dimensions?.height * (media?.image?.crop ? (1 - (media?.image?.crop?.top + media?.image?.crop?.bottom)) : 1))
              )
              .quality(100)
              .url()
          }
          width={
            (behavior !== 'fill') && (
              media?.image?.crop ?
                (media?.image?.asset?.metadata?.dimensions?.width * (1 - (media?.image?.crop?.left + media?.image?.crop?.right))) :
                (media?.image?.asset?.metadata?.dimensions?.width)
            )
          }
          height={
            (behavior !== 'fill') && (
              media?.image?.crop ?
                (media?.image?.asset?.metadata?.dimensions?.height * (1 - (media?.image?.crop?.top + media?.image?.crop?.bottom))) :
                (media?.image?.asset?.metadata?.dimensions?.height)
            )
          }
          layout={behavior ?? 'responsive'}
          objectFit={behavior === 'fill' && 'cover'}
          objectPosition={
            (behavior === 'fill') && (
              media?.image?.hotspot ?
                (`
                  ${inRange(hCX, 0, (1/3)) ? 'left' : ''}
                  ${inRange(hCX, (1/3), (2/3)) ? 'center' : ''}
                  ${inRange(hCX, (2/3), 1) ? 'right' : ''}
                  ${inRange(hCY, 0, (1/3)) ? 'top' : ''}
                  ${inRange(hCY, (1/3), (2/3)) ? 'center' : ''}
                  ${inRange(hCY, (2/3), 1) ? 'bottom' : ''}
                `) : ('center center')
            )
          }
          quality={100}
          placeholder='blur'
          blurDataURL={media?.image?.asset?.metadata?.lqip ?? media?.image?.asset?.metadata?.palette?.dominant?.background}
          lazyBoundary='100px'
          alt={(media?.image?.alt ?? media?.image?.caption)}
        />
      </div>
    </figure>
  ) : null
}
Thats very curious indeed… that only one version of Safari is not working 🤔 Do you get any error messages in the console?

You put in a lot of work into the component, but did you know we have solutions for this built into the
urlForImage and a nextjs image plugin , that gives you a lot of options as well?
No error messages in the console, only a broken image with no content/information in the resources panel.
Yes I definitely cleaned up my code a lot since my original posting, integrated a couple of features from the resources you listed above. But I wanted to rebuild the logic first, just to understand it a bit better.
🤓
BUT looking into it again, I finally figured out the problem/solution: Nothing to do with sanity, all with the
next/image
component apparently “missing” a loader prop for it to properly render the returned URL string. 🤯

<Image
	loader={() => urlFor(media?.image).width(media?.image?.asset?.metadata?.dimensions?.width).url()}
	src={urlFor(media?.image).url()}
	…
/>
Thank you again for saving me from fading into despair!
🌻
Thank you again for saving me from fading into despair!
Please it was all you 🙂 But happy to be of service with any input 🌻
And impressive you were able to come this far without our tooling
:chefs-kiss:

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?