How to replace an image referenced hundreds of times in Sanity?

8 replies
Last updated: Sep 8, 2022
Hey all, is there a way to replace an image in dashboard, that is referenced hundreds of times?
AI Update

Yes, you can definitely replace an image that's referenced hundreds of times! Since the image asset is stored separately from your content documents (which only reference it), you have a couple of approaches:

Best Approach: Use a Migration Script

The most reliable way is to upload your new image and then use Sanity's migration tooling to update all references across your dataset. This ensures all documents are updated consistently.

Here's how to do it:

1. First, upload your new image (either through Studio or programmatically) and note its asset ID.

2. Create a migration script:

npx sanity migration create replaceImageAsset

3. Write the migration to find and replace all references:

import {defineMigration, at, set} from 'sanity/migrate'

const OLD_ASSET_ID = 'image-abc123...' // Your old image asset ID
const NEW_ASSET_ID = 'image-xyz789...' // Your new image asset ID

export default defineMigration({
  title: 'Replace image asset across all documents',
  
  migrate: {
    document(doc, context) {
      const mutations = []
      
      // Helper function to recursively find and replace asset references
      function findAndReplaceAsset(obj: any, path: string[] = []) {
        if (!obj || typeof obj !== 'object') return
        
        // Check if this is an asset reference
        if (obj._ref === OLD_ASSET_ID) {
          mutations.push(at([...path, '_ref'], set(NEW_ASSET_ID)))
        }
        
        // Recursively check nested objects and arrays
        for (const [key, value] of Object.entries(obj)) {
          if (key.startsWith('_')) continue // Skip system fields
          findAndReplaceAsset(value, [...path, key])
        }
      }
      
      findAndReplaceAsset(doc)
      return mutations
    }
  }
})

4. Test with dry-run first:

npx sanity migration run replaceImageAsset --dry-run

5. Execute the migration:

npx sanity migration run replaceImageAsset

Alternative: Target Specific Fields

If you know exactly which fields contain the image (like mainImage or thumbnail), you can make the migration more targeted:

export default defineMigration({
  title: 'Replace specific image field',
  documentTypes: ['post', 'page'], // Only run on certain document types
  
  migrate: {
    document(doc, context) {
      const mutations = []
      
      // Check mainImage field
      if (doc.mainImage?.asset?._ref === OLD_ASSET_ID) {
        mutations.push(at('mainImage.asset._ref', set(NEW_ASSET_ID)))
      }
      
      // Check thumbnail field
      if (doc.thumbnail?.asset?._ref === OLD_ASSET_ID) {
        mutations.push(at('thumbnail.asset._ref', set(NEW_ASSET_ID)))
      }
      
      return mutations
    }
  }
})

Why Not Delete and Re-upload?

You might wonder if you can just delete the old asset and upload a new one with the same ID. Unfortunately, Sanity's asset IDs are system-generated based on the file content (using SHA-1 hashing), so you can't specify custom asset IDs during upload. The migration approach is the supported way to handle this.

Additional Tips

  • The migration will handle all references automatically, even in nested structures
  • Run it with --dry-run first to see what will change
  • The old asset will remain in your dataset until you manually delete it (references don't prevent deletion)
  • Consider using GROQ to find all documents referencing the old asset first: *[references("image-abc123...")]

This approach is reliable, reversible (you can write another migration to switch back), and handles edge cases like nested references automatically.

Show original thread
8 replies
Not without a migration script to do so.
Can you give me a starter on that please?
You'll need to run a patch on any document that contains a reference to that particular asset.
This can also be done using the JS Client if you prefer to write a script that you run using
sanity exec
. This is an example I previously shared:
import { studioClient } from './studioClient';
import cq from 'concurrent-queue';

// Create a queue to limit the rate at which you write changes to Sanity
let queue = cq()
  .limit({ concurrency: 2 })
  .process(task => {
    return new Promise(function (resolve, reject) {
      setTimeout(resolve.bind(undefined, task), 1000);
    });
  });

const mutateDocs = async () => {
  //Fetch the documents you need to mutate
  const query = `*[<your-query>]`;
  const docs = await studioClient.fetch(query);
  // Loop through all of the docs returned from our query
  for (const doc of docs) {
    queue(doc).then(async () => {
      // Add a message to help us know the upload is happening
      console.log(`Mutating ${doc._id}`);
      // Tell the client to patch the current document
      studioClient
        .patch(doc._id)
        // Set the field
        .set({
          //your changes here
        })
        // Commit the changes
        .commit()
        .then((updatedDoc) =>
          console.log(`Hurray, the doc is updated! New document:`, updatedDoc)
        )
        .catch((err) =>
          console.error('Oh no, the update failed: ', err.message)
        );
    // });
  }
};

mutateDocs();

// execute this script by running
// $ sanity exec ./lib/utils/mutateDocs.js --withUserToken
Great, thanks. Do you know if I can keep the hot spots this way?
You can, but you have to explicitly set it. Just remember to take the asset's crop and hot spot and apply it to the new one in your patch.
Thanks, I'll look into this.
user M
Once again saving my bum with some deep-dive research here to alleviate what I ran into here https://sanity-io-land.slack.com/archives/C9Z7RC3V1/p1661800253439929
I recognize that CQ is a separate package, and throttled to 15 requests a second (nice to know nothing else waits on the updates because it takes a few seconds at a hundred plus docs) but now my default sort ordering listens to an invented and patched field I made!

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?