Bram Doppen
Senior Solution Architect @ Sanity
Automatically generate accessible, multilingual alt text for images in your Sanity Media Library using Agent Actions and Sanity Functions
import {defineAssetAspect, defineField, defineArrayMember} from 'sanity'
const languages = [
{title: 'Dutch', value: 'nl'},
{title: 'English', value: 'en'},
{title: 'French', value: 'fr'},
{title: 'German', value: 'de'},
]
export default defineAssetAspect({
name: 'altText',
title: 'Alternative text',
description: 'Accessible alternative text for this asset, in one or more languages.',
type: 'array',
of: [
defineArrayMember({
name: 'altTextItem',
type: 'object',
fields: [
defineField({
name: 'language',
type: 'string',
description: 'The language that the alt text is written in',
options: {
list: languages,
layout: 'radio',
},
}),
defineField({
name: 'value',
title: 'Alternative text',
type: 'string',
description: 'Short description of the image, for screen readers (max ~100 characters).',
}),
],
preview: {
select: {
title: 'value',
subtitle: 'language',
},
},
}),
],
})import {documentEventHandler} from '@sanity/functions'
import {createClient} from '@sanity/client'
const MAX_KEYWORD_WAIT = 5 // How many times to retry (total attempts = MAX_KEYWORD_WAIT + 1)
const KEYWORD_WAIT_MS = 1500 // Wait 1.5 seconds between checks
const languages = ['nl', 'en', 'fr', 'de']
// We have to wait for the keywords to be available because they are auto-generated by the Media Library when an image is uploaded.
async function waitForKeywords(
fetchKeywords: () => Promise<string[] | undefined>,
): Promise<string[] | undefined> {
for (let i = 0; i <= MAX_KEYWORD_WAIT; i++) {
console.log('Waiting for keywords...', i)
const keywords = await fetchKeywords()
if (keywords && keywords.length > 0) {
return keywords
}
if (i < MAX_KEYWORD_WAIT) {
await new Promise((res) => setTimeout(res, KEYWORD_WAIT_MS))
}
}
return undefined
}
export const handler = documentEventHandler(async ({context, event}) => {
const mlId = context.eventResourceId
const {_id, currentVersion} = event.data
const detailedAssetId = currentVersion?._ref
if (!detailedAssetId) {
console.log('No detailedAssetId found, skipping')
return
}
// Create a Media Library client
const mediaLibraryClient = createClient({
token: context.clientOptions.token,
useCdn: false,
apiVersion: '2025-05-08',
resource: {
type: 'media-library',
id: mlId,
},
})
// Query keywords from the Media Library asset using client.fetch()
const fetchKeywords = async () => {
try {
const result = await mediaLibraryClient.fetch<{keywords?: string[]}>(
`*[_id == $assetId][0]{ "keywords": metadata.keywords }`,
{assetId: detailedAssetId},
)
return result?.keywords || []
} catch (err) {
console.error('Failed fetching keywords from asset', err)
return []
}
}
const keywords = await waitForKeywords(fetchKeywords)
if (!keywords || keywords.length === 0) {
console.log('No keywords found after retries, skipping')
return
}
// Generate alt text based on the keywords using Agent Actions
const agentClient = createClient({
...context.clientOptions,
dataset: 'production',
apiVersion: 'vX',
})
// Generate alt text for each language separately for reliability
const altTextItemsArray: {_key: string; _type: string; language: string; value: string}[] = []
for (const lang of languages) {
const altText = await agentClient.agent.action.prompt({
instruction: `Given the following keywords: [${keywords.join(', ')}], generate a short (max 100 chars) alt text in language: ${lang}. Respond with just the alt text string, no quotes or formatting.`,
})
altTextItemsArray.push({
_key: crypto.randomUUID(),
_type: 'altTextItem',
language: lang,
value: String(altText).trim(),
})
}
// Update the asset with the alt text using the Media Library client
const result = await mediaLibraryClient
.patch(_id)
.setIfMissing({aspects: {}})
.set({'aspects.altText': altTextItemsArray})
.commit()
console.log('Mutation response:', JSON.stringify(result, null, 2))
})import {defineBlueprint, defineMediaLibraryAssetFunction} from '@sanity/blueprints'
export default defineBlueprint({
resources: [
defineMediaLibraryAssetFunction({
name: 'media-library-auto-alt-text',
memory: 2,
timeout: 30,
src: './functions/media-library-auto-alt-text',
event: {
on: ['create', 'update'],
filter: 'assetType == "sanity.imageAsset" && !defined(aspects.altText)',
projection: '{ _id, currentVersion }',
resource: {
type: 'media-library',
id: '<your-media-library-id>', // TODO: replace with your media library id
},
},
}),
],
})The Problem: Content teams need to provide accessible alt text for images across multiple languages, but manually writing alt text for every asset is time-consuming and often inconsistent. As the asset count grows and audiences become more global, creating and maintaining high-quality multilingual alt text becomes a bottleneck, leading to accessibility gaps and a poorer experience for screen reader users.
The Solution: This function automatically generates concise, descriptive alt text for images in your Media Library using Sanity’s Agent Actions. When an asset is created or updated, the function waits for Sanity’s auto-generated image keywords, then produces multilingual alt text and stores it directly on the asset as an aspect. Editors get accessible, editable alt text by default without manual effort.
Quick Start
View the complete example and source code.
Initialize blueprints if you haven't already:npx sanity blueprints init
Add the function to your project:npx sanity blueprints add function --example media-library-auto-alt-text
Deploy to production:npx sanity blueprints deploy
How It Works
When an image asset is created or updated in the Media Library, the function automatically:
Key Benefits
Technical Implementation
The function uses Sanity’s Media Library Asset Functions combined with AI Agent Actions. It reads machine-generated image keywords from the underlying image asset, generates alt text per language, and writes the results to a custom Media Library aspect attached to the asset container.
Perfect For
The function is compatible with any Sanity project that has Media Library enabled and can be easily customized to add or remove languages, adjust prompts, or integrate review and approval workflows.
Senior Solution Architect @ Sanity
Automatically track when content was first published with a timestamp that sets once and never overwrites, providing reliable publication history for analytics and editorial workflows.
Go to First Published Timestamp FunctionAI-powered automatic tagging for Sanity blog posts that analyzes content to generate 3 relevant tags, maintaining consistency by reusing existing tags from your content library.
Go to Automatically tag blog postsOn document publish, send a Slack notification so your team is informed
Go to Notify your team via Slack on publishCreate, preview, and send Klaviyo campaigns without ever leaving Sanity Studio"
Go to Klaviyo campaigns without leaving your StudioContent backend


The only platform powering content operations


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store