Query translated Portable Text with nested references in Sanity

8 replies
Last updated: Jan 29, 2026
Following the field level translation documentation, I have a content object that contains the languages as keys. Normally I can access any field like this.

"content": coalesce(content[$lang], content.en)

This content is a Portable text field, and I want to query the current language or fallback, but I want to be able to reach the references inside that object, like referenced images.

Say I want to access this asset of
pageImage
that is a block inside `content`:

_type == "pageImage" => {

...,

"alt": coalesce(alt[$lang], alt.en),

"imageBlock": imageBlock {

"asset": asset->url,

"dimensions": asset->metadata.dimensions

},

},

This doesn’t work:


"content": coalesce(content[$lang], content.en) {

....,

_type == "pageImage" => {

...,

alt,

"imageBlock": imageBlock {

"asset": asset->url,

"dimensions": asset->metadata.dimensions

},

},

}

How would you query that?
AI Update

I understand you're trying to project into a Portable Text field that uses field-level translation, where you need to access references (like image assets) within the translated content blocks.

The key is to use the array projection operator [] immediately after your coalesce expression. This tells GROQ to iterate over each element in the array and apply the projection to each block:

"content": coalesce(content[$lang], content.en)[] {
  ...,
  _type == "pageImage" => {
    ...,
    "alt": coalesce(alt[$lang], alt.en),
    "imageBlock": imageBlock {
      "asset": asset->url,
      "dimensions": asset->metadata.dimensions
    }
  }
}

The [] operator is what enables you to then use conditional projections (the => operator) and dereference operations (->) within each block.

Here's a more complete example that handles different block types:

"content": coalesce(content[$lang], content.en)[] {
  _type != "pageImage" => @,
  _type == "pageImage" => {
    _type,
    _key,
    "alt": coalesce(alt[$lang], alt.en),
    "imageBlock": imageBlock {
      "asset": asset->url,
      "dimensions": asset->metadata.dimensions
    }
  }
}

In this pattern:

  • The @ operator (identity operator from GROQ's pipeline components) passes through non-pageImage blocks unchanged
  • pageImage blocks get the special projection with language fallback and reference dereferencing
  • The asset->url syntax works because you're inside the projection context where the reference operator can resolve

The array projection operator [] is part of GROQ's pipeline architecture, where operations are chained together to transform data. The coalesce() function returns the first non-null value, and when followed by [], that value (which should be an array) is projected element-by-element.

Important note: While this approach works, the Sanity documentation on localization recommends against using field-level translation for Portable Text fields, as it can quickly consume attribute limits and create complex data structures. Document-level localization is generally preferred for Portable Text content.

Show original thread
8 replies

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?