A tool to convert Markdown files into React components. Try it yourself with the MDXC Playground.
MDX is a simpler way to write content for you React applications. While standard Markdown compiles to a string of HTML, MDX compiles directly to JavaScript. If you’re writing a React app, MDX is both easier to use and more flexible than standard Markdown.
Writing with MDX let’s you use the full power of React, even when writing content.
The easiest way to try MDX is to edit the source on the left of this page!
Otherwise, you can try the old fashioned way:
# Install the `mdxc` command line tool using npm
npm install -g mdxc
# Create a file with a single heading
echo '# Hello, World' > example.mdx
# Output the compiled component to your console
mdxc example.mdx
Other ways to use MDX include:
For details, jump to the usage section
Let’s go over some of the basics – we’ll leave out the boilerplate on the JavaScript output. For more detailed examples or examples which can be run locally, see the examples
directory.
MDX, like JSX, requires all tags to be closed. This means that unclosed tags like <br>
are just treated as plain text, while properly closed tags result in React elements.
The tag <br> is not closed. The tag
is closed.
Mixing <span>unclosed and closed treats the outermost tags as text.
Because an MDX file compiles to a React Component, you’ll need to name use React-style attributes.
classy
For tags which are “inline” – i.e. within a paragraph of text, the tag’s content is treated as Markdown – you can use `backticks`, *asterisks**, etc. Inline tags can also contain nested tags, but unlike JSX, {braces}
are treated as text.
Within inline tags, {braces} are text and nested elements are ok.
For tag “blocks” – where a paragraph starts and ends with the same tag – the content is treated as it would be in a JSX document. This means that whitespace is ignored, backticks
are plain text and {braces}
can be used to break out to JavaScript. If you’d like to pass Markdown-formatted text as children, wrap it in the special <markdown>
tag.
# Markdown DOESN'T work here.
But does work here.
Markdown is limited in functionality by design, and MDX is no different. When you want to do something complex, you’ll need to delegate to an old fashioned React component. And to access custom React components, you’ll need to use import
.
The syntax for MDX import
is identical to ES2015 import
. The only difference is that import
statements must come before any whitespace or blank lines.
Say you have a <Playground>
component that compiles and runs some example code on the fly. You might use it like this:
Notice how your content in the generated code uses a different indentation level to the wrapper code. This ensures that any strings within your MDX do not have unwanted spaces inserted at the start of each line.
You can declare that your generated component accepts a prop by adding a prop [propname]
at the top of your file, after any imports. This variable can then be used within your embedded elements. Remember that {braces}
are treated as strings within inline tags; if you want to interpolate a prop within your text, you’ll need to do it within a JSX block!
When MDX generates code for your Markdown, it doesn’t directly generate a JSX <tag />
(or its JavaScript equivalent React.createElement('tag')
). Instead, it calls a factory function with the tag’s props and children.
Generally, you don’t need to worry about factories. MDX provides default factories that just render the tag – as expected. But variety is the spice of life, so in all likelihood you’ll sometimes want to customize the behavior of certain tags. And doing so is as simple as passing an object to your generated component’s factories
prop!
pushState
support to linksIf you’re loading content within a React app, then in all likelihood your app is using HTML5 history through a library like react-router or react-junctions.
The great thing about HTML5 history is that it provides a way to navigate without reloading the page. The crap thing about it is that good-ol’ <a>
tags will still reload the page. So if you want your content to feel snappy and not out of place, you’ll want it to use a <Link>
component instead.
// Assume './content' is a file generated by mdxc
import Content from './content'
import { Link } from 'react-router'
export default function ContentWrapper(props) {
return (
<Content
{...props}
factories={{ a: Link }}
/>
)
}
Another use for factories is to add “anchor links” to your headings. To do so, you’d pass in custom factories for tags like h1
and h2
.
// Assume './content' is a file generated by mdxc
import Content from './content'
function headingFactory(type, props, ...children) {
// Render the same props and children that were passed in, but prepend a
// link to this title with the text '#'. Note that MDX already adds a slug
// to each title under its `id` attribute.
return React.createElement(
type,
props,
<a href={'#'+props.id}>#</a>,
...children
)
}
export default function ContentWrapper(props) {
return (
<Content
{...props}
factories={{
h1: (props, children) => headingFactory('h1', props, children),
h2: (props, children) => headingFactory('h2', props, children),
h3: (props, children) => headingFactory('h3', props, children),
}}
/>
)
}
A tool wouldn’t be a tool without a good old CLI. It probably isn’t that useful, but hey. It’s the vibe, and all that. What I mean is, if you must, you may use the CLI to create js
files from mdx
files by hand.
# Install the `mdxc` command line tool using npm
> npm install -g mdxc
# Then call `mdxc --help` to see how to use it
> mdxc --help
Usage: mdxc [options] <file>
Compile mdx to js
Options:
-h, --help output usage information
-V, --version output the version number
-c, --common use commonJS modules (i.e. module.exports and require)
-o, --output <file> output to file instead of console
-u, --unwrapped don't wrap the content in a React component
MDXC was originally created as a plugin for Sitepack.
Sitepack is a wrapper around Webpack. It gives you a way to add pages to your website using plain-old require()
, it handles the nasty parts of Webpack configuration for you, and it does all this while performing the magic required to build a static HTML version of each of your pages. As it happens, the MDXC website is built with Sitepack.
If your plan is to use MDX to write an actual website (you know, with pages and links and no “login” button), then Sitepack is worth giving a try. It’s documentation is still in its infancy, but you can get around this by using the sitepack-react-starter-kit which supports MDX out of the box.
git clone https://github.com/jamesknelson/sitepack-react-starter-kit.git
cd sitepack-react-starter-kit
npm install
npm start
Once you’ve cloned, installed and started sitepack, open your browser at http://localhost:4000 and start editing the md
files in the content
directory – changes will be reflected live! When you’re ready to release, build your site with npm run build
.
If you’d like to use MDX within an existing react app, chances are you’ll want to use mdx-loader. To do so, just add it to your project and then update the your webpack.config.js
. Bear in mind that MDXC outputs ES2015, so you’ll need to run the output through Babel if you want to support older browsers.
# Add mdx-loader to your project
npm install --save-dev mdx-loader
Assuming you’re using Webpack 2, you’ll need to add an entry to your module.rules
array:
module: {
rules: [
/**
* MDX is a tool that converts Markdown files to React components. This
* loader uses MDX to create Page objects for Markdown files. As it
* produces ES2015, the result is then passed through babel.
*/
{ test: /\.mdx?$/,
use: [
'babel-loader',
'mdx-loader',
]
},
// ...
]
},
Then import and use your components as you’d do with standard JavaScript!
At its core, MDXC is just a wrapper around the excellent and highly configurable markdown-it project. This means that its API is mostly the same.
The major difference is that the default renderer from markdown-it has been replaced with a non-configurable renderer. Existing markdown-it plugins will work, but any hooks on the renderer will silently fail. This is the behavior you want, as these hooks transform HTML strings, and MDXC doesn’t use any HTML strings. If you need to transform the output, use factories instead.
To transform MDX into JavaScript, create an instance of the MDXC
class and then call its render
method. If you’d like to load any markdown-it plugins, call the use
method on your MDXC
instance before calling render
.
import MDXC from 'mdxc'
var mdxc = new MDXC({
linkify: true,
typographer: true,
})
var js = mdxc.render('# This is a heading')
The options to mdxc
are mostly the same as the options for markdown-it. For details, see the markdown-it API docs.
MDXC also provides a few extra options.
commonJS
bool If true, your imports/exports will be transformed to use require()
and module.exports
unwrapped
*bool` If true, the component definition boilerplate will be omittedTo see this all in use, here is a slimmed-down version of the mdx-loader
package:
const url = require('url')
const path = require('path')
const loaderUtils = require('loader-utils')
const Prism = require('prismjs')
const MDXC = require('mdxc')
// Set up syntax highlighting for code blocks with PrismJS
const aliases = {
'js': 'jsx',
'html': 'markup'
}
function highlight(str, lang) {
if (!lang) {
return str
}
else {
lang = aliases[lang] || lang
require(`prismjs/components/prism-${lang}.js`)
if (Prism.languages[lang]) {
return Prism.highlight(str, Prism.languages[lang])
} else {
return str
}
}
}
// A markdown-it plugin that requires your images with Webpack
function mdImageReplacer(md) {
md.core.ruler.push('imageReplacer', function(state) {
function applyFilterToTokenHierarchy(token) {
if (token.children) {
token.children.map(applyFilterToTokenHierarchy);
}
if (token.type === 'image') {
const src = token.attrGet('src')
if(!loaderUtils.isUrlRequest(src)) return;
const uri = url.parse(src);
uri.hash = null;
token.attrSet('src', { __jsx: 'require("'+uri.format()+'")' });
}
}
state.tokens.map(applyFilterToTokenHierarchy);
})
}
module.exports = function mdxLoader(content) {
const mdxc =
new MDXC({
commonJS: true,
linkify: true,
typographer: true,
highlight: highlight,
})
.use(mdImageReplacer)
return mdxc.render(content);
}
Mad props to markdown-it for doing the hard yards which make MDXC possible. Also, markdown-it-jsx provided the base for parsing JSX tags out of Markdown files. Finally, there would be no use making MDXC unless React, Webpack and Babel already existed.
Help is muchly appreciated! In particular, PRs for the following would be super duper great: