Portable Text link reference not resolving in GROQ query with markDefs

11 replies
Last updated: Oct 15, 2021
Hello, I think I might've found a bug?
I have followed this tutorial to create 2 types of rich-text links, both exernal and internal (
https://www.sanity.io/guides/portable-text-internal-and-external-links )
However the GROQ Query will not follow the reference.

This query
*[_type == "post"][7] {

title,

slug { current },

publishedAt,

"author": author->name,

mainImage,

"categories": categories[]->title,

"website": website->webName,

body[]{

...,

asset->{

...,

"_key": _id

},

_type == "productTable" => {

"productImg1": @.productImage1.asset->url,

"productImg2": @.productImage2.asset->url,

"productImg3": @.productImage3.asset->url,

"productImg4": @.productImage4.asset->url,

}

},

markDefs[]{

...,

_type == "refLink" => {

"slug": @.reference->slug

},

_type == "extLink" => {

"slug": @.reference->slug

},

}

}

Will output:


"body":*[*_26 items_

0:*{*_6 items_

"_key":"637903d82c20"

"_type":"block"

"asset":*NULL*

"children":*[*_1 item_

0:*{*_4 items_

"_key":"9dd80e448ec8"

"_type":"span"

"marks":*[*...*]*_1 item_

"text":"Internal Link Test"

}

]

"markDefs":*[*_1 item_

0:*{*_3 items_

"_key":"f5dd3d1740d9"

"_type":"refLink"

"reference":*{*_2 items_

"_ref":"001fd7ed-dcb5-4dcc-9b8b-a25cd4770254"

"_type":"reference"

}

}

]

"style":"normal"

}


etcetera


A bit strange. Also, I have to click 2 times on Sanity Studio for the Link options to pop-up. When I click the second time, I can add the info, but then the Studio tells me "Invalid block value"--it seems to be adding two links because of the 2 clicks. Once I remove invalid marks, it works OK, but it's a strange workflow!

Is this at all known? I don't seem to find any mistakes on my part
AI Update

I can see you're dealing with two issues here: the GROQ query not properly dereferencing links in markDefs, and a UI issue with double-clicking in Studio. Let me help you troubleshoot both.

The GROQ Query Issue

Looking at your query output, the markDefs array in your body is not being expanded properly - it's showing the reference object {"_ref": "...", "_type": "reference"} instead of the resolved slug. The issue is that markDefs lives inside each block within the body array, not at the top level of your query.

Your current query structure has the markDefs expansion outside and separate from the body[] projection. You need to move it inside the body projection. Based on this community answer about resolving references in Portable Text, you need to use the @ operator to reference the current scope when dealing with references inside arrays.

Try this corrected structure:

*[_type == "post"][7] {
  title,
  slug { current },
  publishedAt,
  "author": author->name,
  mainImage,
  "categories": categories[]->title,
  "website": website->webName,
  body[]{
    ...,
    asset->{
      ...,
      "_key": _id
    },
    _type == "productTable" => {
      "productImg1": @.productImage1.asset->url,
      "productImg2": @.productImage2.asset->url,
      "productImg3": @.productImage3.asset->url,
      "productImg4": @.productImage4.asset->url,
    },
    markDefs[]{
      ...,
      _type == "refLink" => {
        "slug": @.reference->slug
      },
      _type == "extLink" => {
        "href": @.href
      }
    }
  }
}

The key changes:

  1. Move markDefs[] inside the body[]{} projection
  2. Use @.reference->slug to dereference the internal link (the @ operator refers to the current markDef object)
  3. For external links, you probably just want @.href since they typically don't have references

According to the Portable Text annotations documentation, each block has its own markDefs array, so you need to expand it at the block level.

The Double-Click Studio Issue

The "Invalid block value" error and needing to click twice to open link options sounds like a schema configuration issue. A few things to check:

  1. Make sure your annotation types are defined as separate object types at the schema root level (not just inline in your block content definition)
  2. Verify they're correctly referenced in your block content's marks.annotations array
  3. Check for duplicate definitions - the double-click behavior often happens when there's a schema mismatch

Your refLink and extLink should each be defined something like:

defineType({
  name: 'refLink',
  type: 'object',
  title: 'Internal Link',
  fields: [
    {
      name: 'reference',
      type: 'reference',
      to: [{type: 'post'}] // or whatever document types you're linking to
    }
  ]
})

Then in your block content schema:

marks: {
  annotations: [
    {name: 'refLink', type: 'refLink'},
    {name: 'extLink', type: 'extLink'}
  ]
}

The "Invalid block value" message typically means the data structure being created doesn't match what your schema expects. Check your browser console for any schema validation warnings when you're editing - they'll give you more specific clues about what's mismatched.

If you're still seeing issues after these changes, share your schema definitions for refLink and extLink and I can help spot any configuration problems!

Show original thread
11 replies
Hey! I haven't seen this sort of behavior reported before. Just to check, can I see your schema for your portable text array and links?
Sure!
user M


/**
 
* This is the schema definition for the rich text fields used for
 
* for this blog studio. When you import it in schemas.js it can be
 
* reused in other parts of the studio with:
 
*  {
 
*    name: 'someName',
 
*    title: 'Some title',
 
*    type: 'blockContent'
 
*  }
 
*/

export
_default_ {
 
title: 'Block Content',
 
name: 'blockContent',
 
type: 'array',
 
of: [
   
{
     
title: 'Block',
     
type: 'block',

      _// Styles let you set what your user can mark up blocks with. These_

      _// correspond with HTML tags, but you can set any title or value_

      _// you want and decide how you want to deal with it where you want to_

      _// use your content._
     
styles: [
       
{title: 'Normal', value: 'normal'},
       
{title: 'H1', value: 'h1'},
       
{title: 'H2', value: 'h2'},
       
{title: 'H3', value: 'h3'},
       
{title: 'H4', value: 'h4'},
       
{title: 'Quote', value: 'blockquote'},
     
],
     
lists: [{title: 'Bullet', value: 'bullet'}],

      _// Marks let you mark up inline text in the block editor._
     
marks: {

        _// Decorators usually describe a single property – e.g. a typographic_

        _// preference or highlighting by editors._
       
decorators: [
         
{title: 'Strong', value: 'strong'},
         
{title: 'Emphasis', value: 'em'},
       
],

        _// Annotations can be any object structure – e.g. a link or a footnote._
       
annotations: [
         
{
           
title: 'Link Externo',
           
name: 'extLink',
           
type: 'object',
           
fields: [
             
{
               
name: 'href',
               
type: 'url',
               
title: 'URL'
             
},
             
{
               
title: 'Abrir en nueva pestaña',
               
name: 'blank',
               
description: 'Más Info <https://css-tricks.com/use-target_blank/>',
               
type: 'boolean'
             
},
             
{
               
title: 'Añadir Nofollow',
               
name: 'nofollow',
               
type: 'boolean'
             
}
           
],
         
},
         
{
           
name: 'refLink',
           
type: 'object',
           
title: 'Link Interno',
           
fields: [
             
{
               
name: 'reference',
               
type: 'reference',
               
title: 'Reference',
               
to: [ { type: 'post' } ]
             
},
           
]
         
},

          
       
],
     
},
   
},

    _// You can add additional types here. Note that you can't use_

    _// primitive types such as 'string' and 'number' in the same array_

    _// as a block type._
   
{
     
type: 'image',
     
options: {hotspot: true},
   
},
   
{
     
type: 'cta',
   
},
   
{
     
type: 'infoText',
   
},
   
{
     
type: 'prosAndCons',
   
},
   
{
     
type: 'productTable',
   
},
 
],

}
Sure!
user M


/**
 
* This is the schema definition for the rich text fields used for
 
* for this blog studio. When you import it in schemas.js it can be
 
* reused in other parts of the studio with:
 
*  {
 
*    name: 'someName',
 
*    title: 'Some title',
 
*    type: 'blockContent'
 
*  }
 
*/

export
_default_ {
 
title: 'Block Content',
 
name: 'blockContent',
 
type: 'array',
 
of: [
   
{
     
title: 'Block',
     
type: 'block',

      _// Styles let you set what your user can mark up blocks with. These_

      _// correspond with HTML tags, but you can set any title or value_

      _// you want and decide how you want to deal with it where you want to_

      _// use your content._
     
styles: [
       
{title: 'Normal', value: 'normal'},
       
{title: 'H1', value: 'h1'},
       
{title: 'H2', value: 'h2'},
       
{title: 'H3', value: 'h3'},
       
{title: 'H4', value: 'h4'},
       
{title: 'Quote', value: 'blockquote'},
     
],
     
lists: [{title: 'Bullet', value: 'bullet'}],

      _// Marks let you mark up inline text in the block editor._
     
marks: {

        _// Decorators usually describe a single property – e.g. a typographic_

        _// preference or highlighting by editors._
       
decorators: [
         
{title: 'Strong', value: 'strong'},
         
{title: 'Emphasis', value: 'em'},
       
],

        _// Annotations can be any object structure – e.g. a link or a footnote._
       
annotations: [
         
{
           
title: 'Link Externo',
           
name: 'extLink',
           
type: 'object',
           
fields: [
             
{
               
name: 'href',
               
type: 'url',
               
title: 'URL'
             
},
             
{
               
title: 'Abrir en nueva pestaña',
               
name: 'blank',
               
description: 'Más Info <https://css-tricks.com/use-target_blank/>',
               
type: 'boolean'
             
},
             
{
               
title: 'Añadir Nofollow',
               
name: 'nofollow',
               
type: 'boolean'
             
}
           
],
         
},
         
{
           
name: 'refLink',
           
type: 'object',
           
title: 'Link Interno',
           
fields: [
             
{
               
name: 'reference',
               
type: 'reference',
               
title: 'Reference',
               
to: [ { type: 'post' } ]
             
},
           
]
         
},

          
       
],
     
},
   
},

    _// You can add additional types here. Note that you can't use_

    _// primitive types such as 'string' and 'number' in the same array_

    _// as a block type._
   
{
     
type: 'image',
     
options: {hotspot: true},
   
},
   
{
     
type: 'cta',
   
},
   
{
     
type: 'infoText',
   
},
   
{
     
type: 'prosAndCons',
   
},
   
{
     
type: 'productTable',
   
},
 
],

}
Cool, thanks! I'll spin this up and report back.
Here's a zip of the repo in case you need something more robust
user M
:
Many thanks!
Ok! Got it!
markDefs
was outside of
body
so it was returning
null
. If you move it up into body it should be return what you're looking for:
body[] {
      ..., 
      asset->{
        ...,
        "_key": _id
      },
      _type == "productTable" => {
        "productImg1": @.productImage1.asset->url,
        "productImg2": @.productImage2.asset->url,
        "productImg3": @.productImage3.asset->url,
        "productImg4": @.productImage4.asset->url,
      },
markDefs[]{
      ...,
            _type == "refLink" => {
        "slug": @.reference->slug
      },
      _type == "extLink" => {
        "slug": @.reference->slug
      },
    }
    },
The link options were popping up fine for me, so I'm not sure what's happening with that behavior. Do you think you could get a screen grab of it the next time it happens? Also, is there anything in the console after it fails that first time?
Ok! Got it!
markDefs
was outside of
body
so it was returning
null
. If you move it up into body it should be return what you're looking for:
body[] {
      ..., 
      asset->{
        ...,
        "_key": _id
      },
      _type == "productTable" => {
        "productImg1": @.productImage1.asset->url,
        "productImg2": @.productImage2.asset->url,
        "productImg3": @.productImage3.asset->url,
        "productImg4": @.productImage4.asset->url,
      },
markDefs[]{
      ...,
            _type == "refLink" => {
        "slug": @.reference->slug
      },
      _type == "extLink" => {
        "slug": @.reference->slug
      },
    }
    },
The link options were popping up fine for me, so I'm not sure what's happening with that behavior. Do you think you could get a screen grab of it the next time it happens? Also, is there anything in the console after it fails that first time?
Oh God, that was the stupidest mistake! Thanks a lot!
I'll run some tests with the double clicking, record some videos and circle around ! Thanks!!
Haha, it's a totally understandable mistake!
Haha, it's a totally understandable mistake!

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?