MDX or Markdown?
TL;DR
When writing a blog with Next.js, there's @next/mdx, which supports the MDX format.
The key features of MDX are:
- You can write JSX within Markdown
- You can import external components
- You can export internal variables
However, for just writing blog articles, these features seem like overkill.
For writing articles alone, you probably don't need that much extensibility. So this time, I'll explain why I chose not to use mdx for writing articles.
In short, the reasons are:
Next.js'sdynamic importdidn't work, making metadata export impossible.- VSCode extension support for MDX is significantly weaker compared to Markdown.
These issues stem from MDX simply being still in development, so they're expected to improve over time depending on adoption trends. However, they're problems right now.
So for this blog, articles are written with the .md extension, and converted to mdx at render time. Also, instead of export, frontmatter is used to express metadata.
Downsides of MDX
What personally concerned me about using MDX for blogging was code like this:
This looks very convenient for building sites. However, it makes it hard to maintain template consistency for articles, and changing something like <BarChart> could easily cause errors, which felt undesirable.
Another reason is that mdx supports things like:
However, when using dynamic import, the exported metadata couldn't be retrieved properly.
I looked at various blogs and the examples looked like this:
It looks like it should work, but in practice it throws errors and doesn't work. So when extracting articles as components, you can't use export, which makes it difficult to handle metadata like dates and titles.
Also, what's frustrating when writing articles is VSCode's MDX extension support. For Markdown, VSCode extensions are well-developed, so you get great completion and linting, making productivity very high. However, MDX completion still felt lacking.
So I want to write blog articles in pure Markdown as much as possible.
Benefits of MDX
One benefit of using MDX with Next.js is Next.js's MDX rendering system. In Next.js, placing an MDX file at pages/posts/hoge.mdx renders it as a page at localhost:3000/posts/hoge. Also, remark and rehype plugins can be applied to MDX just by writing them in next.config.js.
That said, these systems can also be applied to Markdown in Next.js. In other words, just by writing remark and rehype plugins in next.config.js, you can easily extend Markdown and render it as pages. All you need to do is add .md to pageExtensions in next.config.js.
For example, you can support code syntax, KaTeX, and render Markdown like this (reference):
next.config.js
If you were to render Markdown yourself, you'd use react-markdown or embed HTML parsed with remark and rehype via dangerousInnerHTML. In comparison, using @next/mdx felt like a much easier approach.
How to Render
The most straightforward way to render Markdown would look like the following code. Think of it as something like [postId].jsx. Please infer what contentLoader does from its name.
In getStaticPaths, we use the fs module to get a list of markdown files. Then in getStaticProps, we resolve the file path, grab the metadata, and pass the corresponding markdown file and metadata to the Layout.
If you also want to specify the Layout via metadata, you could create metadata like:
and use dynamic to achieve that as well.
At this point I thought, "This is basically all we need," but I couldn't create a sidebar. I wanted to extract headers and build a sidebar from them.
So the approach I came up with was creating a custom remark loader.
Using remark-mdx, a Markdown file is parsed roughly as follows.
A simple Markdown file with frontmatter is converted to an MDAST like this:
Using remark-frontmatter, the frontmatter part becomes accessible as children with type === yaml. Also, headers can be found by looking for type === headings. In other words, by parsing the MDAST, you can extract the frontmatter and headers as metadata.
Then, import the component specified in layout at the top of the file, export the metadata, and export the imported component.
In other words, we transform frontmatter-containing Markdown like above into:
This way, just by placing a markdown file, it gets interpreted as MDX with a custom component and rendered.
Furthermore, since the component receives metadata that includes header information, you can build sidebars and table of contents on the JSX side. If refactoring is needed, it seems like most changes can be handled on the custom loader side, which feels elegant. Another benefit of this approach is that if you suddenly want to add something like:
you can just do it. Since parsing follows the mdx spec, writing in MDX format will be automatically supported.
I personally think this is a great approach, but since nobody else seems to take this approach, I'm a little worried. Maybe there's some issue I'm not seeing (I'd love to know if there's a better approach).