Why does gatsby-remark-prismjs not work with MDX?

gatsby-remark-prismjs is the PrismJS plugin recommended by the official Gatsby docs for adding syntax highlighting; however, it is only compatible with gatsby-transformer-remark which is a transformer for Markdown not MDX. Hence, you will have compatability issues if your site uses MDX.

Theoretically gatsby-remark-prismjs can work with MDX using a solution like the one suggested here; however, I could not get this to work.

How to use PrismJS with Gatsby and MDX in 2023

TLDR; Intercept MDX code blocks before they’re rendered, and re-route the code into a custom <Code /> component that applies the appropriate PrismJS identifiers for syntax highlighting.

1. Install prism-react-renderer

npm install --save prism-react-renderer

prism-react-renderer is a popular React plugin for PrismJS. It’s what will be doing the actual syntax highlighting.

Note: At the time of writing I was using prism-react-renderer 1.3.5 and Gatsby 5. If you are having issues with this implementation, try dropping the prism-react-renderer version. I will update this blog post when I have a chance to upgrade to prism v2.

2. Create a code highlight React component

This React component is similar to the default implementation provided by prism-react-renderer, except it adds a wrapper (.gatsby-highlight) for CSS styling similar to LekoArts implementation.

./src/components/utils/code.js

import React from "react"
import Highlight, { defaultProps } from "prism-react-renderer"
import theme from 'prism-react-renderer/themes/github'

const Code = ({ codeString, language, ...props }) => (
  <Highlight {...defaultProps} code={codeString} language={language} theme={theme}>
    {({ className, style, tokens, getLineProps, getTokenProps }) => (
      <div className="gatsby-highlight" data-language={language}>
        <pre className={className} style={style}>
          {tokens.map((line, i) => (
            <div {...getLineProps({ line, key: i })}>
              {line.map((token, key) => (
                <span {...getTokenProps({ token, key })} />
              ))}
            </div>
          ))}
        </pre>
      </div>
    )}
  </Highlight>
)

export default Code

Note: You can replace the theme import with any prism-react-renderer theme

3. Create a utility to detect <pre><code> blocks in MDX

Create a helper utility to detect <code> blocks wrapped with <pre> tags. This allows us to selectively highlight only code blocks, and leave non-code pre tags alone.

./src/utils/pre-to-code-block.js

exports.preToCodeBlock = preProps => {
  if (
    preProps.children &&
    preProps.children.props &&
    preProps.children.type === "code"
  ){
    const { children, className } = preProps.children.props;
    return {
      codeString: children.trim(),
      language: className && className.split("-")[1],
    };
  }
  return undefined;
};

This is a modification of Christopher Biscardi’s mdx-utils implementation. I changed the variable assignments to work with the current way Gatsby structures MDX code tags. I should probably also blindly pass any additional props present in preProps.children.props in case there are any, but this works for now.

4. Create a render wrapper

Create a render wrapper that runs whenever Gatsby compiles MDX. This logic tells the MDXProvider component to render our custom <Code /> component whenever it detects a <pre> tag with <code> inside.

./wrap-root-element.js

import React from 'react'
import { MDXProvider } from '@mdx-js/react'
import { preToCodeBlock } from './src/utils/pre-to-code-block'
import Code from './src/components/utils/code'

const components = {
  pre: preProps => {
    const props = preToCodeBlock(preProps)
    if (props) { return <Code {...props} /> }
    return <pre {...preProps} />
  },
  wrapper: ({ children }) => <>{children}</>,
}
export const wrapRootElement = ({ element }) => (
  <MDXProvider components={components}>
    {element}
  </MDXProvider>
);

5. Tell Gatsby to use our wrapper

Add the following code to gatsby-browser.js and gatsby-ssr.js (if you don’t have their files already, create them in your root directory).

import { wrapRootElement as wrap } from './wrap-root-element'
export const wrapRootElement = wrap

6. Custom CSS

Because we added our .gatsby-highlight wrapper from step 1 we can target these code blocks and add custom styles. See this article for examples.

Credits

This article does not represent a novel implementation of PrismJS within Gatsby, I referenced many blog posts and GitHub threads to arrive at this working solution. LekoArts’ articles were the closest to a working solution, and much of what you see above was copied from their work; however, I had to update their implementation for compatability with Gatsby & MDX in 2023.