If you’ve ever tried to set up Torchlight syntax highlighting in Statamic’s Bard field, you’ve probably hit a roadblock.
The official Torchlight docs show how to extend Statamic’s Markdown parser, which works beautifully for Markdown
fields. But if you’re using Bard, you’ll notice—it won’t work out of the box.
I wanted a solution that:
Works directly inside Bard.
Doesn’t require creating a custom Bard set.
Supports a copy button and language tag.
After some digging, I found a solid approach using Bard Mutator by Jack Sleight. This addon is fantastic if you want to add niceties to Bard (I also use it on this blog for linkable headings). With it, we can hook into Bard’s output and inject Torchlight highlighting.
Let’s walk through the setup step by step.
1. Extend Statamic’s markdown parser
The first step is to follow the Torchlight docs and extend the Markdown parser.
// AppServiceProvider.php use Torchlight\Commonmark\V2\TorchlightExtension;use Statamic\Facades\Markdown; public function boot(){ Markdown::addExtension(function () { return new TorchlightExtension(); });}
php
Now, any Markdown code blocks will be passed through Torchlight for highlighting.
2. Enable Torchlight inside Bard with Bard Mutator
Bard stores code blocks differently than Markdown. So we need to capture them and re-process them through Torchlight.
// AppServiceProvider.php use Statamic\Facades\Markdown;use JackSleight\StatamicBardMutator\Facades\Mutator; Mutator::html('codeBlock', function ($value, $item) { // Bard stores the language on the node; default to 'text' $lang = $item->attrs->language ?? 'text'; // Pull the raw code string out of the node $code = collect($item->content ?? [])->implode('text', ''); // Build a code block so Markdown + Torchlight can process it $md = "```{$lang}\n{$code}\n```"; // Render Markdown → HTML using Statamic’s Markdown (with Torchlight active) $html = Markdown::parse($md); // Replace the codeBlock node’s HTML with highlighted HTML return ['content' => $html];});
php
Mutator::html()
comes from Bard Mutator addon. It lets you intercept Bard nodes and return the HTML you want. Here we grab the node’s language
, collect the raw text content, wrap it in a code block, and render via Markdown::parse()
—which now runs through Torchlight thanks to step 1.
3. Add copy button and language tag
Highlighting alone is great, but let’s make it even better with:
A copy button.
A language tag overlay.
Update your Torchlight config
First, enable Torchlight’s built-in copyable option:
// config/torchlight.php return [ 'options' => [ 'copyable' => true, // Adds a hidden unformatted block for easy copying ],];
php
This enables a hidden .torchlight-copy-target
that contains the original source. Credit to Austen Cameron for this approach.
Enhanced codeBlock Mutator
Now update your mutator to insert a copy button and language tag:
// AppServiceProvider.php use JackSleight\BardMutator\Facades\Mutator;use Statamic\Facades\Markdown; Mutator::html('codeBlock', function ($value, $item) { // Bard stores the language on the node; default to 'text' $lang = $item->attrs->language ?? 'text'; // Pull the raw code string out of the node $code = collect($item->content ?? [])->implode('text', ''); // Build a code block so the Markdown + Torchlight extension can process it $md = "```{$lang}\n{$code}\n```"; // Render Markdown → HTML using Statamic’s Markdown facade (Torchlight extension is active) $html = Markdown::parse($md); // Render the view for the copy button and trim any trailing whitespace // to prevent introducing an extra preserved newline inside the <pre>. $copyButton = rtrim((string) view('components/copy-code-button')); $languageTag = '<span class="language-tag">' . e($lang) . '</span>'; // Inject the button and language tag inside the first <pre> tag $content = preg_replace('/(<\/pre>)/i', $languageTag . $copyButton . '$1', $html, 1); // Replace the codeBlock node’s HTML with our fully highlighted HTML, copy button and language tag return ['content' => $content];});
php
This injects both a language tag and a copy button into the first <pre>
.
Language note for Bard
Statamic stores the code block language on the node. Pasting code often auto-detects it. If not, check your entry's .md
file and set it manually:
- type: codeBlock attrs: language: yaml
yaml
Copy button partial
{{# resources/views/components/copy-code-button.antlers.html #}} <button x-data="copyCode" type="button" class="hidden md:flex absolute top-6 right-6 text-xs items-center gap-x-1" :class="copied ? 'text-green-400 hover:text-green-400' : 'text-gray-300 hover:text-gray-100'" @click="copyToClipboard()"> <span x-show="!copied" class="flex items-center gap-x-1"> {{ svg src="document-duplicate" class="size-4" }} <span>Copy</span> </span> <span x-cloak x-show="copied" class="flex items-center gap-x-1"> {{ svg src="document-check" class="size-4" }} <span>Copied!</span> </span></button>
antlers
This renders a lightweight UI for copying code with Alpine.
Alpine component
// resources/js/copyCode.js document.addEventListener('alpine:init', () => { Alpine.data('copyCode', () => ({ copied: false, messageDuration: 2000, copyToClipboard() { navigator.clipboard.writeText( this.$el.closest('pre').querySelector('.torchlight-copy-target').textContent ) this.copied = true setTimeout(() => (this.copied = false), this.messageDuration) }, }))})
js
This reads from Torchlight’s .torchlight-copy-target
for clean, unformatted copies.
Load the Alpine component only where needed
{{# resources/views/posts/show.antlers.html #}} {{ push:scripts }} {{ once }} {{ vite src="resources/js/copyCode.js" }} {{ /once }}{{ /push:scripts }}
antlers
In your layout, render the stack near </body>
:
{{# resources/views/layout.antlers.html #}} <body> {{# ... #}} {{# Push scripts when various components are added #}} {{ stack:scripts }} {{ vite src="resources/js/site.js" }}</body>
antlers
Also add resources/js/copyCode.js
to your Vite input array so it’s discoverable at build time.
This is my Alpine component loading technique so I can load components only where they’re used, instead of bundling them sitewide in site.js
.
4. Minimal CSS
Here’s the Tailwind CSS I used for styling code blocks:
/* Positions the button and language tag correctly. */pre { @apply relative;} /* Torchlight code blocks */pre code.torchlight { @apply block min-w-max pb-6 pt-16 relative;} pre code.torchlight .line { @apply px-6;} pre code.torchlight .line-number,pre code.torchlight .summary-caret { @apply mr-4;} pre .language-tag { @apply absolute left-6 top-6 select-none font-mono text-xs uppercase tracking-wide text-gray-300;}
css
Debug checklist
No highlighting? Ensure the Torchlight extension is registered in
AppServiceProvider
before Mutator runs, and your Torchlight API key is set.Wrong language? Verify
attrs.language
on the Bard node inside yourentry.md
.Copy button does nothing? Confirm
torchlight.options.copyable = true
and that.torchlight-copy-target
exists in the rendered HTML.Extra newline inside
<pre>
? Keeprtrim()
around your injected partial to avoid preserved whitespace.Bundling issues? Make sure
copyCode.js
is in Vite’s input and that you push the script stack on pages with code.Copy button position looks off? Make sure your
pre
tags haveposition: relative
.
Why this approach?
It reuses Statamic’s existing Markdown + Torchlight pipeline instead of inventing a custom Bard set or renderer. Bard Mutator turns codeBlock
nodes into Markdown and Torchlight does the rest with zero authoring friction.
If you’ve found a different or cleaner way to hook into Bard for Torchlight highlighting, let me know on X.