Search by

justinholtweb / craft-kody

A real code editor field for Craft CMS 5 — Monaco in the control panel, a snippet library for code worth reusing, and syntax highlighting on the front end without shipping a highlighter to the browser.

Maintainers

Package info

github.com/justinholtweb/craft-kody

Homepage

Type:craft-plugin

pkg:composer/justinholtweb/craft-kody

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-08-29 13:27 UTC

This package is auto-updated.

Last update: 2026-08-29 13:28:46 UTC


README

A code editor field for Craft CMS 5.

Craft has no code editor field, and hasn't since luwes/craft3-codemirror stopped being updated in August 2022. An ecosystem-wide search for a Craft 5 one returns nothing. So when an entry needs to hold a bit of Twig, a config file, an install command or a code sample, the choice today is a plain textarea — no colours, no bracket matching, tab characters that move focus out of the field — or a rich-text field that will quietly eat the code.

Kody is the field. It is also the two things that turn out to be inseparable from it: somewhere to keep code that gets used more than once, and a way to put code on a page that does not involve shipping a syntax highlighter to every visitor.

Free. No editions, no licence key, and no licensing code in the plugin.

What it does

A real editor in the control panel. Monaco — the editor out of VS Code — by way of nystudio107's CodeEditor library, which already solved the hard parts: bundling Monaco for a Craft asset bundle, Twig and Craft API autocomplete, sane defaults. Kody supplies the field: per-field language, height that follows the content, theme, line numbers, minimap, word wrap, syntax validation on save, and a language selector for fields where the author decides.

A snippet library. Some code belongs to an entry. Some code belongs to the site and appears on forty of them. Snippets are elements with a handle, so a template can ask for one by name and a rich-text field can embed one with {kody:install-command:render} — and correcting it once corrects it everywhere.

Highlighting that happens on the server. 46 of the 49 languages Kody knows are tokenised in PHP, cached by content, and rendered as ordinary HTML and CSS. The browser downloads no highlighter, runs no highlighting pass, and shows no flash of unhighlighted code. It works with JavaScript off, in view-source, and in an RSS reader.

Install

composer require justinholtweb/craft-kody
php craft plugin/install kody

Requires Craft 5.3+ and PHP 8.2+.

The field

Add a Code field to any field layout. Templates get a value object:

{{ entry.snippet.render() }}

That is the whole common case. render() takes overrides for anything the field or the plugin settings decided:

{{ entry.snippet.render({
    lineNumbers: true,
    highlightLines: '4, 7-9',
    filename: 'templates/news/index.twig',
    copyButton: true,
    maxHeight: '30rem',
}) }}

The rest of the value:

entry.snippet prints the source, escaped by Twig
.code the source as a string
.language a Language.id, .label, .extension()
.lines, .lineCount, .characterCount
.highlight() just the coloured source, no wrapper
.inline() a <code> for code in a sentence
.json, .isValidJson for fields holding JSON
{% if entry.snippet %} false when empty — count() is the line count

Options

Every one of these is set three times over — a plugin default, a per-field default, and a per-render override — so a site sets lineNumbers once and still turns it off for the one block where it reads badly.

Option
language override the stored language
lineNumbers, startLine numbers are CSS counters, so they are never copied
highlightLines '3', '1,4', '3-5', '3..5', or [3, 4]
wrap soft-wrap instead of scrolling sideways
maxHeight any CSS length; taller blocks scroll
copyButton the only JavaScript Kody puts on a page
filename, caption, showLanguage the caption bar
theme auto (follows the visitor), light, dark
tabSize
dedent strip the indentation every line shares
trim drop blank leading and trailing lines
class, id on the wrapping <figure>

Snippets

Kody → Snippets in the control panel, or from the command line:

php craft kody/snippets/import ./deploy.sh --handle=deploy
php craft kody/snippets/list

Then, from a template:

{{ kodySnippet('deploy') }}
{{ craft.kody.snippet('deploy').code.lineCount }}

{% for snippet in craft.kody.snippets({ language: 'twig' }).all() %}
    <h3>{{ snippet.title }}</h3>
    {{ snippet.render() }}
{% endfor %}

Or from a rich-text field, with no template change at all:

{kody:deploy:render}

A Snippets relation field puts the same library in front of authors as a picker.

Snippets are deliberately not localised. Code is code — an install command has no French version, and pretending it might would mean maintaining every snippet once per site.

Anywhere else

craft.kody works on loose strings too, not only on Kody fields:

{{ craft.kody.render(source, 'php', { filename: 'Plugin.php' }) }}
{{ source|kody('php') }}
{{ 'npm run build'|kodyInline('shell') }}

Languages

49 in the registry. All of them edit with Monaco's own colours; 46 are also highlighted on the front end, by grammars written for the fifteen that carry most of the weight (Twig, HTML, CSS, SCSS, Less, JavaScript, TypeScript, JSON, PHP, YAML, SQL, Markdown, GraphQL, shell, Python, INI, Dockerfile, diff) and by a keyword-driven generic grammar for the rest. Three — CSV, log output and plain text — are honestly not highlighted at all, because guessing would be worse.

php craft kody/languages/list
php craft kody/languages/check ./src/Plugin.php

check reports what the highlighter actually found in a file, which is how you notice a grammar has quietly stopped matching long before anybody notices a page looking flat.

Speed. A 9.5 KB, 288-line PHP file tokenises in 5.6 ms into 1,355 tokens; 23 KB takes 8.5 ms. Then it is cached by content hash, so the same snippet on a thousand pages is tokenised once. Anything over 256 KB is handed back as plain text rather than tokenised.

Styling

Kody registers one stylesheet, and the whole palette is custom properties on .kody:

.kody {
    --kody-bg: #16181d;
    --kody-str: #8ddb8c;
    --kody-kwd: #ff7b72;
}

Turn Include Kody's stylesheet off in the settings to write your own instead. The markup and class names do not change either way, and the copy button keeps working.

Token classes are k-com, k-str, k-num, k-kwd, k-bln, k-fun, k-cls, k-var, k-opr, k-pun, k-tag, k-atn, k-atv, k-sel, k-prp, k-del, k-flt, k-mta, k-esc, k-url, k-hdr, k-bld, k-ita, k-cod, k-dfa, k-dfr.

Running code as Twig

A Kody field can render its own contents as a Twig template:

{{ entry.snippet.eval({ products: products }) }}

It is off, and stays off until two switches are turned on: one in the plugin settings and one on the field. Code in a content field is written by whoever can edit that entry, and running it as Twig hands them everything Twig can reach. There are sites where that is exactly the point; this is how they say so, and config/kody.php is how they say it in a way only a deploy can change.

GraphQL

A Kody field resolves to a KodyCode with code, language, languageLabel, lineCount, characterCount, highlighted and html. A headless front end can take the raw source and highlight it itself, or take highlighted and not have to.

Config

Copy vendor/justinholtweb/craft-kody/src/config.php to config/kody.php for multi-environment settings in code.

Licence

The Craft License. See LICENSE.md. Kody is free: no editions, no licence key, and no licensing code in the plugin.