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 Scanlon (@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