This site (now) uses MDX
Some time last year I stumbled across MDX while looking for way too much tooling around Markdown-to-HTML conversion, to run in a React app.
What is MDX?§
It's a superset syntax to allow Markdown to support JSX. Read here for more. At first, this might seem superfluous since Markdown already is decently capable. But the need for more becomes apparent when trying to use Markdown to author blog posts. Even more so for a technical blog such as anything involving code.
Credit: Logan Weaver
Why bother? (the priorities)§
MDX might seem like a heavy-handed solution for a blog. It's a bit more to setup than just a Wordpress or Squarespace site, or even the Markdown solution I had previously. There's also the question of the content being portable, as technologies and front-end tools shift and change.
On the first point, my intention for this blog is to share technical information alongside the basic prose, as much as I can. So...documentation, code snippets, images, references, and there was always a far-reaching question for interactivity. There are quite a few subtle nuances that turn into bigger problems when trying to do those things with plain Markdown.
On portability, other tools like Gatsby or Remix, either have a healthy enough plugin ecosystem, or support JSX well-enough, that there is ample flexibility. Additionally, I was never able to find a site-builder (Wix, Squarespace, etc.) that had ample tools available for such technical content.
Optimized images & syntax highlighting support§
<Image>
Speaking of those subtle nuances that turn into big problems, embedding
images in these posts has been an issue I've been escaping for a while.
I settled on just using manual <img />
tags directly in Markdown for
a while, since HTML is also valid Markdown. (First layer to the problem,
Markdown is only intended for prose.)
And I seemed pretty well covered in this reasoning, since straight from daringfireball, we have:
For any markup that is not covered by Markdown’s syntax, you simply use HTML itself.
This worked until Next.js introduced a default lint config in v11,
which (at least for me)
stopped allowing regular <img />
elements to make it to production
builds. I tried bypassing this with my own lint config, but nothing worked.
So, I was left to find a way to use Vercel's new <Image />
component
in my Markdown-authored posts. It's objectively a better option than
plain <img />
tags due to its built-in image optimizations, but getting
it into my posts was an issue I couldn't escape anymore.
Docs: https://nextjs.org/docs/api-reference/next/image
<SyntaxHighlighter>
Ah, syntax highlighting. I'll admit; I don't like working on this one. It typically leaves me desperately scratching for some copy/paste solution to just get the language recognition and highlighting out of the way. And even though I don't have it worked out perfectly yet, at least now it can be handled in a React component instead of done via remark plugins half-way between code and config files.
GitHub: https://github.com/react-syntax-highlighter/react-syntax-highlighter
A brief example§
For instance, I can do stuff like this now.
I was able to start with the component from
next/image
,
and wrap a few things of my own around it. Doing all of this in a
React component means I can use this while authoring posts just
as easily as the <Image />
.
Then I'm able to share the code here, using the
<SyntaxHighlighter/>
😎
import Image from 'next/image';
export default function CenteredImage({ src, height, width, alt, caption, imageCredit, imageCreditLink }) {
return (
<div className={utils.center}>
<div>
<Image src={src}
height={height}
width={width}
alt={alt} />
</div>
{caption ? (<p>
<i>
This is a caption - {caption}
</i>
</p>) : ''}
<p>
<i>Credit: <a href={imageCreditLink}>{imageCredit}</a></i>
</p>
</div>
);
}
How is it setup?§
This isn't a full tutorial, just an explanation of what I landed on. I'll write up a step-by-step tutorial in a later post.
next-mdx-remote + @next/mdx§
I couldn't find a solution that provided (a) MDX support across the whole
site and top-level pages, and (b) import()
statement use in MDX
without extra boilerplate code. So, unsure if this is the most correct
solution, but I settled on a combination of the next-mdx-remote
and
@next/mdx
packages.
I followed this blog post from Ebenezer Don
for setting up next-mdx-remote
, and this one
from the Next.js blog to integrate @next/mdx
.
The blog post from Next.js doesn't quite cover everything
for @next/mdx
so check out the example app by running the command below.
npx create-next-app --example with-mdx with-mdx-app
A slight difference§
Many tutorials just choose one option or another and leave it at that.
Taking a shot on this strategy though, my top-level pages in the Next.js
app can be .mdx
files directly (_think pages like about.js
, contact.js
can now be about.mdx
, contact.mdx
). Further, @next/mdx
supports
import()
directly in MDX, but using the .mdx
files on
dynamic routes proved tricky so, the blog posts are rendered via
next-mdx-route
.
An extra dependency isn't great, but it beats extra config code I'm going to forget in 6 months. 🤷♂️
I tried to avoid this weird pattern for using components in MDX that
pops up with packages like next-mdx-remote
and mdx-bundler
,
but I find it an acceptable compromise for the time being.
This is a snippet from the dynamic route performing the render for each post. See
the <MDXRemote />
component? The {...mdx}
prop is our actual MDX content,
but we are forced to pass in each component used. And this isn't for each
individual post, but for all posts across the site. I call this the
"bag of holding" pattern, lol.
Over time I'll likely settle on a set of components that handle the majority of what I want to do in any post. However, for anything special or new, it means we're tightly coupled and need to make boilerplate updates for new component functionality. It isn't the end of the world though, and I'm sure playing with a few things can alleviate some of this.
const Post = (props) => {
return (
<Layout>
<article className={postStyles.content}>
<MDXRemote {...mdx} components={{ Button, Image, CenteredImage, SyntaxHighlighter }} />
</article>
</Layout>
);
}
If you want to read a play-by-play of my issues, see here: [GH issue]
- Images aren't showing in posts anymore: https://github.com/meddlin/rushinglabs-blog-nextjs/issues/59
- Convert posts...dynamic routing: https://github.com/meddlin/rushinglabs-blog-nextjs/issues/60
- broken "successful" deploy: https://github.com/meddlin/rushinglabs-blog-nextjs/pull/65
Autoring tools, potential issues§
MDX doesn't come without snags, so I want to briefly share some issues that have popped up early on.
SyntaxHighlighter§
As mentioned before, the <SyntaxHighlighter>
component has some issues.
Often it's around the caret mark (`) and newline/carriage returns. There's
also some extra complication introduced by trying to use different themes
for this highlighting. And this isn't a
detriment to the package, any sort of syntax parsing is a complex feature.
Components can be weird§
Using components in MDX is great, but will still require me to reference component's code until I get used to using them. Things like: "Am I using this correctly?", "Am I missing a prop?", and components used in the MDX need to first be registered in the parent component before they are available. But I think these are more adjustments to be aware of rather than true long-term issues.
MDX Preview extensions (in VSCode)§
While everything can work completely fine between your Next.js app and MDX, that doesn't mean the "MDX preview"
extensions in the VSCode marketplace understand how your app is structured. Since I'm using <CenteredImage>
in my .mdx
, but the actual component is passed in via the parent component the extension doesn't know about it.
Thus, if I want a preview open while I write blog posts, I need the site running locally with a browser window
open to see it.
These are just the issues I've encountered upon making this switch initially. I still would rather have the benefits of MDX over plain Markdown alone, and I trust each of these can be alleviated over time with some clever solutions.
Why bother? (the nice-to-haves)§
Here's a few of the items I think would be good "nice to haves" with MDX. Sure, other solutions exist, but seeing these become possible all on a single developer's website...that's a bit of the hidden agenda here.
-
Better media embeds & authoring
- I would like to have more pre-made options for off-setting images to the left and right of text, and enabling text wrapping. (insert image of it here)
- It would also be nice to have a little more control around embedding YouTube videos, and other media on the site.
- Pre-built components for drop-downs, table of contents, and similar utilities would be nice too.
-
Better references
- I often do a bit of research while writing my blog posts, and I try to share as much of the material I come across in a "References" section below the post. I've done this with plain Markdown, and it's fine but a little plain. So, I'd really like to build component to display these in a consistent way across each post.
-
Interactive spaces via Sandpack (from Code Sandbox)
- https://codesandbox.io/post/sandpack-announcement
- The only thing better than syntax highlighting (IMO) is adding interactivity directly into the explanations.
-
Future projects
- There are some ideas I want to experiment with on this blog, and I feel MDX is the good foundation needed to get them done.
- Interactive code tutorials for self-made programming courses
- Software project documentation with animations and code examples built-in
- Textbooks as MDX
- Integrated data visualizations through tools like Pyodide
- Experiment with transforming application threat models, stored as text, and converted to MDX
References§
-
Image credit—Logan Weaver: https://unsplash.com/photos/LHDQawgNS_I
-
Markdown intended for prose, daringfireball: https://daringfireball.net/projects/markdown/syntax#html
-
Next.js ESLint config, Vercel - Next.js: https://nextjs.org/docs/basic-features/eslint
-
GH issue, images stopped working: https://github.com/meddlin/rushinglabs-blog-nextjs/issues/59
-
Image optimization, Next.js: https://vercel.com/docs/concepts/next.js/image-optimization
-
Image component, Next.js: https://nextjs.org/docs/api-reference/next/image
-
next-mdx-remote
tutorial, Ebenezer Don: https://blog.jetbrains.com/webstorm/2021/10/building-a-blog-with-next-js-and-mdx/ -
Example app for
@next/mdx
: https://github.com/vercel/next.js/tree/canary/examples/with-mdx -
@next/mdx
package: https://www.npmjs.com/package/@next/mdx -
next-mdx-remote
package: https://github.com/hashicorp/next-mdx-remote