When migrating from platforms like Webflow or WordPress to Sanity CMS, content editors often encounter friction when they discover that standard features they previously took for granted are no longer available out of the box. One of the most commonly requested features is the ability to choose whether a link opens in the same tab or a new tab.
While Sanity's Portable Text offers powerful, rich text editing capabilities, the default link annotation doesn't include this option. This limitation can be particularly frustrating for teams who are used to having granular control over their content presentation, especially when migrating large amounts of content from other platforms.
In this tutorial, we'll walk through the process of extending Sanity's link annotation to include an "Open in New Tab" option. We'll cover both the schema configuration in Sanity Studio and the frontend implementation, ensuring your links behave exactly as expected.
By the end of this tutorial, you'll be able to:
Before we begin, you should have:
The solution we'll implement involves three main components:
target="_blank"
and rel="noopener noreferrer"
attributes when necessary.This approach is recommended because:
Let's dive into the implementation!
Before we customize the link behavior, let's understand how Sanity structures link annotations by default.
In Sanity's Portable Text, links are implemented as "annotations" that can be applied to spans of text. The default link annotation schema is quite simple:
// Default Sanity link annotation schema// Believe it or not, this code...{ name: 'content', title: 'Content', type: 'array', of: [ { type: 'block' } ]}// ...and this code...{ name: 'content', title: 'Content', type: 'array', of: [ { type: 'block', marks: { annotations: [ { type: 'object', name: 'link', title: 'Link', i18nTitleKey: 'inputs.portable-text.annotation.link', options: { modal: { type: 'popover' }, }, fields: [ { name: 'href', type: 'url', title: 'Link', description: 'A valid web, email, phone, or relative link.', validation: (Rule: any) => Rule.uri({ scheme: ['http', 'https', 'tel', 'mailto'], allowRelative: true, }), }, ], }, ], }, }, ],},// ...produce the same thing
They both create a basic link dialog with just a Link/URL field:
When a content editor adds a link, this results in a data structure like this:
// Example Portable Text data with a link{ _type: 'block', children: [ { _key: 'a1b2c3', _type: 'span', marks: ['link-1'], text: 'Visit our website' } ], markDefs: [ { _key: 'link-1', _type: 'link', href: 'https://example.com' } ]}
Note that there's no information about whether the link should open in a new tab or not. Our job is to extend this structure to include that option.
ContentWrap helps companies save thousands on their Sanity bills through smart caching strategies and optimized API usage.
First, let's modify the link annotation schema to include our new option:
export default { type: 'object', name: 'link', title: 'Link', i18nTitleKey: 'inputs.portable-text.annotation.link', options: { modal: { type: 'popover' }, }, fields: [ { name: 'href', type: 'url', title: 'Link', description: 'A valid web, email, phone, or relative link.', validation: (Rule: any) => Rule.uri({ scheme: ['http', 'https', 'tel', 'mailto'], allowRelative: true, }), }, { name: 'openInNewTab', title: 'Open in new tab?', type: 'boolean', }, ], initialValue: { openInNewTab: false, },};
We've added a new boolean field called blank
that will store whether the link should open in a new tab. We've set its initial value to false
so that links open in the same tab by default.
Now that we have our custom link annotation, we need to make sure it's used in our block content fields. Let's update our block content schema:
import link from './annotations/link'export default { title: 'Block Content', name: 'blockContent', type: 'array', of: [ { title: 'Block', type: 'block', marks: { annotations: [ link // Other annotations... ] } }, // Other block content types... ]}
Make sure that the blockContent
schema (or whatever you've named your rich text field) is using the link annotation we defined.
At this point, when you restart your Sanity Studio, content editors should see a checkbox in the link dialog:
Now that our Sanity schema is updated, we need to modify our frontend code to respect the blank
property when rendering links.
If you're using @portabletext/react
for rendering (recommended for newer projects), here's how you can customize the link component:
import { PortableText } from '@portabletext/react'import Link from 'next/link' // Assuming Next.js usageconst components = { marks: { link: ({value, children}) => { const { href, blank } = value // Check if it's an internal or external link const isExternal = href && ( href.startsWith('http') || href.startsWith('mailto:') || href.startsWith('tel:') ) // For external links if (isExternal) { return ( <a href={href} target={blank ? '_blank' : undefined} rel={blank ? 'noopener noreferrer' : undefined} > {children} </a> ) } // For internal links return ( <Link href={href}> <a target={blank ? '_blank' : undefined}> {children} </a> </Link> ) } }}const MyPortableText = ({value}) => { return <PortableText value={value} components={components} />}export default MyPortableText
If you're using the older @sanity/block-content-to-react
package:
import BlockContent from '@sanity/block-content-to-react'import Link from 'next/link'const serializers = { marks: { link: ({mark, children}) => { const { href, blank } = mark const isExternal = href && ( href.startsWith('http') || href.startsWith('mailto:') || href.startsWith('tel:') ) if (isExternal) { return ( <a href={href} target={blank ? '_blank' : undefined} rel={blank ? 'noopener noreferrer' : undefined} > {children} </a> ) } return ( <Link href={href}> <a target={blank ? '_blank' : undefined}> {children} </a> </Link> ) } }}const MyBlockContent = ({blocks}) => { return <BlockContent blocks={blocks} serializers={serializers} />}export default MyBlockContent
For better user experience, you might want to visually indicate links that open in new tabs. Here's a simple CSS approach:
// Modified link component with external link indicatorconst ExternalLinkIcon = () => ( <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ marginLeft: '0.25rem' }} > <path d="M2.5 9.5L9.5 2.5M9.5 2.5H5M9.5 2.5V7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg>)// Then in your link component:return ( <a href={href} target={blank ? '_blank' : undefined} rel={blank ? 'noopener noreferrer' : undefined} className={blank ? 'external-link' : undefined} > {children} {blank && <ExternalLinkIcon />} </a>)
This adds a small external link icon to links that open in new tabs, providing a visual cue to users.
Now that we've implemented both the Sanity Studio customization and the frontend rendering, it's time to test everything:
Your content editors should now have a seamless experience managing link behavior, and your frontend should correctly render links with the appropriate tab behavior.
Solution: This usually happens because the custom component isn't properly registered or the schema doesn't match.
sanity.json
or sanity.config.js
properly registers the custom componentSolution: This is typically a frontend rendering issue.
blank
propertytarget="_blank"
attribute is being addedSolution: Always add proper security attributes to external links.
rel="noopener noreferrer"
for all links that open in new tabswindow.opener
objectSolution: This can happen with custom components in Sanity Studio.
When properly implemented, your content editors will have a clean, intuitive interface for controlling link behavior:
And on the frontend, your links will behave exactly as specified, with proper attributes for security and accessibility:
<!-- Example of rendered HTML for links that open in new tabs --><a href="https://example.com" target="_blank" rel="noopener noreferrer"> Visit our website <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> <!-- SVG path data for external link icon --> </svg></a><!-- Example of rendered HTML for links that open in the same tab --><a href="https://example.com">Visit our website</a>
In this tutorial, we've successfully implemented a custom "Open in New Tab" option for links in Sanity CMS. This seemingly simple feature adds significant value for content editors, especially those transitioning from platforms like Webflow or WordPress.
By extending Sanity's schema and providing a custom form component, we've enhanced the content editing experience without compromising on flexibility or performance. Our frontend implementation ensures that links behave as expected while maintaining proper security and accessibility standards.
This customization is just one example of how Sanity's flexibility allows you to create tailored content management experiences. As you grow more comfortable with Sanity's schema system, you can extend this approach to add other link-related features such as custom styling options, link tracking parameters, or even automatic external link detection.
Now that you've mastered link customization in Sanity, consider exploring these related topics:
In this tutorial, you will learn how to extend Sanity CMS with custom superscript and subscript decorators for block array fields.
Eliminate timezone headaches in Sanity CMS with the rich-date-input plugin. Learn how to prevent publishing date confusion and ensure consistent datetime display for global teams with this simple yet powerful solution.
Frustrated by Sanity's inability to wrap text around images? This tutorial shows you how to build a custom component that gives your content editors the power to float images left or right with adjustable width controls — just like Webflow.