Astro (v4) - Configure Expressive Code with Markdoc
Posted on Apr 28, 2024 in Articles
At time of writing, package versions are:
-
astro
:4.7.0
-
@astro/markdoc
:0.11.0
-
astro-expressive-code
:0.35.2
Overview
This post explores a basic Astro setup utilizing Markdoc and Expressive Code blocks.
The setup will:
- Render code fences parsed with Markdoc as Expressive Code blocks with proper language highlighting
- Work with Expressive Code's frame auto-parsing commented file names as code block titles
This setup will not:
- Parse titles from the fences because of a limitation with the Markdoc parser, but it will provide an alternative using Markdoc attributes.
Prerequisites
- A base Astro project
- Routing set up to serve Markdoc files to test that Expressive Code blocks are rendering
Configuration
Configure Expressive Code
- If
@astrojs/markdoc
isn't already installed, add it to your project withTerminal window pnpm astro add astro-expressive-code - In your
astro.config.mjs
file, configure Expressive Code themes and other desired options. I set a theme and the tab width.astro.config.mjs import { defineConfig } from 'astro/config';import expressiveCode from "astro-expressive-code";// ... Other Imports// https://astro.build/configexport default defineConfig({// ... Other settingsintegrations: [// ... Other integrationsexpressiveCode({themes: ['poimandres'], tabWidth: 2})]});
Configure Markdoc
- If
@astrojs/markdoc
isn't already installed, add it to your project withTerminal window pnpm astro add markdoc - In your
astro.config.mjs
file, configure desired Markdoc options. I use the default options here.astro.config.mjs import { defineConfig } from 'astro/config';import expressiveCode from "astro-expressive-code";import markdoc from "@astrojs/markdoc";// ... Other Imports// https://astro.build/configexport default defineConfig({// ... Other settingsintegrations: [// ... Other integrationsexpressiveCode({themes: ['poimandres'], tabWidth: 2}),markdoc()]}); - If you don't already have a
markdoc.config.mjs
in the project's root directory, create it now and copy/paste this base configuration for later.markdoc.config.mjs import { defineMarkdocConfig } from '@astrojs/markdoc/config';export default defineMarkdocConfig({})
Fence component
Create the component
Value | Markdoc attribute | Expressive Code prop |
---|---|---|
Code content | content | code |
Language | language | lang |
Because the Expressive Code prop names and Markdoc fence node attribute names don't map one-to-one, astro-expressive-code
's Code
component can't be used to replace the default Markdoc fence
component in the config.
Create a component that maps between Markdoc's fence node attributes and Expressive Code's component props to alleviate this issue.
- In
src/components
, create aFence.astro
file. - Write a component that imports the
Code
component from Expressive Code.- Map Markdoc's
content
to Expressive Code'scode
prop. - Map Markdoc's
language
to Expressive Code'slang
prop. - Pass the
title
andframe
attributes to their respective props
src/components/Fence.astro ---import {Code} from 'astro-expressive-code/components';interface Props {/*** @desc Markdoc places the fence content in the `content` property.* @example: `\r\n## Heading\r\n`*/content: string;/*** @desc Markdoc places the parsed fence language in the `language` property.* @example: `typescript`*/language?: string;/*** @desc title is a custom Markdoc schema addition to pass a title to the code fence.* @summary `title` is not required.* If using Expressive Code's commented file name, it will populate that.* If both are specified, it will follow Expressive Code's default behavior.*/title?: string;/*** @desc Custom Markdoc schema addition to specify the Expressive Code frame type* @summary `frame` is not required, and if not specified, defaults to `auto`.* @example* ```javascript {\% frame="none" %}* var foo = "a";* ``` will set the frame to none.*/frame?: PluginFramesProps['frame'];}const {content, language, title, frame} = Astro.props;---<Code code={content} lang={language} title={title} frame={frame} /> - Map Markdoc's
Use the new component in Markdoc
In the Markdoc config file, override the @astrojs/markdoc
renderer with our new component. In order to use Expressive Code's rendering logic, don't spread the other fence node properties from @astrojs/markdoc
's fence node.
If the @astrojs/markdoc
fence node transformer is included, Expressive Code will error when rendering the code blocks.
- The default
@astrojs/markdoc
fence transformer adds content to the defaultslot
. - Expressive Code's
Code
component has a check that nothing is rendered in the defaultslot
, and this is not currently a configurable option.
import { component, defineMarkdocConfig } from '@astrojs/markdoc/config';
export default defineMarkdocConfig({ nodes: { // ... Other nodes fence: { // Add custom Expressive Code component render: component('./src/components/Fence.astro'), attributes: { // ... Default attributes we're using from Markdoc fence component content: {type: String, required: true}, language: {type: String}, // ... Attributes we're adding to have them available in the code component title: {type: String}, frame: {type: String, matches: ['auto', 'none', 'code', 'terminal']} } } }});
Test the new component
Load a page with a code block and it should be displayed in an Expressive Code code block.
Try using different markdown scenarios and see what happens. Here are a few to get you started.
Remove backslashes (\
) from the upcoming Markdoc examples. The backslashes were added to avoid parsing errors.
Example: {\% title="My Title" %}
should not have the \
between the {%
.
Render a code block with a file name as a code comment
The markdown:
```astro// src/components/Paragraph.astro------<p><slot /></p>```
The result:
------<p><slot /></p>
Render a code block with a title attribute
The markdown:
```astro {\% title="My Title" %}------<p><slot /></p>```
The result:
------<p><slot /></p>
Render a frame as a terminal frame instead of a code frame
The markdown:
```astro {\% frame="terminal" %}------<p><slot /></p>```
The result:
------<p><slot /></p>