Using annotations or blocks for dynamic content in Sanity.io

23 replies
Last updated: May 4, 2020
I’m looking to re-produce something similar to the Wordpress short code functionality so my client can make page content dynamic (more on use case in 🧵). Should I use “Annotations” or a “Block” for this? I like how the Annotations are inline by default, which I’ll need. However, a “short code” block would be similar to the Gutenberg Short Code Block. However, I’ve never seen a block be placed inline with a paragraph (hence the name block).
AI Update

Based on the documentation and the specific use case you're describing, annotations are the right choice for WordPress shortcode-like functionality that needs to be inline with text.

Here's why annotations are the better fit:

Annotations are designed for inline content. They're specifically built to be embedded within text spans in Portable Text, making them perfect for inline dynamic content like shortcodes. When you add an annotation, it appears inline with the paragraph text by default - exactly what you need.

Blocks are not inline. As you correctly noted, blocks in Portable Text are standalone content units that sit between paragraphs, not within them. While you can technically create "inline blocks" (objects with inlineBlock: true in your schema), they're more complex and less commonly used. The term "block" itself indicates they're meant to be distinct content units.

How to implement shortcode-like functionality with annotations:

  1. Define custom annotation types in your schema for each shortcode type you need
  2. Store any dynamic values either in the annotation itself or in hidden/read-only fields on the document
  3. Render them on the frontend with custom serializers that replace the annotation with the actual dynamic content

There's a helpful community thread where someone solved almost exactly your use case. They initially tried inline blocks with references but found that annotations combined with custom input components worked better - the annotation marks the position in text, and a custom input component lets editors select which variable to insert.

Quick example schema:

marks: {
  annotations: [
    {
      name: 'dynamicVariable',
      type: 'object',
      title: 'Dynamic Variable',
      icon: CodeIcon,
      fields: [
        {
          name: 'variableType',
          type: 'string',
          title: 'Variable Type',
          options: {
            list: [
              {title: 'User First Name', value: 'userFirstName'},
              {title: 'Current Date', value: 'currentDate'},
              {title: 'Product Price', value: 'productPrice'}
            ]
          }
        }
      ]
    }
  ]
}

Then in your frontend, you'd create a custom serializer for the dynamicVariable annotation that renders the actual dynamic value:

// Example with @portabletext/react
const components = {
  marks: {
    dynamicVariable: ({value, children}) => {
      // Replace with actual dynamic content
      const dynamicValue = getDynamicValue(value.variableType);
      return <span className="dynamic-var">{dynamicValue}</span>;
    }
  }
}

The inline nature, the ability to carry metadata, and the straightforward rendering process make annotations the clear choice for WordPress shortcode-style functionality in Sanity. Your instinct about annotations being inline by default is spot on - that's exactly what makes them perfect for this use case.

You can add custom objects to the block type too. Just add
of: [{type: “aType”}]
to it. https://www.sanity.io/docs/block-type#of-d0f97ffa1dd9
Use Case: For each blog post category, there is a dynamic paragraph that gets displayed on each individual post page. This paragraph is different for each category and sources it’s dynamic data from the fields on the post page. Example:
Hello, welcome to my post called [postTitle]. It was created on [postPublishDate] by [postAuthor].
You can add custom objects to the block type too. Just add
of: [{type: “aType”}]
to it. https://www.sanity.io/docs/block-type#of-d0f97ffa1dd9
user Y
Yes, I have a lot of custom blocks already, that’s the “Block” solution I was referring to as one of my options. However, that will display the full width block in the editor which makes reading the inline content much harder, which was making me lean towards using “Annotations” (like external links are handled). Any down/upside to using one over the other in this case?
If I use an annotation, I would likely use the text being annotated as the reference value to the dynamic data. Re: use case above,
postTitle
=
props.postTitle
on the single blog post page.
Check the doc link ;) it’s not for the array, but for the
block
. This should’ve been covered better in the documentation because it’s pretty powerful
Thanks
user Y
, not sure if there is something else I’m missing but I am implementing custom blocks already.I got this (almost) working using a custom annotation called “DynamicContent”. I pass the page props down to the
BaseBlockContent
which uses a custom serializer. From there, I am matching the
props.children
value with the dynamic page data (page props).
This is working for shallow props, but nested props are returning undefined. Know why?


const content = props.data[props.children]
This is custom inline blocks.

{
	name: "content",
	type:"array",
	of: [
		{
			type: "block",
			of:[{type: "attributeSelection"}]
		}
	]
}
And then you could make a simple object, with predefined strings that you can use to target document fields in the frontend.

{
	name:"attributeSelection",
	type:"object",
	fields:[{
		name:"attribute",
		type:"string",
		options:{
			list:[
			{title:"Publish date", value: "publishedAt"},
			//... and so on
			]
		}
	}]
}
Yup, I have all of that functioning, here is my custom block.
export default {
    title: 'Block Content',
    name: 'blockContent',
    type: 'array',
    of: [
      {
        title: 'Block',
        type: 'block',
        styles: [
          //...
        ],
        lists: [
          //...
        ],
        marks: {
          decorators: [
            //...
          ],
          annotations: [
            //...
            {
              title: 'Dynamic Content',
              name: 'dynamicContent',
              type: 'object',
            }
          ]
        }
      },
    //...
    ]
  }
My challenge is compiling the prop reference from another prop.
Example:
// "postTitle" works and returns "My Post Title successfully
{
  children: [
    0: "postTitle",
  ],
  data: {
    postTitle: "My Post Title",
    category: {
      title: "The Category Title",
    }
  }
}
However:

// "category.title" returns undefined
{
  children: [
    0: "category.title",
  ],
  data: {
    postTitle: "My Post Title",
    category: {
      title: "The Category Title",
    }
  }
}
For some reason, this code doesn’t work with nested objects…
const content = props.data[props.children]
I’m pretty sure that you want to take a closer look at my example :) isn’t this the thing you really want?
Not exactly. You are explicitly referencing another data set, in my case, the reference is dynamic based on the context (props) of the page. This is why I have to use the
children
value and a reference to fetch the pages
props
.
Ok. Let me take a closer look when I’m on my laptop
Use Case:A content section that will display on every blog post page but is unique for each category. Admin can manage this content at the category level, customize the content/paragraph but include dynamic data from the single blog post.


Example:On the single blog post page, you have content that reads:
“Welcome to my post
A Blog Post Title written by Knut Melvæ. Get all your latest news here.”
That post belongs to a category called “News” and in Sanity the field content for the News category is:
“Welcome to my post [TITLE] written by
[AUTHOR]. Get all your latest news here.”
So this isn't the authoring experience you're after
user T
?
And then you could do something like this: https://codesandbox.io/s/winter-sunset-rfebx?file=/src/App.js
Yes, that authoring experience would be fantastic. The key difference is that the dynamic content attribute reference is sourced from another record, not the current document. My body content would be on the Category record and it would be pulling in dynamic content from the blog post record under that category.
Your solution above is similar to what I’ve posted, however it only works on first level object references, you can’t reference anything nested in the props object. I’ve forked your CodeSandbox with an example trying to display a
category.title
. https://codesandbox.io/s/jovial-hypatia-e7s3q?file=/src/App.js:340-389
I was able to get the nested attributes to work by using
eval()
(I’d like feedback if that’s not wise and there are other alternatives). I’ve updated the CodeSandbox above with the new method and it demonstrates the functionality well.
user Y
Thanks for you help, one last point of clarify. In your latest screen recording, how did you configure the “Dynamic content” “Attribute” field reference? Is “Attribute” another document you created just to populate the dropdown and control those options?

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?