👀 Our most exciting product launch yet 🚀 Join us May 8th for Sanity Connect

Anchored Headings for Portable Text

By Knut Melvær

How to automatically generate linked headings with a custom serializer

serializers.js

import PortableText from "@sanity/block-content-to-react";

export const serializers = {
  type: {
    block: (props) => {
      const { node, children } = props
      const { style, _key } = node

      if (/^h\d/.test(style)) {
        const HeadingTag = style;
        // Even though HTML5 allows id to start with a digit, we append it with a letter to avoid various JS methods to act up and make problems
        const headingId = `h${_key}`;
        return (
          <HeadingTag id={headingId}>
            <a 
              href={`#${headingId}`}
              aria-hidden="true"
              tabIndex={-1}
            >#</a>
            <span>{children}</span>
          </HeadingTag>
          )
      }
      // ... you can put in other overrides here
      
      // or return the default ones 👇
      return PortableText.defaultSerializers.types.block(props)
    }
  },
  // more custom types here…
}

This is a minimal example of how you can add extra attributes and markup to headings from Portable Text in React. You'd approach this similarly in other frameworks. Here we have a custom serializer for the block type. It looks after a style property that contains the pattern with the letter h and a digit (so h1, h2, h3, etc).

Then we generate an id from the _key that comes from the block data. This _key will be stable as long as the block (that is, the heading) exists. Alternatively, you can generate a readable id from the heading text data in children.

This markup put's a linked # inside of the heading, but you can of course do it however you want and add styling to it.

Contributor

Other schemas by author