
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeGood news - these "ghost marks" with hex IDs are a common issue with leftover annotation references in your Portable Text content! Here's what's happening and how to fix it.
When you add an annotation (like a link) to text in Portable Text, Sanity creates two things:
marks: ["a5a909c5150a"])markDefs array with that IDWhen you remove the annotation through the editor, sometimes the mark reference gets left behind in the text span even though the markDefs entry is gone. This creates "ghost marks" that your serializer doesn't recognize.
You need to clean up these orphaned mark references from your documents. Here's a migration script approach using the Sanity migration toolkit:
import {defineMigration, at} from 'sanity/migrate'
const cleanGhostMarks = defineMigration({
title: 'Clean ghost marks from portable text',
documentTypes: ['yourDocType'],
migrate: {
document(doc, context) {
// Clean your block content field
if (doc.yourBlockField) {
return at('yourBlockField', cleanBlockContent(doc.yourBlockField))
}
}
}
})
function cleanBlockContent(blocks) {
return blocks.map(block => {
if (block._type !== 'block' || !block.children) return block
const validMarkKeys = new Set(
(block.markDefs || []).map(def => def._key)
)
return {
...block,
children: block.children.map(child => ({
...child,
marks: (child.marks || []).filter(mark =>
validMarkKeys.has(mark) ||
['strong', 'em', 'underline', 'code'].includes(mark) // keep decorators
)
}))
}
})
}This script filters out any marks that don't have a corresponding entry in markDefs, keeping only valid decorator marks (bold, italic, etc.) and annotations.
While you work on cleaning the data, you can suppress the warnings by adding a catch-all serializer:
// For @sanity/block-content-to-react (deprecated)
<BlockContent
blocks={content}
serializers={{
marks: {
// Your existing serializers...
// These hex IDs will just render as plain text
'a5a909c5150a': ({children}) => children,
'80061e4e5eb1': ({children}) => children,
'1df06a28145f': ({children}) => children,
}
}}
/>
// For @portabletext/react (recommended)
<PortableText
value={content}
components={{
marks: {
// Add serializers for each ghost mark
'a5a909c5150a': ({children}) => <>{children}</>,
'80061e4e5eb1': ({children}) => <>{children}</>,
'1df06a28145f': ({children}) => <>{children}</>,
}
}}
/>Note: If you're still using @sanity/block-content-to-react, consider migrating to @portabletext/react as the old library is deprecated.
To prevent this in the future:
The root cause is usually from older versions of the editor or custom annotation handling code that didn't properly clean up both parts of the annotation.
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.
Content operations
Content backend


The only platform powering content operations
By Industry


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