Adding Code Snippets in Sanity Studio v3

Wednesday, January 4th 2023

Welcome to the new year, ✨ 2023 ✨. Just before 2022 ended, Sanity took Sanity Studio v3 out of beta. It was perfect timing for me, as I was working on the launch of this very site.

I used the new Sanity CLI to generate my site's CMS as usual, with a few new steps from v3. The CLI was able to pre-generate a lot of the common schema you'd find in a blog thanks to Sanity's template - Post, Author, and Category were mostly ready to go.

One of the things that is a must-have in a coding blog is, well, the ability to display code in a blog post. This was something I needed to create that wasn't added out of the box. Depending on how you look at it, it is the joy and the peril of having a highly customizable headless CMS.

As someone that only had rudimentary Sanity knowledge and which was only limited Studio v2, it proved to be more difficult than I thought.

Although I find Sanity's documentation great 90% of the time, I find that their how-tos can assume a lot about the knowledge you hold going into it. Sometimes, they can only get you part of the way to a solution if you're new. Plus, as with any software overhaul and launch, Google and their site have a spattering of deprecated docs. Some of the new ways of doing things can be a bit hard to find.

So, let's put the pieces together to add the Code Input block to your Sanity v3 project's Block content schema.

Prerequisites

Feature Requirements

What we need to do to know that work is done.

  • Code Input Block: There must be a way that I can easily add raw code to my blog posts in Sanity Studio when writing, as a block in the WYSIWYG editor. In Sanity terms, this is known as the Block type schema.
  • Syntax Highlighting w/ Language Support: This is a readability essential to me. I need syntax highlighting to be automatically applied according to the language I specify. At minimum I know I will need JavaScript, JSX, HTML, CSS, Liquid, and SASS support. Bonus points for being able to pick the theme, lol.
  • Maintainable & customizable: The way I write this new feature must fit in with my project structure, offer custom styling, and full control over how it renders on the front end.

Now to break those down, while thinking in code:

// Get the right tools for the job.
// A Sanity plugin for a Code Input block.
// A package to render the data (portable text) from that block.
// A package to render the syntax highlighting.

// Register the new Code Input block inside our Block Content schema and define options.

// Create the React component that our blog post raw code will render inside of.
// Configure syntax highlighting.

// Add that new component to the PortableText component that is rendering our blog post.

the Tools for the Job

We'll need 3 external tools to help us with this as outlined above.

The plugin for the Code Input block we'll be using is @sanity/code-input. It's official and quite fantastic. We'll also be referencing that readme a lot.

The best package for rendering portable text in React is @portabletext/react.

Overall, this process is straightforward. But I had a lot trouble with rendering the Code Input in NextJS, and it took me much too long to realize I was using the wrong npm package that was commonly used in V2. The bottom line is make sure your project is using @portabletext/react to render any and all portable text on your blog post.

The context for above is that I started building my new site and learning Sanity in general before V3 was released, but after V2 had been out for quite some time. I found this plugin page on Sanity's site when it came time to render my posts and began to use it. It is called react-portable-text and it worked for my needs in V2.

The correct package to use for rendering all portable text in V3 is @portabletext/react. I mistakenly thought they were the same thing, as they have near identical ways of calling the rendering PortableText component and passing it the same components prop (more on this later). I was able to swap packages once I realized.

The package I'll be using for syntax highlighting is refractor, since it was referenced in the @sanity/code-input readme example. If you have another you like to use, go for it - just make sure it's compatible with @sanity/code-input.

Registering the Code Input inside our Content Block Schema

Navigate to your project's Sanity folder and install the Code Input plugin.

// my-project/my-sanity-folder

npm i @sanity/code-input

Now to register that newly installed plugin inside our Sanity config, in the plugins array in the defineConfig() function. This will expose the plugin to our schema so it can use it.

// sanity.config.js

//....
import { codeInput } from "@sanity/code-input";

export default defineConfig({
  // ....
  dataset: 'production',
  plugins: [deskTool(), visionTool(), codeInput()],

  // ....
  },
})

Let's open up our blog post schema. Mine was conveniently pre-generated by Sanity CLI and looks something like this:

// my-project/my-sanity-folder/schemas/blockContent.js

import { defineType, defineArrayMember } from 'sanity'

export default defineType({
  title: 'Block Content',
  name: 'blockContent',
  type: 'array',
  of: [
    defineArrayMember({
      title: 'Block',
      //....
    }),

    defineArrayMember({
      type: 'image',
      options: { hotspot: true },
    }),
  ],
})

Below the second defineArrayMember() function, we're going to add another call of it to register the Code Input component in the block editor.

// my-project/my-sanity-folder/schemas/blockContent.js

import { defineType, defineArrayMember } from 'sanity'

export default defineType({
//....

    defineArrayMember({
      type: 'image',
      options: { hotspot: true },
    }),
    defineArrayMember({
      type: 'code',
      options: {
        language: 'JavaScript',
        languageAlternatives: [
          { title: 'HTML', value: 'html' },
          { title: 'CSS', value: 'css' },
          { title: 'JavaScript', value: 'js' },
          { title: 'JSX', value: 'jsx' },
          { title: 'SASS', value: 'sass' },
          { title: 'Liquid', value: 'liquid' },
          { title: 'Bash', value: 'bash' },
        ]
      }
    }),
  ],
})

What this does is register a new plugin whose type is code. I've included some options while following the documentation on the Code Inputs page. language just defines what default language will be selected when you add a new code input block. The languageAlternatives is important - this is where we are defining which languages we want to support. Add in whichever languages you'd like to, which are supported by your syntax highlighter.

You can find a list of languages refractor supports here.

At this point, run npm run dev in your Sanity folder and poke around. Navigate to a post in Sanity Studio and your code block option should show up! Fantastic.

Now, let's work on rendering our post, along with the code inputs, in our NextJS front end.

Rendering Code Inputs on the Page

Now, this is where your process may slightly deviate from mine, especially if you aren't using NextJS. That's okay. Our main goal is taking the portable text data of our raw code content that Sanity is generating, and control how it renders to the page.

I am using dynamic routing in NextJS to create my post pages from a file named [slug].js.

Here's a basic example of how the blog post content is currently being rendered on the page:

import { PortableText } from '@portabletext/react'

const Post = ({ post }) => {
  return (
    <>
      <header>
        <h1>{post?.title}</h1>
      </header>
      <section>
        <PortableText value={post.body} />
      </section>
    </>
  )
}

Here, we're using the PortableText component imported from the package mentioned earlier. This code is assuming you've already passed in your post data from Sanity as props.

You may notice your code either is rendering with no styling as text, or possibly not rendering at all. First, we must create a component that will handle the rendering of our code. Second, we must utilize the PortableText components functionality.

Creating the Code Component

This is the component that will render once PortableText finds a code input in your content.

In whichever folder you have that holds your front end components, create Code.js. Below is what mine looks like. Let's go through it piece by piece.

// components/Code.js

import React from 'react'
import Refractor from 'react-refractor'
import markdown from 'refractor/lang/markdown.js'
import jsx from 'refractor/lang/jsx.js'
import liquid from 'refractor/lang/liquid.js'
import css from 'refractor/lang/css.js'
import js from 'refractor/lang/javascript.js'
import sass from 'refractor/lang/sass.js'
import bash from 'refractor/lang/bash.js'

const languages = [jsx, markdown, liquid, js, css, sass, bash]

languages.forEach((language) => {
  Refractor.registerLanguage(language)
})

export function Code({ language, code, highlightedLines }) {
  return (
    <Refractor language={language} value={code} markers={highlightedLines} />
  )
}

I've imported all of the files of common languages I use at the top of this file and the main Refractor component from Refractor. The imported languages are the same languages we registered in my-project/my-sanity-folder/schemas/blockContent.js.

Then I created an array languages to hold all of these variables, which will be iterated over and registered with Refractor's registerLanguage() function.

Then you'll pass the Refractor props, which are actually the content of your Code input. For more context, view the tutorial in the Code Inputs readme here.

Rendering your New Code Component

Alright, now it's time to go back to our Post page component and let the PortableText rendering component know we want to use Code for code input blocks.

First, import your Code component at the top of the file.

Then, let's create a constant called components. It will be an object accepting a key types with a value of another object, which will be the name of the HTML element you'd like to control. In this case, that will be code. The value will be a function returning your component with its needed props passed in from Sanity data.

You can read more about the components prop here.

// pages/post/[slug].js

import { PortableText } from '@portabletext/react'
import { Code } from '/./components/Code'

const components = {
  types: {
    code: (props) => {
      return (
        <Code
          language={props.value.language}
          code={props.value.code}
          highlightedLines={props.value.highlightedLines}
        />
      )
    },
  },
}

const Post = ({ post }) => {
  return (
    <>
      <header>
        <h1>{post?.title}</h1>
      </header>
      <section>
        <PortableText value={post.body} components={components} />
      </section>
    </>
  )
}

Getting the Pretty Theme

Huzzah! Your code is rendering with indenting and all of the great formatting we're used to. Except, your favorite IDE themed syntax highlighting is missing even though we've passed Refractor the highlightedLines prop.

That's because we have to add the theme CSS! Refractor is built with Prism, so all Prism themes will apply nicely.

You can find a list of Prism themes here, or generate your own.

Once you get the raw CSS, you'll have to add it in whichever way you deem fit for your project. I've added mine via Sass and Tailwind. I created a Sass partial _text-editor.scss and pasted the code in there, along with some tweaks for font sizing, font family, and other things here and there.

Here's a snippet I add to all of my Prism themes that I've found helpful.

// your Prism theme CSS
// styles/customizations/_text-editor.scss

code,
pre span {
  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace
  font-size: 14px
  // Whatever font size you want and other updates, accordingly
}

Once the CSS is added to your site, that's it! ✨ Bam. You should have a perfectly working code snippet component. Go forth, and write about code.