How to override the default preview configuration for an image block in Portable Text in Sanity.io

6 replies
Last updated: Sep 28, 2022
Hi all. I’m trying to write a
preview
configuration for an
image
block within Portable Text. By default, the image is shown in thumbnail form with its filename as the preview title. In trying to override that I started with the following:
preview: {
  select: {
    media: 'asset',
  },
},
Sanity correctly resolves the reference to the asset and the preview includes a thumbnail of the image, but it appears as “Untitled”. If I also want to retrieve other fields on the asset, e.g. the
altText
, using the dot-notation to follow the reference, it breaks the image handling. This doesn’t work:
preview: {
  select: {
    media: 'asset',
    title: 'asset.altText',
  },
},
It looks like using the dot notation somehow prevents the built-in image handling code from following the reference. I can do this instead, specifically picking out the image url, but it results in the full-size image being loaded which isn’t ideal:

preview: {
  select: {
    image: 'asset',
    alt: 'asset.altText',
    imageUrl: 'asset.url',
  },
  prepare (selection) {
    const { image, alt, imageUrl } = selection;
    return {
      title: alt,
      media: (
        <img src={imageUrl} alt={alt} />
      ),
    };
  },
},
I would need to write more code to request a thumbnailed version of the image using the asset API. Is there a way to avoid this so I can just use a simple
select
without a complicated
prepare
, but still be able to pick out asset fields as well as using the built-in handling of the asset itself? Thanks!
AI Update

I understand the frustration! Unfortunately, what you're running into is a limitation of how Sanity's preview.select works with image assets.

The issue is that when you use dot notation like asset.altText in your select, Sanity's built-in image handling for the media field gets disrupted. The preview configuration documentation shows that while you can use dot notation to follow references (like author.name), this doesn't work smoothly when you need both the asset reference itself AND fields from that asset in the same preview.

The good news: You don't need to manually construct <img> tags or use the full-size image URL. When you use a prepare function, you can still pass the asset reference directly to media and Sanity will handle the thumbnail generation automatically:

preview: {
  select: {
    asset: 'asset',
    alt: 'asset.altText',
  },
  prepare({ asset, alt }) {
    return {
      title: alt || 'Untitled',
      media: asset,  // Sanity handles thumbnailing automatically
    }
  }
}

The key insight is that in the prepare function's return object, the media property can accept an asset reference directly—you don't need to manually extract URLs or render image components. Sanity's preview system will automatically generate an appropriately-sized thumbnail from the asset reference.

This is much simpler than your third approach with the manual <img> tag and asset.url, and it avoids loading full-size images. The limitation is that there's no pure select-only solution when you want to access both the asset reference (for thumbnail display) and fields from that asset (like altText)—you need the prepare function to handle both, but it's still quite straightforward.

If you have other asset fields you want to display, you can select them all and use them in the prepare function:

preview: {
  select: {
    asset: 'asset',
    alt: 'asset.altText',
    filename: 'asset.originalFilename',
  },
  prepare({ asset, alt, filename }) {
    return {
      title: alt || filename || 'Untitled',
      subtitle: filename,
      media: asset,
    }
  }
}

This gives you full control over how asset metadata is displayed while still leveraging Sanity's built-in thumbnail handling.

Re that extra code, this is what I’ve ended up with, pulling in the necessary extra fields to build the image URL – it does work, but it would be really good to avoid this boilerplate if the built-in image handling could be made to behave when extra fields on the asset are queried.
import React from 'react';
import SanityImageUrlBuilder from '@sanity/image-url';

const imageUrlBuilder = SanityImageUrlBuilder({
    projectId: process.env.SANITY_STUDIO_API_PROJECT_ID,
    dataset: process.env.SANITY_STUDIO_API_DATASET,
});

export default {
    // ...
    preview: {
        select: {
            asset: 'asset',
            _id: 'asset._id',
            altText: 'asset.altText',
            assetId: 'asset.assetId',
            extension: 'asset.extension',
            metadata: 'asset.metadata',
            originalFilename: 'asset.originalFilename',
            path: 'asset.path',
            url: 'asset.url',
        },
        prepare ({ asset }) {
            const url = imageUrlBuilder.image(asset).width(33).height(33).fit('crop').url();
            return {
                title: asset.altText || asset.originalFilename,
                media: (
                    <img src={url} alt={asset.altText} />
                ),
            };
        },
    },
};
OK, I solved this! When using dot notation the type of the reference gets changed to
sanity.imageAsset
. It’s necessary to set it back to
reference
to allow the default preview component to understand the image:
preview: {
  select: {
    asset: 'asset',
    altText: 'asset.altText',
    originalFilename: 'asset.originalFilename',
  },
  prepare ({ asset, altText, originalFilename }) {
    return {
      title: altText || originalFilename,
      media: {
        ...asset,
        _type: 'reference',
      },
    };
  },
},

I’m not sure how commonplace this use-case is but it might be worth adding a note to this section of documentation to cover it off?
Thanks for walking us through this! I'll flag our docs team to add an example.
Thank you!
Sorry, it was a bit of a journey 😛

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?