Highlighted code blocks with Sanity.io and Next.js

  • nextjs
  • sanity
posted on 3rd April 2021 ∙ by Hynek Svacha ∙ 8 min read

In this tutorial, we will create an input for code blocks in Sanity.io. Subsequently, we will create a component for displaying our code blocks in a Next.js app.

Prerequisites

How to integrate code input into Sanity 

Sanity.io does not come with a dedicated input for code blocks out of the box, but there is a nice official plugin available. Let’s install it:

sanity install @sanity/code-input

Once the plugin is installed, we’ll have to enable it in our portable text scheme. Let’s open a file where our portable text scheme is defined and add a code block definition (as highlighted below):

// blockContent.js

export default {
  title: "Block Content",
  name: "blockContent",
  type: "array",
  of: [
    {
      title: "Block",
      type: "block",
      // Styles let you set what your user can mark up blocks with. These
      // correspond with HTML tags, but you can set any title or value
      // you want and decide how you want to deal with it where you want to
      // use your content.
      styles: [
        { title: "Normal", value: "normal" },
        { title: "H1", value: "h1" },
        { title: "H2", value: "h2" },
        { title: "H3", value: "h3" },
        { title: "H4", value: "h4" },
        { title: "Quote", value: "blockquote" },
      ],
      lists: [{ title: "Bullet", value: "bullet" }],
      // Marks let you mark up inline text in the block editor.
      marks: {
        // Decorators usually describe a single property – e.g. a typographic
        // preference or highlighting by editors.
        decorators: [
          { title: "Strong", value: "strong" },
          { title: "Emphasis", value: "em" },
        ],
        // Annotations can be any object structure – e.g. a link or a footnote.
        annotations: [
          {
            title: "URL",
            name: "link",
            type: "object",
            fields: [
              {
                title: "URL",
                name: "href",
                type: "url",
              },
            ],
          },
        ],
      },
    },
    // You can add additional types here. Note that you can't use
    // primitive types such as 'string' and 'number' in the same array
    // as a block type.
    {
      type: "image",
      options: { hotspot: true },
    },
    {
      title: "Code block",
      name: "code",
      type: "code",
    },
  ],
};

If everything went well, we could now be able to add a block of code in our rich text editor:

Image of the Code Block option in Sanity block editor's collapsible menu
To highlight a line, simply click on it. Click again to un-highlight.

How to render our code blocks on the front end

When it comes to rendering our code blocks on the front end, we have basically two options.

  • To render the code blocks client-side (the “old school” way),
  • To prerender the code blocks. Using Next.js, this can mean either server-side rendering or (preferably) the new Static Site Generating option (available since v9.3).

In either case, we need to install another official module, block-content-to-react.

npm i @sanity/block-content-to-react

This module contains default serializers for portable text (like headings, paragraphs, etc.). Unfortunately, code blocks are not among the defaults, so we’ll need to create a dedicated serializer ourselves. The serializers will be used as a prop in our <BlockContent /> component (probably in a post template, but that exceeds the scope of this post).

// The portable text serializers
import BlockContent from "@sanity/block-content-to-react"

import CodeBlock from "./CodeBlock"

const serializers = {
  types: {
    code: (props) => {
      const { code, language } = props.node
      return <PrismjsSnippet language={language} code={code} />
    },
    // ...other serializers
  },
}

export default serializers

Option #1: Render on the client side

First, we gonna need to install the PrismJS module:

npm i prismjs

Once the module is installed, we need to create a React component, which will be responsible for rendering our code blocks.

The component would look like the one below. Since we are rendering client-side, we need to initialize a ref on the wrapper element first. Then, in the useEffect hook, when the DOM is available, we invoke the Prism.highlightAllUnder() method on that ref, which should make our code highlighted.

Note the third line. I am manually importing the file for JSX support. That’s not an optimal solution, because we need to manually add a module each time we want to use a new language (or add them all together, but that will significantly increase our bundle size). I would suggest taking a look at babel-plugin-prism module.

import React, { useEffect, useRef } from "react"
import Prism from "prismjs"
import "prismjs/components/prism-jsx"

const CodeBlock = ({ code, language }) => {
  const codeBlockRef = useRef()

  useEffect(() => {
    if (codeBlockRef.current) {
      Prism.highlightAllUnder(codeBlockRef.current)
    }
  }, []);

  return (
    <>
      <div ref={codeBlockRef}>
        <pre data-language={language} className={`language-${language}`}>
          <code>{code}</code>
        </pre>
      </div>
    </>
  );
};

export default CodeBlock

Option #2: Prerender

While server-side rendering or static generation is definitely possible using bare PrismJS, i’ve run into some issues with it. Therefore, I would recommend installing the library, which uses PrismJS under the hood but is enhanced to be used specifically with React. That library is prism-react-renderer from Formidable labs.

So let’s install it:

npm i prism-react-renderer

Once the module is installed, we need to create a code block component similar to the one above:

// if we use Next.js as frontend, no need to import React
import Highlight, { defaultProps } from "prism-react-renderer"

const CodeBlock = ({ code, language, highlightedLines }) => (
  <Highlight {...defaultProps} code={code} language={language}>
    {({ className, style, tokens, getLineProps, getTokenProps }) => (
      <pre className={className} style={style}>
        {tokens.map((line, i) => (
          <div {...getLineProps({ line, key: i })}>
            {line.map((token, key) => (
              <span {...getTokenProps({ token, key })} />
            ))}
          </div>
        ))}
      </pre>
    )}
  </Highlight>
)

export default CodeBlock

We also need to create a serializer. Since the props used by PrismJS and prism-react-renderer are the same, we can reuse the serializer (take a look at the serializers.js snippet above).

Bonus: line highlighting

One neat thing that might be useful is the ability to highlight selected lines.This is quite simple using client-side PrismJS, and with a little effort can be done using prism-react-renderer as well.

Either way, we need to enhance our serializer with highlightedLines prop:

// The portable text serializers
import BlockContent from "@sanity/block-content-to-react"

import CodeBlock from "./CodeBlock"

const serializers = {
  types: {
    code: (props) => {
      const { code, language, highlightedLines } = props.node
      return <PrismjsSnippet
        language={language}
        code={code}
        highlightedLines={highlightedLines}
      />
    },
    // ...other serializers
  },
}

export default serializers

PrismJS

To add line highlight support, import the corresponding module and stylesheet. Add highlightedLines to props. Use that prop as the data-line attribute value. Because PrismJS expects that prop to be a string (currently an array of numbers), we need to serialize it using Array.join() method:

import React, { useEffect, useRef } from "react"
import Prism from "prismjs"
import "prismjs/plugins/line-highlight/prism-line-highlight"
import "prismjs/plugins/line-highlight/prism-line-highlight.css"
import "prismjs/components/prism-jsx"

const CodeBlock = ({ code, language, highlightedLines }) => {
  const codeBlockRef = useRef()

  useEffect(() => {
    if (codeBlockRef.current) {
      Prism.highlightAllUnder(codeBlockRef.current)
    }
  }, [])

  return (
    <>
      <div ref={codeBlockRef}>
        <pre
          data-language={language}
          className={`language-${language}`}
          data-line={[props.node.highlightedLines].join()}
        >
          <code>{code}</code>
        </pre>
      </div>
    </>
  )
}

export default CodeBlock

prism-react-renderer

First, we need to create custom styles for a highlighted line (I am using CSS modules here, but feel free to choose your own preferred way to style it):

.line_highlight {
  background-color: rgb(53, 59, 69);
  display: block;
  margin-right: -1em;
  margin-left: -1em;
  padding-right: 1em;
  padding-left: 1em;
}

The highlightedLines prop comes from Sanity code input as an array of line numbers. The simplest way to use it would be to check on every line if the line number is included (e.g., using Array.includes() method), but this comes with O(n2) complexity 🤔. Because of this, I would recommend transforming the highlightedLines prop into a Set. Because Set itself has O(1) complexity, the result should be O(n), where n is a number of lines 👍.

When we encounter a line that is in our Set, we simply add a proper className to its props.

There is one little caveat: while the highlightedLines are counted from 0, the rendered lines are counted from 1. Hence the (i + 1):

import Highlight, { defaultProps } from "prism-react-renderer"
import nightOwl from "prism-react-renderer/themes/nightOwl"

import styles from "./HighlightedCode.module.css"

const HighlightedCode = ({ code, language, highlightedLines }) => {
  const linesToHighlight = new Set(highlightedLines)
  return (
    <Highlight {...defaultProps} code={code} language={language} theme={nightOwl}>
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <pre className={className} style={style}>
          {tokens.map((line, i) => {
            const lineProps = getLineProps({ line, key: i })
            return (
              <div
                key={i}
                {...lineProps}
                className={
                  linesToHighlight.has(i + 1)
                    ? `${lineProps.className} ${styles.line_highlight}`
                    : lineProps.className
                }
              >
                {line.map((token, key) => (
                  <span {...getTokenProps({ token, key })} />
                ))}
              </div>
            )
          })}
        </pre>
      )}
    </Highlight>
  )
}

Conclusion

I hope this article helps you create wonderful, colorful code snippets. Thank you for reading.

👍 Enjoy!


If you find anything in this post that should be improved (either factually or in language), feel free to edit it on Github .