Index
Edit

Customization

Sanity’s editor for Portable Text is built to be easily customized for different editorial needs. You can configure and tailor several different editors throughout the studio. To learn more about how to configure the editor in this chapter.

Most of the customizations is done by passing React components into the schema definitions for content types using Portable Text. They will also accept simple strings.

Toolbar Icons and span rendering

When you configure custom markers, that is decorators (simple values) and annotations (rich data structures) they will appear as icon affordances in the toolbar. Out of the box the icon will be the Sanity S. Chances are that you want to change this.

If you add custom decorators and annotations there's also a good chance you want to control their visual representations in the editor. By default decorators will be invisible, and annotations will show with grey background and a dotted underline.

Decorators

You have made a decorator for highlighting using this configuration:

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      marks: {
        decorators: [
          { title: 'Strong', value: 'strong' },
          { title: 'Emphasis', value: 'em' },
          { title: 'Code', value: 'code' },
          { title: 'Highlight', value: 'highlight' }
        ]
      }
    }
  ]
}

Now, let's add a custom toolbar icon by passing in an anonymous function with just the string H to blockEditor.icon:

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      marks: {
        decorators: [
          { title: 'Strong', value: 'strong' },
          { title: 'Emphasis', value: 'em' },
          { title: 'Code', value: 'code' },
          { 
            title: 'Highlight',
            value: 'highlight',
            blockEditor: {
              icon: () => 'H'
            }
          }
        ]
      }
    }
  ]
}

The string is rendered in the decorator button in the toolbar:

Toolbar with custom decorator button

You can also pass in JSX directly in the schema, or via an import. If you do so, remember to add import React from 'react' in the file with the configuration. Here we have just set some simple inline styling to a span with an “H”.

import React from 'react'

const highlightIcon = () => (
  <span style={{fontWeight: 'bold'}}>H</span>
  )

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      marks: {
        decorators: [
          { title: 'Strong', value: 'strong' },
          { title: 'Emphasis', value: 'em' },
          { title: 'Code', value: 'code' },
          {
            title: 'Highlight',
            value: 'highlight',
            blockEditor: {
              icon: highlightIcon
            }
          }
        ]
      }
    }
  ]
}

The next step is to render the actual hightlighted text in the editor. We do this by passing the props into a React component, and wrap them in a span with some styling.

import React from 'react'

const highlightIcon = () => (
  <span style={{ fontWeight: 'bold' }}>H</span>
)
const highlightRender = props => (
  <span style={{ backgroundColor: 'yellow' }}>{props.children}</span>
)

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      marks: {
        decorators: [
          { title: 'Strong', value: 'strong' },
          { title: 'Emphasis', value: 'em' },
          { title: 'Code', value: 'code' },
          {
            title: 'Highlight',
            value: 'highlight',
            blockEditor: {
              icon: highlightIcon,
              render: highlightRender
            }
          }
        ]
      }
    }
  ]
}

In the editor we will now have a yellow background for highlighted text:

Editor with custom render and icon for the highlight decorator
Pro-tip

Sometimes you would like to keep all, or some, of the built in decorators when adding your own ones. The built in decorators are the following:

{"title": "Strong", "value": "strong" },
{ "title": "Emphasis", "value": "em" },
{ "title": "Code", "value": "code" },
{ "title": "Underline", "value": "underline" },
{ "title": "Strike", "value": "strike-through" }

Make sure to include the ones you would like to keep.

Annotations

Customizing annotations works much in the same way as decorations. You can pass in borth an icon and a renderer in the schema definitions. A common use case is to have an annotation for an internal reference, in addition to a link with an external URL. Let's customize the editor so that we use a custom icon for the internal link, and a renderer that shows inline in the text wich of the links are external.

Sanity comes with the react-icons library as a dependency. We'll use a paper clip icon for internal references.

import linkIcon from 'react-icons/lib/fa/paperclip'

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      marks: {
        annotations: [
          {
            name: 'link',
            type: 'object',
            title: 'link',
            fields: [
              {
                name: 'url',
                type: 'url'
              }
            ]
          },
          {
            name: 'internalLink',
            type: 'object',
            title: 'Internal link',
            blockEditor: {
              icon: linkIcon
            },
            fields: [
              {
                name: 'reference',
                type: 'reference',
                to: [
                  { type: 'post' }
                  // other types you may want to link to
                ]
              }
            ]
          }
        ]
      }
    }
  ]
}

Now the S-icon will be replaced with the paperclip:

The editor with a custom paperclip icon for the internal link annotation

Custom renderer

The next step is to make a custom renderer for external links, appending an "arrow out of a box" icon to those marks. We'll do this by passing in a small React component. Let's make a file called ExternalLinkRenderer.js and put it in /schemas/components (you are free to structure the files however you want).

// ExternalLinkRenderer.js
import React from 'react'
import PropTypes from 'prop-types'
import ExternalLinkIcon from 'react-icons/lib/fa/external-link'

const ExternalLinkRenderer = props => (
  <span>
    {props.children} <ExternalLinkIcon />
  </span>
)

ExternalLinkRenderer.propTypes = {
  children: PropTypes.node.isRequired
}

export default ExternalLinkRenderer

Now we can import this component and pass it to blockEditor.render in our schema:

import linkIcon from 'react-icons/lib/fa/paperclip'
import ExternalLinkRenderer from './components/ExternalLinkRenderer'

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      marks: {
        annotations: [
          {
            name: 'link',
            type: 'object',
            title: 'link',
            blockEditor: {
              render: ExternalLinkRenderer
            },
            fields: [
              {
                name: 'url',
                type: 'url'
              }
            ]
          },
          {
            name: 'internalLink',
            type: 'object',
            title: 'Internal link',
            blockEditor: {
              icon: linkIcon
            },
            fields: [
              {
                name: 'reference',
                type: 'reference',
                to: [
                  { type: 'post' }
                  // other types you may want to link to
                ]
              }
            ]
          }
        ]
      }
    }
  ]
}

Now external links will look like this:

The editor with custom renderer for external links.

Block Styles

The editor for Portable Text comes with a set of styles that translate well to those you'll find in HTML. Your might not be targeting HTML though. While it has always been possible to add and configure block styles, you can now also configure how these styles render in the editor using markup and CSS modules. This means you can tune your editor to be aligned with your organization’s design system.

To illustrate this, let's make a custom title style using Garamond as the font-face and a slightly increased font size. We begin with defining a custom style called title:

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      styles: [
        {title: 'Normal', value: 'normal'},
        {title: 'Title', value: 'title'},
        {title: 'H1', value: 'h1'},
        {title: 'H2', value: 'h2'},
        {title: 'H3', value: 'h3'},
        {title: 'Quote', value: 'blockquote'},
      ]
    }
  ]
}

Without any customization the block will just look the same as the normal. In order to style it, we'll need to make a renderer in React. It works pretty much the same way as renderers for marks: We pass in a React component to blockEditor.render. The values for the block is comes in as prop, and we can wrap it in the approiate element with either inline styling, or using CSS Modules. In this example we'll just add the React component to the configuration file to illustrate how it's done.

import React from 'react'

const TitleStyle = props => (
  <span style={{fontFamily: 'Garamond', fontSize: '2em'}}>{props.children}</span>
)
export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      styles: [
        {title: 'Normal', value: 'normal'},
        {title: 'H1', value: 'h1'},
        {title: 'H2', value: 'h2'},
        {title: 'H3', value: 'h3'},
        {title: 'Quote', value: 'blockquote'},
        {
          title: 'Title',
          value: 'title',
          blockEditor: {
            render: TitleStyle
          }
        },
      ]
    }
  ]
}

Now the title block will render with the customized style in the editor:

The editor with a custom title block style

Custom paste handling

The editor is built to convert HTML, Word (with most of its weird edge cases!), and Google Docs content from the clipboard to Portable Text.

There are cases where you want more control over how content from the clipboard should be handled. Maybe you want to support some custom markup, or convert certain HTML patterns to a custom content type. Customizing the paste handling can be a great way to let your editors structure legacy content by copy-pasting, when scripting or automatisation is not a feasible option.

Example: Custom pasting of code blocks in HTML

Perhaps you want to convert the <pre><code>/* some code */</code></pre> pattern in HTML into a custom content type for code in Portable Text, so that you can query it in GROQ, and have more controll over the frontend rendering. There are two ways of adding custom paste handling to Sanity’s studio. You can do it globally for all rich text editors by adding your paste handler function in the parts array of sanity.json, or you can pass it in as a prop for a specific rich text field in your project.

The custom paste handler function is passed a ClipboardEvent with clipboardData. You can learn more about these on the MDN web docs.

You are free to name and place your paste handler anywhere in your project, just make sure to get the path right:

{
  "implements": "part:@sanity/form-builder/input/block-editor/on-paste",
  "path": "./customization/onPaste.js"
}

In our example we'll add the custom paste handler to a specific editor. We'll start by writing the paste handler function. Here's a bare bones example that only logs out the pasted content and returns undefined, leaving it to the default configuration.

import React from 'react'
import {BlockEditor} from 'part:@sanity/form-builder'

export default class CustomEditor extends React.PureComponent {
  render() {
    return (
      <div>
        <BlockEditor {...this.props} onPaste={handlePaste} />
      </div>
    )
  }
}

function handlePaste(input) {
  const {event} = input
  const json = event.clipboardData.getData('text/plain')
  const text  = event.clipboardData.getData('application/json')
  if (json) {
    console.log(json)
  }
  if (text) {
    console.log(text)
  }
  // return undefined to let the defaults do the work
  return undefined
}

To use this customized editor, import and pass it to inputComponent:

import CustomEditor from './components/customEditor'

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  inputComponent: CustomEditor,
  of: [
    {type: 'block'},
    {type: 'code'}
  ]
}

Now we want to deal with the incoming HTML. In order to convert HTML to Portable Text we need a library called blockTools. It's already a dependency in the Studio so you don't need to install it. In the paste handler we'll look for HTML content on the clipboard and if there is a code type in the schema. If these criteria is met, we'll pass the HTML content into the blocktools method called htmlToBlocks. This method lets you pass in rules for how to deserialize HTML elements.

import React from 'react'
import { BlockEditor } from 'part:@sanity/form-builder'
import blockTools from '@sanity/block-tools'

export default class CommentBlockEditor extends React.PureComponent {
  render () {
    return (
      <div>
        <BlockEditor {...this.props} onPaste={handlePaste} />
      </div>
    )
  }
}

function handlePaste (input) {
  const { event, type, path } = input
  const html = event.clipboardData.getData('text/html')
  // check if schema has the code type
  const hasCodeType = type.of.map(({ name }) => name).includes('code')
  if (!hasCodeType) {
    console.log(
      'Run `sanity install @sanity/code-input, and add `type: "code"` to your schema.'
    )
  }
  if (html && hasCodeType) {
    const blocks = blockTools.htmlToBlocks(html, type, {
      rules: [
        {
          deserialize (el, next, block) {
            /**
             *  `el` and `next` is DOM Elements
             * learn all about them:
             * https://developer.mozilla.org/en-US/docs/Web/API/Element
             **/

            if (
              !el ||
              !el.children ||
              (el.tagName && el.tagName.toLowerCase() !== 'pre')
            ) {
              return undefined
            }
            const code = el.children[0]
            const childNodes =
              code && code.tagName.toLowerCase() === 'code'
                ? code.childNodes
                : el.childNodes
            let text = ''
            childNodes.forEach(node => {
              text += node.textContent
            })
            /**
             * Return this as an own block (via block helper function),
             * instead of appending it to a default block's children
             */
            return block({
              _type: 'code',
              code: text
            })
          }
        }
      ]
    })
    // return an insert patch
    return { insert: blocks, path }
  }
  return undefined
}

Validation of annotations

You can also add content validation to annotations as with other content types. Warnings will appear in the margin, and in the document with a pointer that activates the annotation modal for the editor. Validations can be useful to help editors structure the content correctly. It's generally considered a good idea to involve editors in creating validatons and to test the warning messages so that they are helpful.

Let's say that you are using the same content for multiple websites.Then it's especially important that interal linking is done by using an annotation with a reference input. Both to prevent accidental deletion of content that is linked to, but also to be able to resolve internal links in the frontend project. You can create a simple validation that takes care of this:

export default {
  name: 'content',
  title: 'Content',
  type: 'array',
  of: [
    {
      type: 'block',
      validation: Rule => Rule.regex(/.*damnation.*/gi, { name: 'profanity' }),
      marks: {
        annotations: [
          {
            name: 'link',
            type: 'object',
            title: 'link',
            fields: [
              {
                name: 'url',
                type: 'url',
                validation: Rule =>
                  Rule.regex(
                    /https:\/\/(www\.|)(portabletext\.org|sanity\.io)\/.*/gi,
                    {
                      name: 'internal url',
                      invert: true
                    }
                  ).warning(
                    `This is not an external link. Consider using internal links instead.`
                  )
              }
            ]
          },
          {
            name: 'internalLink',
            type: 'object',
            title: 'Internal link',
            fields: [
              {
                name: 'reference',
                type: 'reference',
                to: [
                  { type: 'post' }
                  // other types you may want to link to
                ]
              }
            ]
          }
        ]
      }
    }
  ]
}

The regular expression /https:\/\/(www\.|)(portabletext\.org|sanity\.io)\/.*/i triggers on all URLs that matches all the variatons of either portabletext.org or sanity.io with some sub-paths (because we want to allow linking to the root domain).

A validation warning for the link annotation in the editor.

Margin Markers

Documentation coming soon…

Margin Actions

Documentation coming soon…

Previous: ConfigurationNext: Previewing