Adding a Language Shiki Doesn't Know
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
The Problem You Don’t Notice
This site has two blog posts about Caddy. Both use fenced code blocks with ```caddy to show Caddyfile configuration. Expressive Code renders them, Shiki tokenizes them, the page builds without errors.
Except every Caddy block was rendering as plain text. No syntax highlighting, no color, no visual structure. Just monochrome characters on a dark background.
The blocks looked like code. They had line numbers, the frame, the copy button. Everything Expressive Code adds was there. What was missing was what Shiki adds: the actual highlighting. Directives, strings, comments, values, all the same color.
I noticed when I compared the blog to VS Code. The same Caddy config, full color in the editor, monochrome on the page.
Why It Happens
Expressive Code uses Shiki for syntax highlighting, and Shiki bundles grammars for over 100 languages. JavaScript, Python, TypeScript, Bash, HTML, CSS, Go, Rust. The languages most technical blogs write about.
Caddy is not one of them. Neither is Traefik, HAProxy, or Docker Compose, tools that infrastructure-focused developers write about daily. When Shiki encounters a language identifier it doesn’t recognize, it falls back to plaintext. No error, no build failure. Just a warning buried in build output that you probably don’t read:
[astro-expressive-code] Error while highlighting code block using language "caddy".The language could not be found. Using "txt" instead.The word “error” is generous. Nothing breaks. The page renders. The code block looks like a code block. It just doesn’t do the one thing a syntax highlighter is supposed to do.
Where Grammars Come From
Shiki uses TextMate grammars, the same format VS Code uses for syntax highlighting. Every VS Code language extension ships a .tmLanguage.json file that defines how the language is tokenized: what’s a keyword, what’s a string, what’s a comment.
If VS Code highlights your language correctly, the grammar already exists. It’s sitting in your extensions folder.
For Caddy, the official extension is caddyserver/vscode-caddyfile, published as matthewpi.caddyfile-support. MIT licensed.
The grammar file is at:
~/.vscode/extensions/matthewpi.caddyfile-support-0.4.0/syntaxes/caddyfile.tmLanguage.jsonThe same file that makes VS Code highlight Caddyfiles can make Expressive Code highlight them too. Copy it into your project, point the config at it, done.
Five Lines of Config
Expressive Code accepts custom grammars through the shiki.langs option in ec.config.mjs:
import { defineEcConfig } from 'astro-expressive-code'import fs from 'node:fs'
const caddyfile_Grammar = { ...JSON.parse(fs.readFileSync('./src/Scripts/Build/Grammars/caddyfile.tmLanguage.json', 'utf-8')), name: 'caddy',}
export default defineEcConfig({ shiki: { langs: [caddyfile_Grammar], langAlias: { caddyfile: 'caddy' }, },})Load the grammar, add it to langs, optionally alias alternative names. The rest of the config stays the same.
Build the site. No more warnings. Code blocks light up.
Why It Still Didn’t Work
Except they didn’t. Not the first time.
The grammar loaded without errors. The build ran cleanly. The code blocks still rendered as plain text. No warnings, no indication that anything was wrong.
The issue was the grammar’s name field. The Caddyfile grammar declares itself as name: "Caddyfile", capital C, capital F. But the fenced code blocks in the MDX files use ```caddy, lowercase. Shiki matches language names case-sensitively. "Caddyfile" does not match "caddy".
The langAlias option looked like the fix. Map caddy to caddyfile. But the alias resolves before checking loaded languages, and the grammar registered under "Caddyfile", not "caddyfile". The alias pointed to a name that didn’t exist.
The fix: override the grammar’s name property when loading it.
const caddyfile_Grammar = { ...JSON.parse(fs.readFileSync('./path/to/caddyfile.tmLanguage.json', 'utf-8')), name: 'caddy', // override "Caddyfile" to match ```caddy fences}One line. The grammar now registers as "caddy", the fences say caddy, Shiki finds a match. Everything works.
This is the kind of bug that doesn’t announce itself. No stack trace, no error message, no failed build. The grammar loads successfully. Shiki just never connects it to your code blocks. You have to know that Shiki’s name matching is case-sensitive, and you have to know that the grammar’s internal name might not match the identifier you’re using in your Markdown. Neither of these is documented in Expressive Code’s setup guide.
The Pattern
Adding a custom language to Expressive Code is a three-step process:
-
Find the grammar. Check your VS Code extensions folder. If your editor highlights the language, the
.tmLanguage.jsonexists. Copy it into your project so the build doesn’t depend on your local editor setup. Check the license. -
Register it. Add it to
shiki.langsin your Expressive Code config. UselangAliasif you want multiple identifiers to resolve to the same grammar. -
Match the name. Override the grammar’s
nameproperty to match whatever you use in your fenced code blocks. Don’t assume the grammar’s internal name matches your fence identifier.
The whole process, once you know it, takes five minutes. Finding out that you need to do it, and why the first attempt fails silently, is the actual work.
What It Looks Like Now
Before:
handle_errors { rewrite * /404.html file_server}After:
handle_errors { rewrite * /404.html file_server}Same content. The difference is whether the reader’s eye can parse the structure at a glance or has to read every word. For a post that’s trying to teach someone how to configure Caddy, that difference is inshallah the gap between useful and frustrating.
Code blocks on a technical blog are a quiet credibility signal. When they render with full highlighting, nobody notices. When they don’t, the people who would notice are exactly the people you want reading your work.
Aerospace engineer
Ethical entrepreneur in public
You handle your business
I handle the digital side
Work with me
- AI with honesty
- Private infrastructure
- Websites that perform
Tell me about your situation:
javed@javedab.com More about me