👋 Next.js Conf 2024: Come build, party, run, and connect with us! See all events
Last updated May 28, 2019

Internal and external links with Portable Text

Official(made by Sanity team)

By Knut Melvær

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:

// portableText.js
export default {
  name: 'portableText',
  type: 'array',
  title: 'Content',
  of: [
    {
      type: 'block'
    }
  ]
}

A “link” is expressed as a mark annotation in Portable Text. You will have to add an annotation with a reference field to make an internal link.

// portableText.js
export default {
  name: 'portableText',
  type: 'array',
  title: 'Content',
  of: [
    {
      type: 'block',
marks: {
annotations: [
{
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
]
}
]
}
]
}
} ] }

This will add a new icon to the editor’s toolbar that lets you add a reference:

Adding an internal link

Go into the document data using the inspector on the top left corner of the form to see how this rich text content is structured.

[
  {
    "_type": "block",
    "_key": "09b77a1b27b6",
    "style": "normal",
    "markDefs": [
      {
        "_type": "internalLink",
"_key": "38c5fca2ab61",
"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:

// portableText.js
export default {
  name: 'portableText',
  type: 'array',
  title: 'Content',
  of: [
    {
      type: 'block',
      marks: {
        annotations: [
{
name: 'link',
type: 'object',
title: 'External link',
fields: [
{
name: 'href',
type: 'url',
title: 'URL'
},
{
title: 'Open in new tab',
name: 'blank',
description: 'Read https://css-tricks.com/use-target_blank/',
type: 'boolean'
}
]
},
{ 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"]{
  ...,
  body[]{
    ...,
    markDefs[]{
      ...,
      _type == "internalLink" => {
        "slug": @.reference->slug
      }
    }
  }
}

Let’s go through what this query does:

  • *[_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:

// Body.js
import React from 'react'
import {PortableText} from '@portabletext/react'

export const Body = blocks => {
  return <PortableText value={blocks} />
}

Since you have added a custom annotation, you must add a serializer component that tells React how to deal with the data.

// Body.js
import React from 'react'
import {PortableText} from '@portabletext/react'

const components = {
marks: {
internalLink: ({value, children}) => {
const {slug = {}} = value
const href = `/${slug.current}`
return <a href={href}>{children}</a>
}
}
}
export const Body = blocks => {
return <PortableText value={blocks} components={components} />
}

To render the (external) link with the ability to open in a new tab/window, we have to add a component for that as well:

// Body.js
import React from 'react'
import {PortableText} from '@portabletext/react'

const components = {
  marks: {
    internalLink: ({value, children}) => {
      const {slug = {}} = value
      const href = `/${slug.current}`
      return <a href={href}>{children}</a>
    },
link: ({value, children}) => {
// Read https://css-tricks.com/use-target_blank/
const { blank, href } = value
return blank ?
<a href={href} target="_blank" rel="noopener">{children}</a>
: <a href={href}>{children}</a>
}
} } export const Body = blocks => { return <PortableText value={blocks} components={components} /> }

Go to @portabletext/react for more examples in the README.md.

Render the links with Vue

The easiest way to render Portable Text in Vue.js is to add sanity-blocks-vue-component to your project and use the component to render the array:

<template>
  <PortableText
    :blocks="blocks"
  />
</template>

<script>
import PortableText from 'sanity-blocks-vue-component'

export default {
  props: {
    blocks: {
      type: [Array]
      default: () => []
    }
  },
  components: {
    PortableText
  }
}
</script>

Since you have added a custom annotation, you need to add a serializer that tells React how to deal with the data.

<template>
  <PortableText
    :blocks="blocks"
:serializers="serializers"
/> </template> <script> import PortableText from 'sanity-blocks-vue-component' export default { props: { blocks: { type: [Array] default: () => [] } }, components: { PortableText },
data() {
return {
serializers: {
marks: {
internalLink: ({mark, children}) => {
const {slug = {}} = mark
const href = `/${slug.current}`
return <a href={href}>{children}</a>
}
}
}
}
}
} </script>

To render the (external) link with the ability to open in a new tab/window, you have to add a serializer for that as well:

<template>
  <PortableText
    :blocks="blocks"
:serializers="serializers"
/> </template> <script> import PortableText from 'sanity-blocks-vue-component' export default { props: { blocks: { type: [Array] default: () => [] } }, components: { PortableText }, data() { return { serializers: { marks: { internalLink: ({mark, children}) => { const {slug = {}} = mark const href = `/${slug.current}` return <a href={href}>{children}</a> } },
link: ({mark, children}) => {
// Read https://css-tricks.com/use-target_blank/
const { blank, href } = mark
return blank ?
<a href={href} target="_blank" rel="noopener">{children}</a>
: <a href={href}>{children}</a>
}
} } } } </script>

Render the links with Markdown

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.js
const portableText = require('@sanity/block-content-to-markdown')


function body (blocks) {
  return portableText(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.

// body.js
const portableText = require('@sanity/block-content-to-markdown')


const serializers = {
marks: {
internalLink: ({mark, children}) => {
const {slug = {}} = mark
const href = `/${slug.current}`
return `[${children}](${href})`
}
}
}
function body (blocks) { return portableText(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:

// body.js
const portableText = require('@sanity/block-content-to-markdown')


const serializers = {
  marks: {
    internalLink: ({mark, children}) => {
      const {slug = {}} = mark
      const href = `/${slug.current}`
      return `[${children}](${href})`
    },
link: ({mark, children}) => {
// Read https://css-tricks.com/use-target_blank/
const { blank, href } = mark
return blank ?
`<a href=${href} target="_blank" rel="noopener">${children}</a>`
: `[${children}](${href})`
}
} } function body (blocks) { return portableText(blocks, { serializers }) } modules.export = body

Go to the demo on CodeSandbox for a more elaborate example.

Sanity – build remarkable experiences at scale

Sanity Composable Content Cloud is the headless CMS that gives you (and your team) a content backend to drive websites and applications with modern tooling. It offers a real-time editing environment for content creators that’s easy to configure but designed to be customized with JavaScript and React when needed. With the hosted document store, you query content freely and easily integrate with any framework or data source to distribute and enrich content.

Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.

Other guides by author