I started using Shiki the day I migrated my blog from N(ext.js|uxt.js) to Astro. I fell in love with it and couldn't go back. But recently, I discovered an even better option.
Expressive Code claims to be a fully-featured,framework-agnostic, plugin-based and accessible toolkit for enhancing code blocks in technical articles. You can probably see it in action these days on many technical blogs, by way of Starlight .
But, after a quick look in the Expressive Code repository, I knew I had to use my Saturday coding because Markdoc support is not added yet.
The "metas" of it all
Markdoc only recognizes content
and language
attributes on the so-called fence
nodes. So even overwriting the fence node and adding a ton of attributes on it might not work as expected if using the wrong syntax in the meta string.
What is the meta string? It's the bit after this:
So, something like {3} ins={1-2}
after the code fence and language (as shown in the docs) will not highlight line 3 and mark lines 1-2 as inserts in Markdoc because they are ignored.
I also discovered that the Expressive Code component distinguishes between meta configuration for the default set of features (meta
), and the optional plugins (collapse
, showLineNumbers
, startLineNumber
, etc).
To replicate my solution in your project, start with at least a basic Astro + Markdoc app. See how you can organize your Markdoc configs in the Astro project .
Let's go!
Install and configure Expressive Code
I tried following the documentation to install the library, but for some reason the npx astro add astro-expressive-code
failed and I had to go the manual configuration route.
For the full setup, we will be using the Collapsible Sections and the Line Numbers plugings, so the installation and configuration procedure below covers them as well.
To configure Expressive Code and plugins manually:
In your terminal, change directory to your Astro app.
Install the package and plugins using you package manager of choice:
Sidenote: I need tabs :|
In the
astro.config.m(js|ts)
file, import and enable the Expressive Code integration:In the root folder of your application create a new file named
ec.config.mjs
, with the following content:
Override the fence node in the Markdoc configuration
For all this to work, however, we need to override the default behavior of the fence
Markdoc node.
By default, the fence
node supports the attributes language
and content
. To make it accept the meta string and other custom attributes that Expressive Code can use, we need to add them as shown in the following example:
Import the fence
node configuration in the main Markdoc config, and add it to the nodes
configuration, if not already added.
Create the CodeBlock component
With all the configuration in place, it's time to define the CodeBlock
component that we assigned to the fence
node renderer. It's a tiny Astro component:
With everything in place, we can now test things out.
Configuration examples
While not perfect, this implementation works well with Markdoc. Configuring the meta strings can be tricky, but by using single and double quotation marks carefully you can get the desired behavior.
Usig the configuration ts {% meta="{'3': 8} del={'1': 5} ins={'2': 6} 'friggin cool!'" collapse="1-3,5-7" showLineNumbers=true startLineNumber=5 %}
, you should get:
Here's a breakdown for configuring various directives:
- Give
astro
code boxes a title - you must use themeta
property:{% meta="title='@components/astro/CodeBlock.astro'"%}
. For others, use the comment syntax for that language in the first line. For example// my-file.js
. - Set the frame:
{% meta="frame='none'"%}
. See the editor & terminal frames documentation for more information. - Add text and line markers :
- Highlight line 5:
{% meta="{5}"%}
. - Highlight the word "something":
{% meta=" 'something' "%}
. You can use spaces, any alphanumeric character without issues. Apostrophes and quotes would be tricky. - Highlight multiple lines:
{% meta="{5, 7-9}"%}
highlights line 5 and 7 through 9. - Mark lines added and removed:
{% meta="del={1-2} ins={3-6, 8}"%}
.
- Highlight line 5:
- Wrap lines :
{% meta=" wrap=true "%}
marks lines 1 and 2 as deleted, 3 through 6 and 8 as insertions. - Collapse lines:
{% collapse="1-3,5-7" %}
collapses lines 1-3 and 5-7. See the documentation on collapsible sections of code . - Configure line numbers :
{% showLineNumbers=true startLineNumber=5 %}
.
Points to remember
For future use, and when updating to the component and fence
node, unless Expressive Code makes breaking changes:
- If it is a key/default feature, use the
meta
string attribute. - If it's a plugin, you configure the plugin "metas" individually.
- When labelling the code with very long strings, you need to add empty lines and adjust the line number as needed.
- Juggling the single and double quotes for the
meta
attribute value can get tricky, but the result is totally worth it.