How to setup MDX in Next.js
The what
Next.js is a framework for isomoprhic React apps. It makes it easy to write blazing fast websites, by rendering them on the server first for a speed and SEO boost. The client takes over after, to get the seamless UX of a Single Page App.
Markdown is a minimal markup language. It makes it easy to write!
HTML, the most famous markup language for instance, renders beautifully but is hard to read in raw formā¦ Markdown however, may be parsed into HTML for your browser to render just as beautifully, but even when you see it raw through your human eyeballs, you can tell what is a title, what is a list, what is emphasisedā¦ Itās just brilliant, and itās especially popular with developers.
Its popularity has not stopped there however: Ulysses is a prime example of a slick writing app that has adopted it to great success.
So that ās Markdown, MD! MDX is markdown with JSX support, which pretty much means you can embed React components in your posts!
But why?!
The why
A common use-case would be to render a component for your readers to mess around withā¦ Given this whole site is using Next.js and I just brought in MDX support, I can show rather than tell:
And then even inline the code of the above component:
import {useState} from 'react'
import cn from 'classnames'
export const Demonstration = () => {
const [tapped, setTapped] = useState(false)
return (
<button
type="button"
className={cn(
'shadow-lg rounded p-4 text-center transition duration-300',
{
'bg-teal-200 text-teal-900': tapped,
'bg-purple-600 text-purple-100': !tapped,
},
)}
onClick={() => setTapped(!tapped)}
>
š¤ Tap this React Component š¤
</button>
)
}
Isnāt that cool?!
And if you think itās cool enough to bring in your own Next.js blog, well, youāre in luck, as the next section isā¦
The how
Config
The library weāll be using is next-mdx-enhanced. It is not the official plugin by the Nextjs team, which is workable but has you jumping through hoops to make a āblog indexā page with all your posts, and to get front-matter support.
Front-matter is useful metadata you may put at the front of your .mdx
files and use for things like titles, or tags, or thumbnails and summaries for social share cards.
So first step is to bring that library in!
yarn add next-mdx-enhanced
The plugin uses an .mdx-data
directory for caching, so letās add it to our .gitignore
at this point.
Since Iām putting code snippets in my markdown, and if youāre thinking of using MDX you probably will be too, letās bring in a library that adds classes to style those snippets nicely! I went with rehype-prism:
yarn add @mapbox/rehype-prism
Our goal with those two libraries is to initialise the Next.js
plugin that will sort mdx out for us. In our code weāll do something like:
const rehypePrism = require('@mapbox/rehype-prism')
const mdx = require('next-mdx-enhanced')({
defaultLayout: true,
fileExtensions: ['mdx', 'md'],
rehypePlugins: [rehypePrism],
})
Depending on how youāve set up your Next.js
site, you may or may not have a next.config.js
at the root of your repository. If you do, you will need to pipe that const mdx
to your existing configuration. If you do not, just create an empty file and paste the below.
In either case, I do suggest adding one more library, one that makes using multiple plugins with Next.js
much easier. Itās no worse when youāre using only one, but the moment you use more than one, it really helps keep things readable. So letās bring in next-compose-plugins:
yarn add next-compose-plugins
And hereās how our complete next.config.js
may look like:
const withPlugins = require('next-compose-plugins')
const rehypePrism = require('@mapbox/rehype-prism')
const mdx = require('next-mdx-enhanced')({
defaultLayout: true,
fileExtensions: ['mdx', 'md'],
rehypePlugins: [rehypePrism],
})
// you may tweak other base Next options in this object
// we are using it to tell Next to also handle .md and .mdx files
const nextConfig = {pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx', 'md']}
module.exports = withPlugins(
[
mdx,
// you may add more plugins, and their configs, to this array
],
nextConfig,
)
Content
The config above gets us most of the way there, but of course now we need some MDX to render! So letās create an .mdx
file!
It may be anywhere inside our pages
directory and Next.js
will resolve it as it would a āregularā javascript/typescript file. Iāve created this pages/blog/mdx-ftw.mdx
file, and of course the name content could be anything adhering to the format, but feel free to copy & paste the below:
---
title: MDX FTW
timestamp: 1587386236000
snippet: I got React components to render in my blogposts, and I love it
---
import {Demonstration} from '../../components/blog/Demonstration'
It means two big things:
## I can render React Components
<Demonstration />
## I can render code blocks nicely
```tsx
import {useState} from 'react'
const regularStyles = {
backgroundColor: '#40C9A2',
color: '#E5F9E0',
}
const tappedStyles = {
backgroundColor: '#E5F9E0',
color: '#40C9A2',
}
export const Demonstration = () => {
const [tapped, setTapped] = useState(false)
return (
<div
style={tapped ? tappedStyles : regularStyles}
onClick={() => setTapped(!tapped)}
>
š¤ Tap this React Component š¤
</div>
)
}
```
The properties between the ---
s, the front-matter, can be anything youād want to use in your layout file; their names have no special significance.
Your import
s should resolve to components relative to the .mdx
file, as you might expect! I prefer putting all components that are just used in my blog section in components/blog
, so for this example I created a components/blog/Demonstration.tsx
file, which contains what is also in the code block after it!
With those two files set, you might expect navigating to /blog/mdx-ftw
would just work, but we have one more step: we have to create our default layout!
Layout
When I said the front-matter property names have no special significance it was only mostly true. There is one that does mean something by default to next-mdx-enhanced
: layout
. You may use it to specify which layout the mdx content should be nested in, so you may have a layouts/docs
and a layout/blog
, and even a layout/your-wildest-dreams
according to the occasion.
In our case, weāve also passed the defaultLayout: true
in the plugin config before, so instead of specifying the layout on every blogpost, we may just create a layouts/index.tsx
that everything will be falling back to when there is no layout
property in the front-matter. So letās create that file!
import {ReactNode} from 'react'
import cn from 'classnames'
type FrontMatter = {
title: string
snippet: string
}
export default ({title, snippet}: FrontMatter) =>
({children}: {children: ReactNode}) => (
<div>
<h1>{title}</h1>
<p>{snippet}</p>
<section>{children}</section>
</div>
)
Itās somewhat funky, as the default export has to be a Higher Order Component type of thing, which is soooo 2018; but hopefully it makes some sense: The first method takes all the properties in the front-matter as an argument, and the second has all of the parsed mdx, everything after the front-matter, as children
.
At this point, navigating to /blog/mdx-ftw
will work, check it out!
Great job!
Styling
The mdx content will be there, it will have correctly been transformed to the equivalent html, but, according to how youāve set up your styles, it might be looking a bitā¦ plain. I know I always declare a āstyle resetā at the start of all of my apps, so headers are indistinguishable from paragraphs for example, unless I explicitly style them. This helps me keep an important habit, using different html tags for semantics, not for styling!
So how do we explicitly style the mdx output?
We could configure the plugin with a map for the components to use in each case: give it a Styled Component to use for p
s, a regular component with some css classes from Tailwind.css to use for h2
s, anything weād like.
Or we could do it with CSS modules! Nothing wrong with any approach, but this is the one I find to be the most straightforward. So letās create a layouts/mdx.module.css
that looks something like this:
.container > *,
.container section > * {
margin-bottom: 0.5rem;
}
.container h1 {
font-size: 2.25rem;
}
.container h2 {
font-size: 1.875rem;
}
/* Add your own styles as you fancy them */
And letās import it in our layout file:
import { ReactNode } from 'react'
import styles from './mdx.module.css'
// ...
=> (
<div className={styles.container}>
<h1>{title}</h1>
<p>{snippet}</p>
<section>{children}</section>
</div>
)
This should make our post easier to read! How about we make it a bit moreā¦ shareable, by tweaking its metadata?
Metadata
For this Iām using yet another third party library, Next SEO, as it does take care of a lot of the boilerplate needed to have things like Open Graph attributes that get you pretty cards when sharing on social media. Letās add it to our project with:
yarn add next-seo
And amend /blog/mdx-ftw.mdx
to use it:
import {NextSeo} from 'next-seo'
// ...
export default ({title, snippet}: FrontMatter) =>
({children}: {children: ReactNode}) => (
<>
<NextSeo title={title} description={snippet} />
<div className={styles.container}>
<h1>{title}</h1>
<p>{snippet}</p>
<section>{children}</section>
</div>
</>
)
An image is worth a thousand words, so how would we go about configuring a bespoke image to show depending on the article? I propose nesting all of the blog images into components/blog/images
and creating a components/blog/FeaturedImage.tsx
component that looks like this:
import {NextSeo} from 'next-seo'
import mdxFtw from './images/mdx-ftw.jpg'
import {rootUrl} from '../../lib/constants'
const titlesToImages = {
'MDX FTW': mdxFtw,
}
type Props = {
title: string
}
export const FeaturedImage = ({title}: Props) => {
const imageSrc = titlesToImages[title]
return imageSrc ? (
<>
<NextSeo
openGraph={{
images: [
{
url: `${rootUrl}${imageSrc}`,
width: 2048,
height: 1152,
alt: `Featured image for ${title}`,
},
],
}}
twitter={{
handle: '@aTwitterHandle',
cardType: 'summary_large_image',
}}
/>
<img src={imageSrc} />
</>
) : null
}
rootUrl
must equal to the root of wherever your blog gets deployed to; unfortunately these things will not work with relative urls.
That titlesToImages
map needs to be updated manually with every new post, a nice thing to optimise once youāve got more than a handful! Iām conscious of the length of this article, so I will simply point to [a babel plugin for glob imports](a%20babel%20plugin%20for%20glob%20imports āitās called ābabel-plugin-import-glob-array""); please do let me know and Iāll to go into further detail.
Glob imports would also help us get all of our posts from the pages/blog
directory, if we wanted a page that lists all of them; which we do but weāll make do with the manual approach in the next section. Before that, letās not forget to render our new component in layouts/index.tsx
!
import { FeaturedImage } from '../components/blog/FeaturedImage'
// ...
=> (
<>
<NextSeo title={title} description={snippet} />
<div className={styles.container}>
<h1>{title}</h1>
<p>{snippet}</p>
<FeaturedImage title={title} />
<section>{children}</section>
</div>
</>
)
Should work a treat, and thatās the last weāll be messing with these files!
But even though weāve set up the posts to render for their appropriate URLs, you may only navigate to them directly, only if you know those URL! So letās finish strong by scaffolding an index page that can navigate to all of them!
Creating a blog index
Having /blog
resolve to something is done via the common Next.js way: by defining a pages/blog/index.tsx
which has a React component as its default export.
next-mdx-enhanced
gives us a __resourcePath
along with the front-matter it parses, which weāll use to link to the blog posts.
Given those two pieces of knowledge, letās create a pages/blog/index.tsx
that looks like this:
import Link from 'next/link'
import {frontMatter as mdxFtw} from './mdx-ftw.mdx'
const pages = [mdxFtw]
const formatPath = (p: string) => p.replace(/\.mdx$/, '')
const BlogIndex = () => (
<>
<h1>Blog</h1>
<ul>
{pages.map((page) => (
<li key={page.__resourcePath}>
<Link href={formatPath(page.__resourcePath)}>
<a>
<h2>{page.title}</h2>
<p>{page.snippet}</p>
</a>
</Link>
</li>
))}
</ul>
</>
)
export default BlogIndex
This would have worked, if Typescript had any idea how our current setup resolves .mdx
files! We have to clarify by declaring the *.mdx
module somewhere. Iāve setup my tsconfig.json
to include my global.d.ts
file in the root, where I also specify what .svg
and .jpg
imports resolve to, so this is where Iāll be adding this snippet:
declare module '*.mdx' {
import {ReactNode} from 'react'
export const frontMatter: {
title: string
snippet: string
timestamp: number
__resourcePath: string
// type additional properties according to
// the front-matter you define
}
const component: ReactNode
export default ReactNode
}
After that, Typescript will be happy, the code will transpile, and navigating to /blog
will give us a list of all of our MDX pages. Yep, all one of āthemā, and clicking on āthemā will get us to the complete article!
Again, have a look at babel-plugin-import-glob-array if you donāt fancy updating that pages
array manually, or let me know and Iāll follow up this post with an explanation of how to set that up.
More important at this point would be to style those pages nicely and, hey, how about writing more posts?
But before doing either, make sure to pat yourself in the back: we did it, we set up MDX in our Next.js app! šš¼š„³š