A minimal implementation of footnotes as inline annotations in Sanity's Portable Text, rendered with bidirectional links in a Next.js frontend.
Footnotes are implemented as mark annotations (not separate blocks) on Portable Text. Each footnote annotation carries its own text field (using a simple Portable Text type for rich text support).
footnote— an object type used as a mark annotation insidebodyPortableTextexcerptPortableText— a simple Portable Text array used for the footnote text contentbodyPortableText— the main content field, withfootnoteregistered as an annotation alongside links
footnotes.ts— extracts footnotes from Portable Text blocks by scanningmarkDefs, de-duplicates by_key, and assigns sequential indicesfootnotes.tsx— renders the "Notes" section at the bottom with backlinks (↩)portable-text.tsx— thefootnotemark renders a superscript[n]link pointing to the footnote section
Sanity Studio → author highlights text → adds footnote annotation with text
↓
Portable Text block.markDefs = [{ _type: "footnote", _key: "abc", text: [...] }]
↓
Frontend: extractFootnotes() scans markDefs → builds ordered list
↓
Inline: [n] superscript link (#fn-abc) ←→ Footnote section: backlink (#fnref-abc)
sanity(schema definitions)@sanity/icons(editor icon)next-sanity/@portabletext/react(rendering)
In a page component:
import { extractFootnotes } from './lib/footnotes'
import { Footnotes } from './components/footnotes'
import { CustomPortableText } from './components/portable-text'
export default async function PostPage({ params }) {
const post = await fetchPost(params.slug)
const footnotes = extractFootnotes(post.body)
return (
<article>
<CustomPortableText value={post.body} footnotes={footnotes} />
<Footnotes footnotes={footnotes} />
</article>
)
}