Skip to content

RFC: MDX plugins #741

@johno

Description

@johno

Summary

The plugins from remark and rehype have been great to add in features and functionality, however I think MDX-specific plugins can take advantage of features more inline with the MDX language.

Motivation

There are many common needs in the MDX ecosystem (frontmatter, table of contents, syntax highlighting, oembed, etc.) which don't make sense in core but are complex to set up or configure. Not to mention, there's some fragmentation developing around how these things are accomplished in different ecosystems (like Gatsby and Next.js).

Providing a plugin-based approach for these common pieces of functionality can help ensure that the best possible solution can be arrived upon and shared amongst a large group of folks. Not to mention, this could also provide a path for folks to experiment and innovate much more in userland.

A table of contents is the perfect example to show how a piece of functionality in an MDX-based stack needs a "vertical slice" of functionality. This allows a single plugin to set up everything it needs across all transformations MDX => MDAST => HAST => JSX => React|Vue.

The way it works today

First, let's consider the ecosystem as it currently stands. For this RFC we will use adding a table of contents as the example functionality. Without MDX plugins, the current flow looks like:

  • Add remark-slug
  • Install github-slugger which remark-slug uses
  • Customize h1-h6 to be linkable using github-slugger
  • Create a custom plugin that would assign headings for toc as an export (or use a custom loader, gatsby-plugin-mdx, etc)
  • Build a component for import/render that turns the toc into a linked list
  • Know the data export for the toc and pass to component

One could also use remark-toc to achieve some of this, but it doesn't give you much flexibility in how/when the toc is rendered and it doesn't allow the heading information to be imported from outside the document.

This isn't the end of the world, but it results in a lot of moving parts where a single plugin could handle a lot of this and end up being more flexible.

Basic (proposed) example

Note: This is not intended to be the implementation/usage but an abstract example to illustrate the functionality.

mdx-toc

End user functionality

Inject table of contents data

By default, mdx-toc might process the MDXAST and inject an export of the table of contents as an array.

export const tableOfContents = [
  {
    value: 'Some heading',
    depth: 2
  }
]

# My document

<!-- ... -->
Import table of contents data for usage at the layout level

The injected data could be used at the layout level to add a table of contents.

import { tableOfContents }, Document from './document.mdx'

export default () => (
  <DocumentLayout>
    <SidebarNav />
    <Main>
      <Document />
    </Main>
    <SidebarTableOfContents>
      <TableOfContents headings={tableOfContents} />
    </SidebarTableOfContents>
  </DocumentLayout>
)
Use the table of contents inside the document
import { TableOfContents } from 'mdx-toc'

## Table of contents

<TableOfContents />

This TableOfContents would receive the data it expects as part of its babel transformation.

Plugin developer perspective

A plugin author could expose a file with properties for the different stages in the pipeline (all of which are optional)

module.exports.remarkPlugins = [
  remarkSlug,     // Add slugs
  remarkMdxToc // Adds export const tableOfContents = []
]
module.exports.rehypePlugins = []
module.exports.babelPlugins = [addTableOfContentsProps] // Adds data prop for component

Detailed design

An MDX plugin would be given access to each stage of the transpilation pipeline. This means that a plugin could manipulate the MDAST and HAST in order to add rich functionality.

MDX plugins would be passed as their own option and would be appended to the existing remark/rehype plugins. We'd also need to add babel plugins to the existing babel processing.

The component rendering portion would exist in userland to avoid any magic. They'd have to be manually added to MDXProvider at the layout level.

Ultimately this wouldn't require much engineering effort since we'll be mostly forwarding plugins to remark/rehype/babel. Most of the work will be in making sure the API is solid and documenting it.

Other feature considerations

Perhaps it could make sense to allow plugins to compose in additional functionality via the MDXProvider. Considering the above example for mdx-toc, that same plugin might want to do something like:

// mdx-toc/react.js
import slug from 'github-slugger'

const slugifyComponents = ({
  h1, h2, h3, h4, h5, h6, a
  ...rest
}) => {
  const slugify = Tag => props => (
    if (!props.id) return <Tag {...props} />

    return (
      <Tag {...props}>
        <a href={'#' + props.id}>
          {props.children}
        </a>
      </Tag>
  )

  return {
    a,
    h1: slugify(h1),
    h2: slugify(h2),
    h3: slugify(h3),
    h4: slugify(h4),
    h5: slugify(h5),
    h6: slugify(h6),
    // A plugin could add itself as a shortcode
    TableOfContents,
    ...rest
  }
}

export default slugifyHeadings

Example usage:

// src/components/layout.js
import componentFactory from 'mdx-toc/react'

export default ({ children }) => (
  <MDXProvider components={componentFactory(myComponents)}>
    {children}
  </MDXProvider>
)

Using a context-based approach for plugins to change rendering would allow for more fine-grained customizability for users. Plugins typically act globally on all MDX documents, however it's probable that this isn't desired when MDX is used for a large site with complex needs.

Something worth exploring is the ability to add some type of getInitialProps or query that can be used to pull in data and statically render. This can be super useful for things like Twitter embeds, oembed, or anything else that only needs to fetch data at compile time.

Drawbacks

This will dramatically add to the complexity of the MDX codebase, but I think it will generally be worth the effort and longterm maintenance. It will allow for richer functionality without having to directly modify core and will add additional features that remark plugins can't necessarily provide as easily.

Debugging could get tricky, too. Plugins will have the ability to drastically change MDX compilation and rendering.

Adoption strategy

This would be an optional feature, but will allow us to provide plugins for common asks that don't make sense adding to core.

Related issues

None that I know of.

Acknowledgements

Thanks to @jxnblk, @wooorm, and @ChristopherBiscardi for discussions on this RFC.

Metadata

Metadata

Assignees

No one assigned

    Labels

    👎 phase/noPost cannot or will not be acted on🙅 no/wontfixThis is not (enough of) an issue for this project

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions