Type something to search...
Adding Code Blocks to Sanity Content

Adding Code Blocks to Sanity Content

To add code blocks to Sanity content, install the official @sanity/code-input plugin, add codeInput() to the plugins array in sanity.config.ts, and use the new code type either as a field or as a member of your Portable Text array. Editors get a proper code editor with syntax highlighting, a language picker, optional filenames and clickable line numbers for highlighting lines. Sanity stores each block as an object with code, language, filename and highlightedLines, and you render it on the front end with any syntax highlighter you like, such as Shiki or Prism.

Technical blogs, documentation sites and developer portfolios all need code in their content. A plain text field loses indentation hints and language information, and inline code formatting doesn't work for multi-line snippets. This guide shows how to install and configure the code input plugin, choose which languages editors can pick, add it to Portable Text, query it with GROQ and render highlighted, accessible code blocks in a Next.js App Router project.

Inline Code vs Code Blocks

Sanity supports two kinds of code formatting, and you'll usually want both:

KindHow you get itStored asUse for
Inline codeThe code decorator on text blocksA code mark on a spanShort references like a function name in a sentence
Code blockThe code type from @sanity/code-inputA code object in the arrayMulti-line snippets, commands and config files

The inline code decorator is included in Portable Text's default decorators. If you've customised marks.decorators, make sure it's still in the list. The rest of this article focuses on code blocks.

Step 1: Install the Plugin

From your Studio project folder:

npm install @sanity/code-input

Then register it in your Studio configuration:

// sanity.config.ts
import { defineConfig } from 'sanity';
import { structureTool } from 'sanity/structure';
import { visionTool } from '@sanity/vision';
import { codeInput } from '@sanity/code-input';
import { schemaTypes } from './schemaTypes';

export default defineConfig({
  name: 'default',
  title: 'My Blog',
  projectId: 'your-project-id',
  dataset: 'production',
  plugins: [structureTool(), visionTool(), codeInput()],
  schema: {
    types: schemaTypes,
  },
});

The plugin registers a schema type called code, so you don't define it yourself. If you're unsure where these files live, see Sanity project structure explained for beginners.

Step 2: Use the Code Type as a Field

The quickest way to try it is as a standalone field, for example on a snippet document:

// schemaTypes/snippet.ts
import { defineField, defineType } from 'sanity';

export const snippet = defineType({
  name: 'snippet',
  title: 'Snippet',
  type: 'document',
  fields: [
    defineField({ name: 'title', type: 'string' }),
    defineField({
      name: 'example',
      title: 'Example',
      type: 'code',
      options: {
        language: 'typescript',
        languageAlternatives: [
          { title: 'TypeScript', value: 'typescript' },
          { title: 'JavaScript', value: 'javascript' },
          { title: 'GROQ', value: 'groq' },
          { title: 'Shell', value: 'sh' },
          { title: 'JSON', value: 'json' },
        ],
        withFilename: true,
      },
    }),
  ],
});

The options work like this:

  • language: the default language for new code blocks.
  • languageAlternatives: the languages editors can choose from. Limiting the list keeps the picker short and your front-end highlighter's language set predictable.
  • withFilename: shows an extra input for a filename, which is great for tutorials that say which file a snippet belongs in.
  • disableFullscreen: hides the button that expands the editor to fill the document pane, which is otherwise available.

Step 3: Add Code Blocks to Portable Text

For articles, add code to your Portable Text array so editors can insert snippets between paragraphs:

// schemaTypes/blockContent.ts
import { defineArrayMember, defineType } from 'sanity';

export const blockContent = defineType({
  name: 'blockContent',
  title: 'Block Content',
  type: 'array',
  of: [
    defineArrayMember({
      type: 'block',
      marks: {
        decorators: [
          { title: 'Strong', value: 'strong' },
          { title: 'Emphasis', value: 'em' },
          { title: 'Code', value: 'code' },
        ],
      },
    }),
    defineArrayMember({
      type: 'code',
      options: {
        languageAlternatives: [
          { title: 'TypeScript', value: 'typescript' },
          { title: 'TSX', value: 'tsx' },
          { title: 'JavaScript', value: 'javascript' },
          { title: 'GROQ', value: 'groq' },
          { title: 'Shell', value: 'sh' },
          { title: 'JSON', value: 'json' },
          { title: 'CSS', value: 'css' },
        ],
        withFilename: true,
      },
    }),
  ],
});

The insert menu in the editor now offers Code. The general pattern for adding non-text types is covered in adding custom blocks to Sanity Portable Text.

What Gets Stored

Each code block is an object in the array:

{
  "_type": "code",
  "_key": "c4e1",
  "language": "typescript",
  "filename": "src/sanity/client.ts",
  "code": "import { createClient } from 'next-sanity';\n\nexport const client = createClient({ /* ... */ });",
  "highlightedLines": [1]
}

In the editor, clicking a line number toggles it in highlightedLines. The filename property appears only when withFilename is enabled. Note that the property holding the source is code, the same name as the type, so in your renderer the snippet is value.code.

Step 4: Supported and Custom Languages

The plugin highlights a default set of languages in the editor, including JavaScript, JSX, TypeScript, TSX, GROQ, JSON, HTML, CSS, SCSS, Sass, Markdown, PHP, Python, Ruby, Java, C#, Go, SQL, MySQL, XML, YAML, shell and plain text. Some stored values differ from what you might expect, such as golang for Go and sh for shell, which matters when you map them to a front-end highlighter.

If a language is close enough to a supported one, reuse its highlighting with mode:

languageAlternatives: [
  { title: 'Bash', value: 'bash', mode: 'sh' },
  { title: 'Astro', value: 'astro', mode: 'html' },
],

To add real support for another language, pass codeModes to the plugin with a loader for a CodeMirror 6 language package:

// sanity.config.ts
import { codeInput } from '@sanity/code-input';

codeInput({
  codeModes: [
    {
      name: 'rust',
      loader: () =>
        import('@codemirror/legacy-modes/mode/rust').then(({ rust }) =>
          import('@codemirror/language').then(({ StreamLanguage }) =>
            StreamLanguage.define(rust),
          ),
        ),
    },
  ],
}),

Then add { title: 'Rust', value: 'rust' } to languageAlternatives. The plugin already depends on @codemirror/legacy-modes and @codemirror/language, so legacy modes like this need no extra install. Official CodeMirror 6 packages such as @codemirror/lang-angular do need installing separately.

Step 5: Query Code Blocks With GROQ

Code blocks are plain fields, so a normal query returns them:

*[_type == "post" && slug.current == $slug][0]{
  title,
  body
}

Because the data is structured, you can also query code directly. This lists the languages used in each post, which is handy for loading only the highlighter grammars you need:

*[_type == "post"]{
  title,
  "languages": array::unique(body[_type == "code"].language)
}

And this finds posts with shell commands:

*[_type == "post" && "sh" in body[_type == "code"].language]{ title }

For more on array filters like these, see how to query nested arrays in GROQ.

Step 6: Render Highlighted Code in React

Sanity doesn't dictate a highlighter. Shiki is a good fit for the App Router because it highlights on the server and sends plain HTML, with no highlighting JavaScript in the browser:

npm install shiki

Create a Server Component that maps the plugin's language values to Shiki's and applies line highlights:

// src/components/CodeBlock.tsx
import { bundledLanguages, codeToHtml, type BundledLanguage } from 'shiki';

const LANGUAGE_MAP: Record<string, string> = {
  golang: 'go',
  sh: 'bash',
  batchfile: 'bat',
  mysql: 'sql',
};

function resolveLanguage(language?: string): BundledLanguage | 'text' {
  const lang = LANGUAGE_MAP[language ?? ''] ?? language ?? 'text';
  return lang in bundledLanguages ? (lang as BundledLanguage) : 'text';
}

type Props = {
  code?: string;
  language?: string;
  filename?: string;
  highlightedLines?: number[];
};

export async function CodeBlock({ code, language, filename, highlightedLines = [] }: Props) {
  if (!code) return null;

  const html = await codeToHtml(code, {
    lang: resolveLanguage(language),
    theme: 'github-dark',
    transformers: [
      {
        line(node, line) {
          if (highlightedLines.includes(line)) this.addClassToHast(node, 'highlighted');
        },
      },
    ],
  });

  return (
    <figure className="my-6">
      {filename && <figcaption className="font-mono text-sm">{filename}</figcaption>}
      <div className="overflow-x-auto text-sm" dangerouslySetInnerHTML={{ __html: html }} />
    </figure>
  );
}

Then map the code type in your Portable Text components:

// src/components/portable-text-components.tsx
import type { PortableTextComponents } from '@portabletext/react';
import { CodeBlock } from './CodeBlock';

export const components: PortableTextComponents = {
  types: {
    code: ({ value }) => <CodeBlock {...value} />,
  },
  marks: {
    code: ({ children }) => <code className="rounded bg-gray-100 px-1">{children}</code>,
  },
};

A few points about this approach:

  • Server only: CodeBlock is an async Server Component, so render your Portable Text in a Server Component, which is the default in the App Router.
  • Safe HTML: Shiki escapes the source code, so injecting its output is safe. Never inject raw value.code as HTML.
  • Unknown languages fall back: GROQ isn't in Shiki's bundled set, so it renders as plain text unless you add a custom grammar. The fallback means a new language in Studio never breaks the page.
  • Style highlighted lines: add CSS for .highlighted, such as a subtle background colour across the full line.
  • Both meanings of code: the types.code component handles code blocks, and marks.code handles the inline decorator. They're separate keys, so the shared name doesn't clash.

If you need highlighting in a client component, Prism-based libraries and react-refractor work too, as shown in the plugin's README. The rest of the renderer setup is covered in how to render Portable Text in React.

Adding a Copy Button

A copy button needs browser APIs, so make it a small client component and pass it the raw code:

// src/components/CopyButton.tsx
'use client';

import { useState } from 'react';

export function CopyButton({ code }: { code: string }) {
  const [copied, setCopied] = useState(false);

  return (
    <button
      type="button"
      onClick={async () => {
        await navigator.clipboard.writeText(code);
        setCopied(true);
        setTimeout(() => setCopied(false), 2000);
      }}
      className="text-xs"
    >
      {copied ? 'Copied' : 'Copy'}
    </button>
  );
}

Render <CopyButton code={code} /> inside the figure in CodeBlock. The Server Component does the highlighting and only the button ships JavaScript.

Tips for Code Content

  • Limit languages to what you render: if your highlighter doesn't support a language, don't offer it in Studio.
  • Use filenames in tutorials: readers copy code into the right file far more reliably when it's labelled.
  • Prefer code blocks over images of code: they're searchable, accessible and copyable.
  • Keep snippets short: long files are hard to read in an article. Link to a repository for full examples.
  • Watch indentation when importing: if you migrate content from HTML, check that pre elements became code blocks with whitespace intact. How to convert HTML to Portable Text covers this.

FAQ: Code Blocks in Sanity

Install @sanity/code-input, add codeInput() to the plugins array in sanity.config.ts, and use type: 'code' as a field or as a member of your Portable Text array. Studio then shows a code editor with a language picker.

Set options.languageAlternatives on the code field to an array of objects with a title and value. Use options.language to set the default language for new blocks.

Click the line numbers in the Studio code editor. The selected line numbers are stored in highlightedLines, and your front-end highlighter can add a class or marker to those lines.

Yes. Set options.withFilename to true on the code field. Editors get an extra filename input, stored as filename, which you can render as a caption above the snippet.

Any of them work, because Sanity stores plain text and a language name. Shiki suits Server Components in the Next.js App Router, while Prism-based libraries such as react-refractor work in client components.

The code decorator formats a few words inside a paragraph as inline code. The code type from @sanity/code-input is a separate block for multi-line snippets with a language, filename and highlighted lines.


Conclusion

Adding code blocks to Sanity takes one plugin and a few lines of schema. Install @sanity/code-input, register codeInput() in your config, and add the code type to your Portable Text array with a curated list of languages and filenames enabled. Editors get a real code editor, and you get structured data with the source, language and highlighted lines.

On the front end, pick a highlighter that suits your rendering model, map the plugin's language values to it, and fall back to plain text for anything unsupported. With Shiki in a Server Component and a small client-side copy button, your code blocks are fast, accessible and easy for readers to use.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper