← Overview

SBBy · · 3 min read

A real code editor for Storyblok, built for my own blog

Code, language, file name and highlighted lines in one field, instead of a textarea and a dropdown

TL;DR A Storyblok field with a CodeMirror editor, language picker, file name, highlighted lines and fullscreen. Copy the prompt and build it yourself.

Code in a textarea is painful. No highlighting, Tab jumps out of the field, and the language lives in a second field that editors forget to set. This field puts a real code editor into Storyblok, with the language, a file name and highlighted lines in the same value.

What it's for

This is the first of these fields that I built for myself. My blog, d12r.io, has a code_block component, and until now that was a plain textarea plus a separate language field. Every post with code meant typing in a box that eats your indentation. I asked the same session that had written the generation guide that night to build it, and it handed the job to a sub-agent working from that guide. My only instruction on the design: let's not restrict ourselves, breaking the old shape is fine. The new value bundles everything a blog code block renders: { code, language, filename, highlight }. The field still reads the old plain strings, so existing posts keep working, and the migration happens on the next edit.

Inside, it's CodeMirror 6: highlighting for twelve languages, line numbers, bracket matching, undo and a wrap toggle. The editor grows with the code up to maxLines and then scrolls. For anything longer, there's a fullscreen view in Storyblok's modal.

What you get

Inline: TypeScript, a file name, and "3, 1" stored as "1,3" with both lines marked.The inline editor with TypeScript highlighting, file name src/greet.ts and lines 1 and 3 highlighted in yellow
Fullscreen for longer snippets, with a visible Close button.The fullscreen editor in Storyblok's modal with a Close button, toolbar, large editor and keyboard help
At narrow width the toolbar wraps, and nothing overflows.The field at about 330 px: the toolbar wraps onto two rows

The stored value:

json
{
  "code": "export function greet(name: string): string {\n  return `Hello, ${name}!`\n}",
  "language": "typescript",
  "filename": "src/greet.ts",
  "highlight": "1,3"
}
OptionDefaultWhat it does
defaultLanguageplaintextLanguage for new blocks and old plain strings. Aliases like ts work.
allowedLanguagesallComma-separated languages offered, in this order.
tabSize2Spaces per indent level (1–8).
maxLines20Inline height in lines before it scrolls (6–200).
placeholderPaste or type code…Placeholder of the empty editor.

Using it in the frontend

typescript
import { codeToHtml } from 'shiki'

const block = readCodeBlock(blok.code) // accepts the object or an old plain string
const lines = highlightedLines(block.highlight) // "1,3-5" → Set {1, 3, 4, 5}
const html = await codeToHtml(block.code, {
  lang: block.language,
  theme: 'github-light',
  transformers: [{ line(node, line) { if (lines.has(line)) this.addClassToHast(node, 'highlighted') } }],
})

The language ids are valid Shiki, Prism and highlight.js names. Both helpers are in the plugin's PLUGIN.md.

What I learned building it

  • A real editor has a real price. The bundle is 534 kB raw, 174 kB gzipped, about 3.4× the other fields, mostly CodeMirror. Good to know: field plugins deploy as one CommonJS file, so everything loads up front, with no lazy loading. For a field I use in every technical post, that's a trade-off I accept.
  • Escape is a shared key. The browser test caught it: pressing Escape inside the fullscreen editor closed the modal. The @storyblok/field-plugin library closes the modal, without saving, on any Escape that reaches document. The editor now stops Escape inside CodeMirror and saves pending edits before the modal closes.
  • Debounce, then flush. Edits are saved 300 ms after typing stops, but also right away on blur, on a language change and when switching to or from fullscreen. The browser test types a line and hits Close within the debounce window, and the line is still stored.
  • Keyboard users need a way out. Tab indents, so it can't also move focus. CodeMirror's answer is Esc, then Tab, to leave the editor. Ctrl/⌘+Shift+Enter opens and closes fullscreen from anywhere.
  • Good to know: field type names are global. Deploying code-editor returned DUPLICATED_NAME: another Storyblok user already had that name. It went live as d12r-code-editor, and the deploy script now takes --name. Tip: prefix your field names.

It has 23 unit tests and a browser walkthrough that passed 16 of 16 steps. The full story is in the article about the original experiment. The other fields: brand color picker, product picker, author picker, recipe picker.