it seems like there are some ghost mark types creeping their way into my block content
Good 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.
The Problem
When you add an annotation (like a link) to text in Portable Text, Sanity creates two things:
- A mark on the text span (e.g.,
marks: ["a5a909c5150a"]) - A corresponding entry in the
markDefsarray with that ID
When 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.
The Solution: Clean Your Content
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.
Quick Fix: Add Default Serializers
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.
Prevention
To prevent this in the future:
- Keep your Sanity Studio dependencies updated
- If you're doing programmatic updates to Portable Text, make sure you're removing both the mark reference AND the markDefs entry
- The newer versions of the Portable Text editor handle annotation removal more reliably
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.
Show original thread9 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.