Home

Docs

Remark plugin allowing you to write clean markdown, while still using all the great features of MDX.

Use Astro 🚀 and this plugin to build your publishing pipeline for feature-rich and clean Markdown/MDX.

Have a look at the full documentation.

Astronaut, dust off your MDX!

What is this?

This package is a remark plugin for markdown files in the context of Astro site generation.

When should I use this?

If you use Astro to generate a site from Markdown files and you want to dust off your MDX.

The different features of this plugin will help you keep your Markdown clean:

Install

This package is ESM only. In Node.js (version 12.20+, 14.14+, or 16.0+), install with npm:

npm install astro-m2dx

…and in your astro.config.mjs

import { defineConfig } from 'astro/config';

import mdx from '@astrojs/mdx';
import m2dx from 'astro-m2dx';
//                ^^^^^^^^^^

/** @type {import('astro-m2dx').Options} */
const m2dxOptions = {
  // activate any required feature here *
};

// https://astro.build/config
export default defineConfig({
  integrations: [mdx()],
  markdown: {
    remarkPlugins: [[m2dx, m2dxOptions]],
    //               ^^^^
    extendDefaultPlugins: true,
  },
});

Use

When adding astro-m2dx to your project, none of the features is active by default, you have to activate them in the Astro configuration (and by providing the respective configuration files, e.g. for per-directory frontmatter).

The following features are available, toggle them by adding the option to your configuration object in the Astro configuration:

Default Frontmatter

frontmatter: boolean | string | { name?: string, resolvePaths: true };

Merge YAML frontmatter files into the frontmatter of MDX files.

The merge is only applied after all file-specific frontmatter items have been added. These will not be overwritten.

Now you can create frontmatter YAML files with the defined name in your src directory to define common properties.

All files up the directory tree are merged into the frontmatter, with values from the files frontmatter taking highest precedence and values from frontmatter files furthest up the tree taking least precedence. Object properties will be deeply merged, Array, Date and Regex objects will not be merged.

A very simple frontmatter file defining a default layout for all MDX files in a directory:

layout: @layouts/BlogLayout.astro

⚠️ Beware of relative references inside these files: The values are merged as-is and hence will be relative to the receiving MDX file and not the default frontmatter-file. It is safer to define paths in your tsconfig.json.
🦊 You can now specify resolvePaths: true to have your relative paths resolved with respect to the _frotnmatter.yaml file.

Export Components

exportComponents: boolean | string;

Merge ESM component mapping-files into the exported components object of MDX files.

In Astro you can define a mapping from HTML elements to JSX components in any MDX file by exporting a constant object components. With this feature you can define this export per directory, by creating an ESM file exporting a components constant object expression, that maps HTML tags to JSX components:

import { Title } from '@components/Title';

export const components = {
  h1: Title,
};

All files up the directory tree are merged, with mappings from the MDX file itself taking highest precedence and mappings from files furthest up the tree taking least precedence.

Auto-imports

autoImports: boolean | string;

Add imports for known JSX components in MDX files automatically.

Now create an auto-import file exporting known components:

import { Code } from 'astro/components';

export const autoimports = {
  Code,
};

Despite the suffix of the default value, these files should be simple ESM files (i.e. ES >=6) and not use any none-ES TypeScript features, because we need to parse them using acorn

In your MDX file you can now use <Code ... /> without importing it:

# My Title

Here I am embedding some fancy code from the frontmatter:

<Code code={frontmatter.rawmdx} />

You can structure your export pretty much as you like, as long as the variable initialization is an object expression without spread operator. Files are evaluated up the directory tree, i.e. files closer to the MDX file take precedence over files further up the tree.

The variables inside a file are evaluated in order of appearance, i.e. the following export would yield the component FuzzyBear over FozzieBear for the use in <Bear />, although b is the default export:

import { FuzzyBear } from '@components/FuzzyBear';
import { FozzieBear } from '@components/FozzieBear';

export const a = {
  Bear: FuzzyBear,
};

const b = {
  Bear: FozzieBear,
};

export default b;

Auto-imports have one sub-option

autoImportsFailUnresolved: boolean;

Fail if unresolved components cannot be resolved by autoImports.

Normalize Paths

normalizePaths:
  | boolean
  | string
  | {
    withFrontmatter?: boolean;
    rebase?: string;
    checkExistence?: boolean;
    includeOnly?: string[];
    exclude?: string[];
  };

Normalize relative paths in MDX file.

NOTE: If you want to use this feature together with relativeImages, you must exclude the node type image.

Relative Images

relativeImages: boolean;

Resolve relative image references in MDX files.

All relative image references (textual values) with a resolvable reference are replaced with an imported image reference in the compiled MDX.

Original MDX

![My alt text] (my-image.png "Fancy Title")

will be interpreted as if you wrote

import rel_image__0 from './my-image.png';

<img alt="My alt text" src={rel_image__0} title="Fancy Title" />

The resolution will also be applied to obviously relative image references in JSX components, i.e. any attribute value that starts with ./ or ../ and has typical image suffixes will be replaced by a MdxJsxAttributeValueExpression similar to the above.

Unwrap Images

unwrapImages: boolean;

Unwrap stand-alone images from paragraph

Identify Images

  identifyImages: boolean | string | number | { prefix?: string; digits?: number };

Assign identifiers to all images in the document

Style Directives

styleDirectives: boolean;

Apply classes from style directive to surrounding element.

The directive style is supported in all three directive forms

Leaf and text directive will apply the classes from the directive to the parent element and remove the directive from the MDAST. Using the container form will apply the class to the generic <div> element that is created by the directive itself, i.e. the following MDX snippet

:::style{.bg-accent}

## Chapter 1

::style{.decent}

A lot of text here.

:::

will result in this HTML

<div class="bg-accent decent">
  <h2>Chapter 1</h2>
  <p>A lot of text here.</p>
</div>

As you can see, if the classes from multiple directives are applied to the same element, the class list is joined (the class decent from the leaf directive is applied to its containing element, which in this case is the generic <div> element from the container style directive).

Because lists are not present in Markdown as such (only the list items), style could not be applied to the list as a whole. Therefore, there is a special directive ::list-style that applies the classes from the directive to the succeeding list, if there is one, i.e.

::list-style{.nav}

- Home
- Blog
- Docs

will result in this HTML

<ul class="nav">
  <li>Home</li>
  <li>Blog</li>
  <li>Docs</li>
</ul>

⚠️ Prerequisite remark-directive: In order to use this feature, you must insert the plugin remark-directive before astro-m2dx.

import { defineConfig } from 'astro/config';

import mdx from '@astrojs/mdx';
import m2dx from 'astro-m2dx';
import remarkDirective from 'remark-directive';

/** @type {import('astro-m2dx').Options} */
const m2dxOptions = {
  styleDirectives: true,
};

// https://astro.build/config
export default defineConfig({
  integrations: [mdx()],
  markdown: {
    remarkPlugins: [
      remarkDirective, // required for styleDirectives
      [m2dx, m2dxOptions],
    ],
    extendDefaultPlugins: true,
  },
});

One final request: This feature allows to mix content and representation, use carefully and prefer semantic class names over visual ones (I know the examples use some visual ones ;-()

Include directive

includeDirective: boolean | string;

Include other MDX files in your MDX file with a ::include[./partial.mdx] directive

This feature renders the included MDX file without modification as loaded from its origin, i.e. if its (merged) frontmatter contains a layout, then it will be rendered including the layout.

The directive recognizes the option unwrap, that inserts the included file into the parent directly after the node that contains the directive. This can be handy, e.g. if you sectionize your markdown according to headings and want to insert a section inbetween sections:

## Section 1

::include[./section2.mdx]{unwrap}

## Section 3

Without the option unwrap, the section2.mdx would always be inluded in the section created for ‘Section 1’.

⚠️ Prerequisite remark-directive: In order to use this feature, you must insert the plugin remark-directive before astro-m2dx.

Component directives

componentDirectives: boolean | string;

Map generic markdown directives to JSX components.

These files should be simple JavaScript/ESM files (i.e. ES >=6), e.g.

import { CTA } from '@components/CTA';

export const directives = {
  callToAction: CTA,
};

…and then use it in your Markdown like this:

::CTA[Dear Astronauts, grab your vacuum cleaner and dust off your MDX, now!]{href="https://www.npmjs.com/package/astro-m2dx"}

⚠️ Limitation: The names of the defined directives must be valid ES variable names, i.e. you can only use names, that you do not need to quote (especially: no snake-case).

⚠️ Prerequisite remark-directive: In order to use this feature, you must insert the plugin remark-directive before astro-m2dx.

Add-ons

  addOns: AddOn[];

Apply any custom transformations to the MDAST.

Inject Raw MDX

rawmdx: boolean | string;

Inject the raw MDX into the frontmatter.

Inject MDAST

mdast: boolean | string;

Inject the MD AST into the frontmatter.

The injected tree is not read by the HTML generation, so manipulation does not make sense.

Scan Title

scanTitle: boolean | string;

Scan the content for the title and inject it into the frontmatter.

The title will be taken from the first heading with depth=1, i.e. the first line # My Title.

If the frontmatter already has a property with that name, it will NOT be overwritten.

Scan Abstract

scanAbstract: boolean | string;

Scan the content for the abstract and inject it into the frontmatter.

The abstract will be taken from the content between the title and the next heading. It will only be textual content.

If the frontmatter already has a property with that name, it will NOT be overwritten.