How make internal and external links with Portable Text and render them in frontends
The editor for Portable Text will give you a link annotation with a URL field by default. This is great for you to link to external resources on the web. It isn’t the best way to link to the content in your dataset, though. For that, you would want to use the reference field that enables you to join in the documents in GROQ and GraphQL and keep internal consistency because references can prevent you from accidentally deleting documents that have references on them.
Additionally, you may want to add more fields to the external link field. The typical example is something that can be serialized to the target="_blank" attribute in HTML to open pages in a new tab or window.
So let’s take a look at how this can be achieved.
Internal links
Either find an existing rich text field or make a new one for where you need it. We’ll begin with the minimal configuration:
"reference":{"_type":"reference","_ref":"cf23ddb6-953d-4596-a5c0-dde6213e8e7f"}}],"children":[{"_type":"span","_key":"09b77a1b27b60","text":"Go to this ","marks":[]},{"_type":"span","_key":"09b77a1b27b61","text":"post","marks":[
"38c5fca2ab61"
]},{"_type":"span","_key":"09b77a1b27b62","text":" to learn more.","marks":[]}]}]
This JSON structure expresses “Go to this post to learn more,” where “post” has a mark that references the mark definition of the type “internalLink,” which has a reference to another document, i.e., by its ID.
Protip
If you don't want a reference to prevent the deletion of the target document, you can add weak: true to the reference field in the schema definition. You will still get a warning, but be able to delete the target document.
External links
You have to re-implement the external link since we now have a custom definition of marks.annotations. This also gives you the opportunity to add a boolean to open the link in a new tab:
{name:'internalLink',type:'object',title:'Internal link',fields:[{name:'reference',type:'reference',title:'Reference',to:[{type:'post'},// other types you may want to link to]}]}]}}]}
Querying the links with GROQ
Now that you have configured the editor and added some content (preferably with links), you’re ready to query your content in your API. This section will take you through how to do it in GROQ and the reasoning behind how to query this data structure. If you need a general introduction to GROQ, you can get one here.
First, you must filter the documents containing the rich text field with the links. For the sake of this example, say that these documents are of the type post. The field where we use the portableText type is called body, defined like this:
{name:'body',type:'portableText',title:'Body'}
Let’s say you want all of the fields in our posts and resolve your internal links to the target post’s slug field. The GROQ query could look like this:
*[_type == "post"] Select all the documents, and filter them down by the type “post”
{ ..., } Project and output all the fields
body[]{ ... } For the body array, also output all the fields
markDefs[]{ ... } If the object in the body array has an array field called markDefs, loop through that, output all the fields in it
_type == "internalLink" => { "slug": @.reference->slug } If the object in markDefs is of the type “internalLink,” output a key called “slug,” follow the reference, and return the document’s slug.
Render the links with React
The easiest way to render Portable Text in React is to add @portabletext/react to your project, and use the component to render the array:
You can also render Portable Text to Markdown. This can be useful if you want to use content from Sanity with a static site generator that only consumes Markdown files. You can run a script that writes these files from Sanity before the generator builds the site (see Codesandbox demo below).
// body.jsconst portableText =require('@sanity/block-content-to-markdown')functionbody(blocks){returnportableText(blocks)}
modules.export = body
Since you have added a custom annotation, you need to add a serializer that tells React how to deal with the data.
functionbody(blocks){returnportableText(blocks,{ serializers })}
modules.export = body
To render the (external) link with the ability to open in a new tab/window, you must also add a serializer for that. Since Markdown hasn’t a syntax for the target attribute, we’ll have to use HTML to achieve this:
Sanity – The Content Operating System that ends your CMS nightmares
Sanity replaces rigid content systems with a developer-first operating system. Define schemas in TypeScript, customize the editor with React, and deliver content anywhere with GROQ. Your team ships in minutes while you focus on building features, not maintaining infrastructure.
Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.
Automatically track when content was first published with a timestamp that sets once and never overwrites, providing reliable publication history for analytics and editorial workflows.
AI-powered automatic tagging for Sanity blog posts that analyzes content to generate 3 relevant tags, maintaining consistency by reusing existing tags from your content library.