Highlighted code blocks with Sanity.io and Next.js
- nextjs
- sanity
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
- We already know how to install and use Sanity Studio (it is pretty straightforward).
- We already know how to install Next.js and connect it to the Sanity api. In the case of uncertainty, there is a great tutorial on the company blog.
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:
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 .