Syntax highlighting with Gatsby, MDX, Tailwind and Prism React Renderer
In this post I’ll explain how I use the excellent Prism React Renderer by Formiddable to add syntax highlighting to code blocks using Tailwind and Tailwind Typography, using both MDX 1 and MDX 2 with the help from gatsby-plugin-mdx.
The complete examples can be found on the following links
Branch: main » MDX 1
Branch: refactor/mdx-2 » MDX 2
MDX 1
This guide is for v3 of the Gatsby MDX Plugin: gatsby-plugin-mdx
: ^3.0.0
which is designed to work with MDX 1.
Tailwind Install
I’m going to assume you already have Tailwind and Tailwind Typography installed and configured. If you haven’t, have a look at the Tailwind installation guides.
Prose
To enable prose
and Typography settings across the entirety of my example site I’ve added the necessary Tailwind
classes to an HTML elment that wraps the whole Gatsby site and created a RootElement
component. The src
for this
component can be found here:
root-element.js
This component looks a little something like this.
// src/components/root-element.js
import React from 'react';
const RootElement = ({ children }) => {
return <main className='mx-auto px-2 sm:px-4 prose prose-sm sm:prose'>{children}</main>;
};
export default RootElement;
I then use the RootElement
component in both gatsby-browser.js
and gatsby-ssr.js
like this.
// gatsby-browser.js
import React from 'react';
import RootElement from './src/components/root-element';
import './src/styles/global.css';
export const wrapRootElement = ({ element }) => {
return <RootElement>{element}</RootElement>;
};
// gatsby-ssr.js
import React from 'react';
import RootElement from './src/components/root-element';
export const wrapRootElement = ({ element }) => {
return <RootElement>{element}</RootElement>;
};
MDXProvider | MDX 1
The location of your MDXProvider
will entirely depend on where in your project you’ve used it. On the
main branch in my example repo I’ve added it to the
collection route responsible
for rendering my .mdx
files: You can find the src
code here:
{mdx.fields__slug}.js.
The main thing to draw your attention to is the use of the components
prop. This is responsible for handling
transformations of HTML tags or React components found in any MDX file.
// src/pages/posts/{mdx.fields__slug}.js
import React from 'react';
import { graphql } from 'gatsby';
import { MDXProvider } from '@mdx-js/react';
import { MDXRenderer } from 'gatsby-plugin-mdx';
import PrismSyntaxHighlight from '../../components/prism-syntax-highlight';
const components = {
code: ({ children, className }) => {
return <PrismSyntaxHighlight className={className}>{children}</PrismSyntaxHighlight>;
},
};
const Page = ({
data: {
mdx: { body },
},
}) => (
<MDXProvider components={components}>
<MDXRenderer>{body}</MDXRenderer>
</MDXProvider>
);
export const query = graphql`
query ($id: String) {
mdx(id: { eq: $id }) {
body
}
}
`;
export default Page;
In the case of syntax highlighting you’ll want to transform any HTML code
elements into a “styled code block”. You
achieve this using the components
prop. You’ll see in the components
object I’ve added a key for code
and
destructured both the children
and className
from the MDX props.
I pass the className
on to the <PrismSyntaxHighlight />
component via a prop called className
and render the
children
as children of the <PrismSyntaxHighlight />
component.
Components | MDX 1
// components object
const components = {
code: ({ children, className }) => {
return <PrismSyntaxHighlight className={className}>{children}</PrismSyntaxHighlight>;
},
};
PrismSyntaxHighlight
The next step is to create the <PrismSyntaxHighlight />
component. The below is the default setup that I’ve lifted
straight from the repo: Usage.
Default Highlight
This example will work just fine but you’ll need to make a few changes in order to pass the language from MDX. I also made a few more changes so I could more easily style the code using Tailwind.
import React from 'react';
import Highlight, { defaultProps } from 'prism-react-renderer';
const PrismSyntaxHighlight = ({ children, className }) => {
return (
<Highlight {...defaultProps} code={, className} language="jsx">
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={className} style={style}>
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
);
};
export default PrismSyntaxHighlight;
Custom Highlight
The src
for my completed version can be found here:
prism-syntax-highlight.js,
but it looks similar the below.
// src/components/prism-syntax-highlight.js
import React from 'react';
import Highlight, { defaultProps } from 'prism-react-renderer';
import theme from 'prism-react-renderer/themes/dracula';
const PrismSyntaxHighlight = ({ children, className }) => {
const language = className.replace(/language-/gm, '');
return (
<Highlight {...defaultProps} code={children} language={language} theme={theme}>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<code className={className} style={style}>
{tokens.slice(0, -1).map((line, i) => (
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</code>
)}
</Highlight>
);
};
export default PrismSyntaxHighlight;
Here are the changes in a little more detail.
Language
In order to pass the language from MDX on to the language
prop it’s necessary to remove the word “language from the
MDX className
. I accomplished this using a Regular Expression (RegEx).
const language = className.replace(/language-/gm, '');
The new language
const can now be passed on to the <Highlight />
component via the language
prop.
- <Highlight {...defaultProps} code={children} language="jsx">
+ <Highlight {...defaultProps} code={children} language={language}>
Theme
Prism React Renderer comes with a few themes, you can find them by having a look at the repo: prism-react-renderer/src/themes/. I chose to use “dracula”.
+ import theme from 'prism-react-renderer/themes/dracula';
- <Highlight {...defaultProps} code={children} language={language}>
+ <Highlight {...defaultProps} code={children} language={language} theme={theme}>
Code
I chose to return a code
element rather than the pre
element from the Default Highlight example so I could more
easily style it using Tailwind.
- <pre className={className} style={style}>
+ <code className={className} style={style}>
...
+ </code>
- </pre>
Slice
At this point everything was looking great. However, I noticed a spurious extra empty line at the bottom of each code block. Using Array.prototype.slice() did allow me to remove it, but I haven’t tested every scenario, so this might not be a good idea. 😬
<code className={className} style={style}>
- {tokens.map((line, i) => (
+ {tokens.slice(0, -1).map((line, i) => (
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</code>
Tailwind
In addition to styling code that appears in a “code block” you’ll probably also need to style inline code. The src
for
the complete Tailwind config can be here:
tailwind.config.js
Inline code
The first thing I noticed was that “inline” code was displaying the backtick around the code. E.g. `code`. To remove
this I added the following to tailwind.config.js
// ./tailwind.config.js
module.exports = {
...
theme: {
...
extend: {
typography: (theme) => ({
DEFAULT: {
css: {
code: {
'&::before': {
content: '"" !important'
},
'&::after': {
content: '"" !important'
}
}
}
}
})
}
},
plugins: [require('@tailwindcss/typography')]
};
MDX 2 (RC Version)
This guide is for v4 of the Gatsby MDX Plugin: gatsby-plugin-mdx
: 4.0.0-rc.3
which is designed to work with MDX 2.
The main changes for using MDX 2 are that body
and the <MDXRenderer />
component are no longer required. There are
no Tailwind changes required.
Here’s a diff of {mdx.fields__slug}.js
// src/pages/posts/{mdx.fields__slug}.js
import React from 'react';
import { graphql } from 'gatsby';
import { MDXProvider } from '@mdx-js/react';
- import { MDXRenderer } from 'gatsby-plugin-mdx';
import PrismSyntaxHighlight from '../../components/prism-syntax-highlight';
const components = {
code: ({ children, className }) => {
- return <PrismSyntaxHighlight className={className}>{children}</PrismSyntaxHighlight>;
+ return className ? (
+ <PrismSyntaxHighlight className={className}>{children}</PrismSyntaxHighlight>
+ ) : (
+ <code>{children}</code>
+ );
}
};
const Page = ({
data: {
- mdx: { body }
},
+ children
}) => (
<MDXProvider components={components}>
- <MDXRenderer>{body}</MDXRenderer>
+ {children}
</MDXProvider>
);
export const query = graphql`
query ($id: String) {
mdx(id: { eq: $id }) {
- body
}
}
`;
export default Page;
Changes | MDX 2
Here are the changes in a little more detail.
Dependencies
These are the dependencies used in the example repo in the refactor/mdx-v2 branch, you can see them listed here in package.json.
// package.json
"dependencies": {
"@mdx-js/react": "^2.1.2",
"gatsby": "4.20.0-mdxv4-rc.65",
"gatsby-plugin-mdx": "4.0.0-rc.3",
"gatsby-source-filesystem": "4.20.0-mdxv4-rc.124",
...
}
MDXRenderer
The MDXRenderer is no longer required so this can be removed all together, and instead of querying mdx.body
you can
pass children
directly to the <MDXProvider >
component.
Components | MDX 2
I’ve added a ternary condition to the to catch any “code” passed that doesn’t have a class name. Inline code for
instance won’t have a language
defined at the start of the code fences. If that’s the case I return an HTML <code />
element rather than the <PrismSyntaxHighlight />
component.
And that’s about it!
General Availability (GA)
The new MDX Plugin isn’t quite ready yet but the team have been doing such a super job, and I’m so excited by it, that I thought I’d share how I’ve been using it.
I don’t expect there to be any further major changes in the way you use it, so once it hits GA this example should still work. If that turns out not to be the case I’ll circle back to this post, and update it.
I recently implemented @FormidableLabs excellent Prism React Renderer on my site.
— Paul 🇬🇧 (@PaulieScanlon) August 8, 2022
Here's a quick post for how to use it with the @GatsbyJS @mdx_js plugin, and a pinch of @tailwindcss
There's also an example for the RC version which supports MDX 2 🎉https://t.co/rawrgirGKT