Introducing the new Editor for Portable Text (ssr)

Written by Even Eidsten Westvang

Missing Image!

Central to Sanity is the ability to work with content with all the advantages of treating it as data. Non-obviously perhaps, that makes us pretty obsessed with text. We are typing words into Sanity everyday when we're writing blog posts, documentation, and developing new features.

Treating text as data is super hard. For authors, it needs to look like a modern text editor. This while developers design flexible schemas that combine text with embedded objects and where the end result is data that you can subject to queries and joins.

In our latest release, we ship a new editor for Portable Text. On the surface it may not seem that much has happened, but much work has happened on its underpinnings and machinery. We're super proud of our team that has spent hours developing, testing, finding edge cases (oh, the edge cases!), and obsessing over details.

Run this line in your command line to upgrade and get the new editor in your Sanity project:

npm i -g @sanity/cli && sanity upgrade

What we now have is an editor that works as expected out of the box, but gives you extensive possibilities for configuration and customization. Most of it is done in the simple JavaScript configuration you might already know, while previews, toolbar icons, and block actions can be customized with React components. So let's dive into some of the new things!

Have it your own style

The editor for Portable Text comes with a set of styles that translate well to those you'll find in HTML. You 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.

Editor with custom title style

Total render control!

One of the big advantages of Portable Text is the ability to add marks, that is, to annotate inline text with simple keys or complex typed content structures. We have now made it easy to add your own icons via the schema configuration. You can pass in simple strings, or React components (yeah, you can theoretically render a video in your toolbar – but you probably shouldn't). You can also control how marks in the inline text are rendered, for example, if you want to add highlight functionality.

import { HighlightIcon, Highlight } from './decorators/Highlight'

marks: {
  decorators: [
    {title: 'Strong', value: 'strong'},
    {title: 'Emphasis', value: 'em'},
    {title: 'Code', value: 'code'},
    {
      title: 'Highlight',
      value: 'highlight',
      blockEditor: {
        icon: HighlightIcon,
        render: Highlight
      }
    }
  ],
  ...
}

And in ./decorators/Highlight.js:

import React, {Fragment} from 'react'
import PropTypes from 'prop-types'
import styles from './Highlight.css'


const HighlightIcon => {
  return <Fragment>🖍</Fragment>
}

const Highlight = props => {
  return <span className={styles.root}>{props.children}</span>
}

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

export default {
  Highlight,
  HighlightIcon
}

Tailored paste handling

Now, this is pretty cool. We left you hooks to override and tune how content should be pasted into the editor. The editor already knows to deal with a lot of weird edge cases from Word documents. And it deals copy-pasted text from Google Docs too. But perhaps you have a legacy system with some special markup, or want to make it easy for editors to be able to copy paste content from the old website into the editor and have some parts be transformed into some custom content blocks.

Pasting markdown into the editor for portable text

We also updated the tool for working programmatically with Portable Text to make it easier to design customizations like this.

Validations and Actions

If you add validation rules to your markers, the warnings will now be highlighted in the margin, and your editors can activate the annotation modal directly from the validation menu. Perhaps you want to make sure that all images have got an alternative text, or warn editors if they use the http:// and not https:// URL in a link.

With Actions you can pass the content of a block to a React component that renders in the margin. This can be useful if you want to create workflows that triggers on certain strings or content types. You can, for example, make a thing that looks at your text for product names, and makes it easy to generate an internal reference when they appear.

Open Sourced Specification of Portable Text

It has always been a core idea of Sanity to have deeply typed content. Rich text is too often just left in the hands of markdown or HTML. While they're two perfectly good formats for what they're made for, they have no business as the storage format in a modern content backend.

We needed something that made it possible to have nested typed content structures, while being nimble for a WYSIWYG editor and that could be serialized into whatever markup or data structure you needed it for, without to much hassle. We wanted it to be JSON out of the box, and not leave the burden of transpiling XML on our users. That's how Portable Text was born. Since we find it mighty useful, we wanted to share it with the world. On www.portabletext.org you can find the specification and links to various tooling. We think it would have been super fun if someone decided to adapt it to their own use case.

Wrap it!

In order to add your own custom marker renderers, or actions, you have to pass those as props into the editor. It's fairly straight forward if you know some React:

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

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

A byproduct of this is of course that it's easy to wrap the editor in whatever you may desire. Be it real-time text statistics (you have access to the whole document in this.props) or perhaps be able to send the text to a text-to-speech API as we did with our SSML editor.

No more locking

With this upgrade, all fields in the Sanity Studio have support for real-time collaboration. You can sit on your desktop hammering away, while your co-worker fixes your spelling errors at the same time.

Example of real time editing

There's still a caveat – we still need to correctly handle the caret positions of users writing within the same paragraph. Annoyingly your caret position resets to the top of the block when other patches are applied to the same block. We'll be working more on collaboration in the new year and this is near the top of lists of things to improve.

The last large upgrade in an amazing year!

We hope you feel our enthusiasm for this rich text editor when you try it out. It seems like an appropriate occasion to note the passing of Evelyn Berezin, the creator of the first word processor, who sadly passed away at 93. We all owe Evelyn our gratitude for her efforts in paving the road and launching a revolution for all of us who friviously type on digital sheets.

This will be the last large feature upgrade in 2018. But since the calendar shows December 20th, we feel that it is perhaps reasonable. It has been an absolutely amazing year for us at Sanity HQ! We look forward to seeing you after the holidays and tell you some other exciting things we have been working on lately with some of our friends.