Syntax highlighting in Gatsby with prism-react-renderer & MDX
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.
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.