Extend Astro Syntax Highlighting

Update (August 2026)

Little overdue, but there are good news! As of Astro v4.6, this whole workaround is no longer needed!

Starting with v4.6, shikiConfig.langs went back to extending the built-in language list instead of replacing it. Astro now lazy-loads the languages that Shiki ships with on demand (PR #10618), so langs is only needed to add your custom grammars:

js
// astro.config.mjs

// 1. Pull in the languages that are bundled with Shiki
import { bundledLanguages } from 'shiki'
// 2. Import the grammar file for the language(s) you want to add
import caddyLang from 'path/to/caddyfile.tmLanguage.json' with { type: 'json' }

export default defineConfig({
    // ...
    markdown: {
        shikiConfig: {
            // ...
            langs: [
                // 3. Then we add our custom language(s) to the list
                {
                    id: 'caddyfile',
                    scopeName: 'source.Caddyfile',
                    aliases: ['caddy'],
                    ...caddyLang,
                },
            ]
        }
    }
})

That’s it! No more manually opting in to every language, they all keep working out of the box.

Funny enough, the replacing behavior only lived from Astro v4.0 to v4.5 — I published this post just 11 days before Astro v4.6 made it obsolete. If you keep the old snippet around, it still works, but you’ll be eagerly loading every bundled grammar and losing the lazy-loading benefit.

Original Post

Shiki is by far my favorite tool for syntax highlighting. It is (now) ESM-compatible, framework-agnostic, even runtime-agnostic, and has native support for TextMate grammars, which are used by MANY code editors and IDEs. This allows you to quickly integrate Shiki wherever you need it, supporting whichever languages you want, and having the best theme out there for your needs.

Astro with its content-focused integrations, provides support for syntax highlighting in code blocks out of the box with Shiki if you are making use of Markdown (or MDX) or their <Code> component. Such integration saves a lot of time and makes it pretty easy to customize.

Recently, I updated this very blog from Astro v3 to Astro v4 (btw, their upgrade guide is awesome!) and noticed that while there were very minimal breaking changes between the two versions, one thing was never mentioned: In Astro v3, the configuration property markdown.shikiConfig.langs extended the list of languages for which Shiki enabled syntax highlighting; while in Astro v4, this same property now overrides the list of languages.

This one was pretty easy to catch as there is only one language (at the moment) that I use in this blog that is not part of the official list of languages included in Shiki: Caddyfile.

Checkout my post about HTTPS behind VPNs using Caddy!

After a quick dive into Shiki’s source code, I noticed that the 1.0 release made some big changes in the way languages are loaded, being this probably the reason behind the change from extending to overriding the language list via the configuration property.

In my case, I don’t want to manually “opt-in” all the languages that I may (or may not) be using in this blog, I just want to extend the available languages with the grammar files for those not-officially packed with Shiki… And it is actually pretty easy to do!

js
// astro.config.mjs

// 1. Pull in the languages that are bundled with Shiki
import { bundledLanguages } from 'shiki'
// 2. Import the grammar file for the language(s) you want to add
import caddyLang from 'path/to/caddyfile.tmLanguage.json' assert { type: 'json' }

export default defineConfig({
    // ...
    markdown: {
        shikiConfig: {
            // ...
            langs: [
                // 3. As bundled language are already registered,
                //    we just pull their names.
                ...Object.keys(bundledLanguages),
                // 4. Then we add our custom language(s) to the list
                {
                    id: 'caddyfile',
                    scopeName: 'source.Caddyfile',
                    aliases: ['caddy'],
                    ...caddyLang,
                },
            ]
        }
    }
})

That’s all! Now, the caddyfile language is back in this blog without needing to manually opt in to all the other languages.

Happy Easter!