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.
Package info
github.com/justinholtweb/craft-kody
Type:craft-plugin
pkg:composer/justinholtweb/craft-kody
Requires
- php: ^8.2
- ext-json: *
- ext-mbstring: *
- craftcms/cms: ^5.3.0
- nystudio107/craft-code-editor: ^1.0.20
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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.