Skip to content

Astro (v4) - Configure Expressive Code with Markdoc

Posted on Apr 28, 2024 in Blog Posts


Version Reference

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

  1. If @astrojs/markdoc isn't already installed, add it to your project with
    Terminal window
    pnpm astro add astro-expressive-code
  2. 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/config
    export default defineConfig({
    // ... Other settings
    integrations: [
    // ... Other integrations
    expressiveCode({themes: ['poimandres'], tabWidth: 2})
    ]
    });

Configure Markdoc

  1. If @astrojs/markdoc isn't already installed, add it to your project with
    Terminal window
    pnpm astro add markdoc
  2. 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/config
    export default defineConfig({
    // ... Other settings
    integrations: [
    // ... Other integrations
    expressiveCode({themes: ['poimandres'], tabWidth: 2}),
    markdoc()
    ]
    });
  3. 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

ValueMarkdoc attributeExpressive Code prop
Code contentcontentcode
Languagelanguagelang

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.

  1. In src/components, create a Fence.astro file.
  2. Write a component that imports the Code component from Expressive Code.
    • Map Markdoc's content to Expressive Code's code prop.
    • Map Markdoc's language to Expressive Code's lang prop.
    • Pass the title and frame 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} />

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.

Warning

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 default slot.
  • Expressive Code's Code component has a check that nothing is rendered in the default slot, and this is not currently a configurable option.
markdoc.config.mjs
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.

Important

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:

src/components/Paragraph.astro
---
---
<p><slot /></p>

Render a code block with a title attribute

The markdown:

```astro {\% title="My Title" %}
---
---
<p><slot /></p>
```

The result:

My Title
---
---
<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:

Terminal window
---
---
<p><slot /></p>

Resources