Visual Editing with Next.js App Router and Sanity Studio
Setup interactive live preview with Presentation in a Next.js app router application
Go to Visual Editing with Next.js App Router and Sanity StudioHow 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.
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:
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.
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.
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
]
}
]
}
]
}
}
]
}
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 fieldsbody[]{ ... }
For the body array, also output all the fieldsmarkDefs[]{ ... }
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.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.
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>
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 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.
Setup interactive live preview with Presentation in a Next.js app router application
Go to Visual Editing with Next.js App Router and Sanity StudioA complete guide to setting up your blog using Astro and Sanity
Go to Build your blog with Astro and SanityThis guide teaches how to add a custom input component to a field for Sanity Studio v3
Go to How to build an input component for Sanity Studio v3A thorough intro to using GROQ-projections in a webhook contest
Go to GROQ-Powered Webhooks – Intro to Projections