Time to Complete: 45-60 minutes
Are your content editors constantly asking how to make links open in new tabs? If you're migrating from Webflow to Sanity CMS, you've probably noticed this seemingly simple feature is missing from Sanity's default configuration. In this tutorial, we'll solve this problem once and for all.
When migrating from platforms like Webflow or WordPress to Sanity CMS, content editors often experience friction when they discover that standard features they 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:
1// Default Sanity link annotation schema
2
3// Believe it or not, this code...
4{
5 name: 'content',
6 title: 'Content',
7 type: 'array',
8 of: [
9 { type: 'block' }
10 ]
11}
12
13// ...and this code...
14{
15 name: 'content',
16 title: 'Content',
17 type: 'array',
18 of: [
19 {
20 type: 'block',
21 marks: {
22 annotations: [
23 {
24 type: 'object',
25 name: 'link',
26 title: 'Link',
27 i18nTitleKey: 'inputs.portable-text.annotation.link',
28 options: {
29 modal: { type: 'popover' },
30 },
31 fields: [
32 {
33 name: 'href',
34 type: 'url',
35 title: 'Link',
36 description: 'A valid web, email, phone, or relative link.',
37 validation: (Rule: any) =>
38 Rule.uri({
39 scheme: ['http', 'https', 'tel', 'mailto'],
40 allowRelative: true,
41 }),
42 },
43 ],
44 },
45 ],
46 },
47 },
48 ],
49},
50
51// ...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:
1// Example Portable Text data with a link
2{
3 _type: 'block',
4 children: [
5 {
6 _key: 'a1b2c3',
7 _type: 'span',
8 marks: ['link-1'],
9 text: 'Visit our website'
10 }
11 ],
12 markDefs: [
13 {
14 _key: 'link-1',
15 _type: 'link',
16 href: 'https://example.com'
17 }
18 ]
19}
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.
First, let's modify the link annotation schema to include our new option:
1// schemas/annotations/link.js
2
3export default {
4 type: 'object',
5 name: 'link',
6 title: 'Link',
7 i18nTitleKey: 'inputs.portable-text.annotation.link',
8 options: {
9 modal: { type: 'popover' },
10 },
11 fields: [
12 {
13 name: 'href',
14 type: 'url',
15 title: 'Link',
16 description: 'A valid web, email, phone, or relative link.',
17 validation: (Rule: any) =>
18 Rule.uri({
19 scheme: ['http', 'https', 'tel', 'mailto'],
20 allowRelative: true,
21 }),
22 },
23 {
24 name: 'openInNewTab',
25 title: 'Open in new tab?',
26 type: 'boolean',
27 },
28 ],
29 initialValue: {
30 openInNewTab: false,
31 },
32};
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:
1// schemas/blockContent.js
2
3import link from './annotations/link'
4
5export default {
6 title: 'Block Content',
7 name: 'blockContent',
8 type: 'array',
9 of: [
10 {
11 title: 'Block',
12 type: 'block',
13 marks: {
14 annotations: [
15 link
16 // Other annotations...
17 ]
18 }
19 },
20 // Other block content types...
21 ]
22}
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:
1// components/PortableText.jsx
2
3import { PortableText } from '@portabletext/react'
4import Link from 'next/link' // Assuming Next.js usage
5
6const components = {
7 marks: {
8 link: ({value, children}) => {
9 const { href, blank } = value
10
11 // Check if it's an internal or external link
12 const isExternal = href && (
13 href.startsWith('http') ||
14 href.startsWith('mailto:') ||
15 href.startsWith('tel:')
16 )
17
18 // For external links
19 if (isExternal) {
20 return (
21 <a
22 href={href}
23 target={blank ? '_blank' : undefined}
24 rel={blank ? 'noopener noreferrer' : undefined}
25 >
26 {children}
27 </a>
28 )
29 }
30
31 // For internal links
32 return (
33 <Link href={href}>
34 <a target={blank ? '_blank' : undefined}>
35 {children}
36 </a>
37 </Link>
38 )
39 }
40 }
41}
42
43const MyPortableText = ({value}) => {
44 return <PortableText value={value} components={components} />
45}
46
47export default MyPortableText
If you're using the older @sanity/block-content-to-react
package:
1// components/BlockContent.jsx
2
3import BlockContent from '@sanity/block-content-to-react'
4import Link from 'next/link'
5
6const serializers = {
7 marks: {
8 link: ({mark, children}) => {
9 const { href, blank } = mark
10
11 const isExternal = href && (
12 href.startsWith('http') ||
13 href.startsWith('mailto:') ||
14 href.startsWith('tel:')
15 )
16
17 if (isExternal) {
18 return (
19 <a
20 href={href}
21 target={blank ? '_blank' : undefined}
22 rel={blank ? 'noopener noreferrer' : undefined}
23 >
24 {children}
25 </a>
26 )
27 }
28
29 return (
30 <Link href={href}>
31 <a target={blank ? '_blank' : undefined}>
32 {children}
33 </a>
34 </Link>
35 )
36 }
37 }
38}
39
40const MyBlockContent = ({blocks}) => {
41 return <BlockContent blocks={blocks} serializers={serializers} />
42}
43
44export default MyBlockContent
For better user experience, you might want to visually indicate links that open in new tabs. Here's a simple CSS approach:
1// Modified link component with external link indicator
2
3const ExternalLinkIcon = () => (
4 <svg
5 width="12"
6 height="12"
7 viewBox="0 0 12 12"
8 fill="none"
9 xmlns="http://www.w3.org/2000/svg"
10 style={{ marginLeft: '0.25rem' }}
11 >
12 <path
13 d="M2.5 9.5L9.5 2.5M9.5 2.5H5M9.5 2.5V7"
14 stroke="currentColor"
15 strokeWidth="1.5"
16 strokeLinecap="round"
17 strokeLinejoin="round"
18 />
19 </svg>
20)
21
22// Then in your link component:
23return (
24 <a
25 href={href}
26 target={blank ? '_blank' : undefined}
27 rel={blank ? 'noopener noreferrer' : undefined}
28 className={blank ? 'external-link' : undefined}
29 >
30 {children}
31 {blank && <ExternalLinkIcon />}
32 </a>
33)
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:
1<!-- Example of rendered HTML for links that open in new tabs -->
2<a href="https://example.com" target="_blank" rel="noopener noreferrer">
3 Visit our website
4 <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
5 <!-- SVG path data for external link icon -->
6 </svg>
7</a>
8
9<!-- Example of rendered HTML for links that open in the same tab -->
10<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: