MDX previews in Netlify CMS

July 23, 2020

MDX is an incredible toolkit that allows you to write JSX in your Markdown files; creating opportunities for more dynamic and interactive experiences in your content.

What’s even better than the technology itself, is the ability it provides content-contributors to work alongside these advanced components within the articles they’re crafting. When coupled with GUI interfaces like Netlify CMS, we’re able to provide an abstraction-layer for whatever technology we’re using.

Technology is only as good as the simplicity it introduces.

The problem

While Netlify CMS provides a wealth of features upfront that makes setup and creating collection-types easy, an assumption it doesn’t make is within its preview-panel.

Netlify CMS: Before
Netlify CMS: Before

This normally wouldn’t be a huge deal. We’re able to see the fields and parsed Markdown - albeit rough. But, as we look closer at the article, we can see some MDX components that either don’t show anything at all or present a vastly different experience than what would be expected within the preview panel.

The solution

Luckily, Netlify CMS is pluggable, and allows us to hook into and generate our own previews per collection-type:

cms.js
js
1import CMS from 'netlify-cms-app'
2
3... // CMS.init() functionality
4
5CMS.registerPreviewTemplate('posts', withEmotion(ArticlePreview))

In my case, I also wanted to wrap the iFrame that powers the preview and utilize Emotion, so a HOC was created to target and provide that scope:

withEmotion.js
js
1import { CacheProvider, Global } from '@emotion/core'
2import createCache from '@emotion/cache'
3import weakMemoize from '@emotion/weak-memoize'
4import { ThemeProvider } from 'emotion-theming'
5import { theme } from 'chaoskit/src/assets/styles/theme'
6import { globalStyles } from 'chaoskit/src/assets/styles/global'
7import { Container } from 'chaoskit/src/components'
8
9const memoizedCreateCacheWithContainer = weakMemoize((container) => {
10 const newCache = createCache({ container })
11
12 return newCache
13})
14
15export default (Component) => (props) => {
16 const iframe = document.querySelector('#nc-root iframe')
17 const iframeHeadElem = iframe && iframe.contentDocument.head
18
19 if (!iframeHeadElem) {
20 return null
21 }
22
23 return (
24 <CacheProvider value={memoizedCreateCacheWithContainer(iframeHeadElem)}>
25 <ThemeProvider theme={theme}>
26 <Global styles={[globalStyles(theme)]} />
27 <Container
28 css={{
29 paddingTop: theme.space.base,
30 paddingBottom: theme.space.base,
31 }}
32 >
33 <Component {...props} />
34 </Container>
35 </ThemeProvider>
36 </CacheProvider>
37 )
38}

Finally, we are ready to tackle the preview render:

MDXPreview.js
js
1import MDX from 'mdx-scoped-runtime'
2
3const MDXPreview = ({ entry }) => {
4 const theme = useTheme()
5
6 return (
7 <MDX
8 components={
9 {
10 // DOM element + React component overrides
11 }
12 }
13 >
14 {entry.getIn(['data', 'body'])}
15 </MDX>
16 )
17}
18
19MDXPreview.propTypes = {
20 entry: PropTypes.object.isRequired,
21}
22
23export default MDXPreview

mdx-scoped-runtime comes to the rescue with the ability to parse MDX on-the-fly. We can then use this component alongside any other fields we want to display within our preview.

ArticlePreview.js
js
1import MDXPreview from './MDXPreview'
2
3const ArticlePreview = ({ entry }) => <MDXPreview entry={entry} />
4
5ArticlePreview.propTypes = {
6 entry: PropTypes.object.isRequired,
7}
8
9export default ArticlePreview

Netlify CMS: After
Netlify CMS: After

Gatsby tip

For scenarios where you may have components that use a bit more Gatsby magic than what mdx-scoped-runtime can provide; like graphql, you can override the default behavior to provide a fallback component as needed to avoid compilation errors:

MDXPreview.js
js
1import MDX from 'mdx-scoped-runtime'
2
3const UnsupportedComponent = ({ label, ...rest }) => (
4 <div {...rest}>
5 <code>{label}</code> requires a bit more magic than we are able to display
6 in the CMS.
7 </div>
8)
9
10UnsupportedComponent.propTypes = {
11 label: PropTypes.string.isRequired,
12}
13
14const MDXPreview = ({ entry }) => {
15 const theme = useTheme()
16
17 return (
18 <MDX
19 components={{
20 SuperSpecialGatsbyComponent: () => (
21 <UnsupportedComponent label="SuperSpecialGatsbyComponent" />
22 ),
23 }}
24 >
25 {entry.getIn(['data', 'body'])}
26 </MDX>
27 )
28}
29
30MDXPreview.propTypes = {
31 entry: PropTypes.object.isRequired,
32}
33
34export default MDXPreview
Copyright © 2020 Zach Schnackel. Penalty is 🔥