astro-m2dx
v0.7.16
Published
Remark plugin to enhance MDX in the scope of Astro site generation
Downloads
225
Maintainers
Readme
astro-m2dx
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!
Content
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:
- Define default frontmatter properties for all files in a directory, e.g. the
layout
- Map HTML elements to JSX components on a per-directory basis
- Auto-import known JSX components on a per-directory basis
- Scan the document for Title or Abstract to use your content to define the title and abstract for your document and omit ugliness like
# {frontmatter.title}
- Inject raw MDX and get (read-only) access to the (really) raw MDX content of your file.
- Inject the MDAST and get (read-only) access to the MarkDown Abstract Syntax Tree, e.g. to transform to text or analyze for added meta-info in your layout.
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.
- default:
false
, no frontmatter is merged true
, to enable frontmatter merging from files with name_frontmatter.yaml
and without resolving relative paths<name>
, to find frontmatter in YAML files named<name>
{ name?: string, resolvePaths: true }
, to resolve relative paths from the merged frontmatter file with respect to that file
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 specifyresolvePaths: 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.
- default:
false
, no component mapping is merged true
, to enable component mapping merging from files with name_components.ts
<name>
, to find component mapping-files with<name>
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.
- default:
false
, no components are imported automatically true
, to enable automatic imports from files with name_autoimports.ts
<name>
, to find automatic imports in files named<name>
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.
- default:
false
true
to throw an error on unresolved components
Normalize Paths
normalizePaths:
| boolean
| string
| {
withFrontmatter?: boolean;
rebase?: string;
checkExistence?: boolean;
includeOnly?: string[];
exclude?: string[];
};
Normalize relative paths in MDX file.
- default:
false
true
, to have relative paths normalized with default settings (see below){...}
, use the options object to configure individual settings- withFrontmatter: boolean, default = true, whether to normalize paths in frontmatter
- rebase: string, default =
undefined
(i.e. resulting paths will be absolute), path to use as new base and make all resulting paths relative - checkExistence: boolean, default = true, normalize path only, if normalized path exists, leave untouched otherwise
- includeOnly: list of MDX element types or JSX tags (if put in angle
brackets, e.g.
<img>
) to include during path normalization (only the named types will be included) - exclude: list of MDX element types or JSX tags (if put in angle
brackets, e.g.
<img>
) to include during path normalization - exclude: list of MDX element types to exclude during path
(e.g.
['link', '<a>']
to exclude markdown links and JSX anchor tags)
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.
- default:
false
true
, to enable relative image resolution
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
- default:
false
true
, remove wrapping paragraph element around stand-alone images
Identify Images
identifyImages: boolean | string | number | { prefix?: string; digits?: number };
Assign identifiers to all images in the document
- default:
false
, no identifiers are assigned true
, identifiers are assigned with the default prefiximg_
and default number of digits3
, the resulting ids look likeimg_007
<prefix>: string
, identifiers use the prefix<prefix>
and default number of digits3
<digits>: number
, identifiers use the default prefiximg_
and the number of digits is<digits>
{ prefix: <prefix>, digits: <digits> }
, identifiers use the given values for prefix and digits, e.g.{ prefix: 'photo', digits: 5 }
would result in identifiers likephoto12345
Style Directives
styleDirectives: boolean;
Apply classes from style directive to surrounding element.
- default:
false
true
, to apply classes to surrounding element
The directive style
is supported in all three directive forms
- container -
:::style{.some-class} ... :::
around a list of elements - leaf -
::style{.some-class}
inside container elements - text -
:style{.some-class}
inside paragraphs or spans
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 pluginremark-directive
beforeastro-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
- default:
false
true
, to enable this directive with the name::include
<name>
, to enable the directive with name::<name>[./ref.mdx]
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 pluginremark-directive
beforeastro-m2dx
.
Component directives
componentDirectives: boolean | string;
Map generic markdown directives to JSX components.
- default:
false
, no directives are mapped to components true
, to enable mapping directives to components according to files with name_directives.ts
<name>
, to find directive mappings in files named<name>
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 pluginremark-directive
beforeastro-m2dx
.
Add-ons
addOns: AddOn[];
Apply any custom transformations to the MDAST.
- default: none
- Set of transformer functions that are executed after all internal astro-m2dx transformations
Inject Raw MDX
rawmdx: boolean | string;
Inject the raw MDX into the frontmatter.
- default:
false
true
, to have it injected into propertyrawmdx
<name>
, to have it injected as property<name>
Inject MDAST
mdast: boolean | string;
Inject the MD AST into the frontmatter.
- default:
false
true
, to have it injected into propertymdast
<name>
, to have it injected as property<name>
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.
- default:
false
true
, to have it injected into propertytitle
<name>
, to have it injected as property<name>
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.
true
, to have it injected into propertyabstract
<name>
, to have it injected as property<name>
- default:
false
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.