Guide

Internal and external links

Knut Melvær

Knut runs developer relations at Sanity.io.

The editor for Portable Text will give you a link annotation with a URL field by default. This is great for when you want to link to external resources on the web. It isn't the best way to link to content in your dataset though. For that you would want to use the reference field that enables you not only to join in the documents in GROQ and GraphQL, but also keep internal consistency because references can prevent you from accidentily 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 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. To make an internal link we'll have to add an annotation with a reference field.

// 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 interal link

If we go into the document data for this rich text content we'll, we see how it's 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 express "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 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

We have to re-implement the external link since we now have a custom definition of marks.annotations. This gives us also the opportunity to add a boolean for 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 have added some content to it (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 GRQO, you can get one here.

First you have to filter the documents that contains the rich text field with the links. For the sake of this example, we'll 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 we want all of the fields in our posts, and resolve our 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 @sanity/block-content-to-react to your project, and use the component to render the array:

// Body.js
import React from 'react'
import PortableText from '@sanity/block-content-to-react'

const Body = blocks => (
  <PortableText blocks={blocks} />
  )

export default Body

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

// Body.js
import React from 'react'
import PortableText from '@sanity/block-content-to-react'

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

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

// Body.js
import React from 'react'
import PortableText from '@sanity/block-content-to-react'

const 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>
}
} } const Body = blocks => ( <PortableText blocks={blocks} serializers={serializers} /> ) export default Body

Demo

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 we have added a custom annotation we 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>

In order to render the (external) link with the ability to open in a new tab/window, we 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>

Demo

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 we have added a custom annotation we 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

In order to render the (external) link with the ability to open in a new tab/window, we have to add a serializer for that as well. Since Markdown hasn't it's own 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

Demo