MDX vs Plain Markdown for Dev Blogs: When the Complexity Earns Its Keep
MDX is a genuinely good idea. The pitch is clean: write Markdown for prose, drop in React components when you need interactivity, and get both in a single file. For documentation sites and component libraries, it delivers. For a dev blog with posts that are mostly words and code blocks, it’s usually a complexity tax you pay every day for a benefit you use once a month.
That’s not a dismissal. It’s a threshold question: does your content actually need JSX inside the post file? If it does, MDX is the right tool. If it doesn’t — and for most dev bloggers, it doesn’t — you’re carrying build-pipeline weight for capabilities you never deploy.
What MDX Actually Costs
The promise of MDX is seductive, so it’s worth being concrete about what you’re buying when you add it.
A heavier build pipeline. MDX needs to be compiled. That means @mdx-js/mdx or the framework adapter, a Babel or esbuild transform, and often an updated TypeScript config. In Astro it’s a first-class integration; in Next.js you’re reaching for @next/mdx or next-mdx-remote (which are different enough to cause confusion). In either case, your build now has a JSX compilation step that didn’t exist when you were shipping plain Markdown.
Slower local dev on large post archives. MDX files go through the JS bundler. A blog with 50+ posts will notice. Contentlayer (now abandoned), the old Next.js MDX setups, and even Astro’s integration all add latency to the dev server’s HMR cycle as your archive grows. Plain Markdown parsed by remark is fast and stays fast.
Fragility at the boundary. Markdown is forgiving. JSX is not. A stray < in a code snippet, an unclosed component tag, a prop type mismatch — any of these throws a build error in MDX. In plain Markdown, they’re usually rendered literally. The brittleness is manageable but it’s real, and it lands in the worst place: inside content files maintained by writers or your future self at midnight.
Import churn in every post. Once you’re in MDX, components live alongside the content. Every post that wants a callout box, a chart, or an interactive demo needs to import it. That’s a line of code per component per post. Refactor the component path and you’re updating dozens of files.
None of these are showstoppers. They’re friction. And friction is only worth carrying if the capability you unlock is genuinely something you use.
The Capabilities MDX Unlocks
Let’s be honest about what MDX actually gives you that plain Markdown doesn’t.
JSX components inline in prose. This is the core feature. You can write <InteractiveDemo /> in the middle of a post paragraph and the demo renders there, wired to React state, responding to user input. No iframe, no separate page, no workaround.
Props on content blocks. You can pass data directly: <Chart data={someImport} />, <Callout type="warning">, <CodeSandbox id="abc123" />. The component receives typed props, not just raw HTML attributes.
Shared layout components per post. Posts can override their layout, pull in specialized navigation, or render differently based on data in the file. Useful for docs, less useful for a blog where every post looks the same.
If your content archive includes live coding playgrounds, interactive data visualizations, embedded component demos, or tutorials where the reader manipulates state to learn — MDX earns every ounce of its weight. That’s what it was designed for.
What Remark, Rehype, and Shortcodes Actually Cover
Here’s the underappreciated truth: most of what people reach for MDX to accomplish can be done at the Markdown level with remark/rehype plugins or a framework’s native component slot pattern — without touching JSX in post files.
Callouts and custom containers. The remark-directive plugin adds a syntax for custom block-level and inline containers:
:::warning
This behavior changed in v4.
:::
One plugin, zero JSX, renders whatever React component you wire to the warning directive. Works in every Markdown post without an import.
Syntax highlighting. Shiki or Rehype Pretty Code runs at build time over fenced code blocks. No <CodeBlock language="js">, no component import. The plugin intercepts every code block and outputs pre-tokenized HTML with your theme baked in.
Embed patterns. Custom rehype plugins can intercept link syntax and transform YouTube URLs, GitHub gists, CodeSandbox URLs, or tweet links into their embed HTML. The author writes [embed](https://youtube.com/watch?v=xyz) and the build outputs an iframe. Clean in the Markdown file, zero coupling to React.
Astro’s Content Collections with component slots. In Astro specifically, you can inject components into MDC content at the layout level rather than the post level. Your [post].astro layout receives the compiled content and can inject components around it. Posts stay as .md files.
These tools cover callouts, syntax highlighting, embeds, image optimization, table of contents generation, and more — the full checklist of “reasons devs reach for MDX” — without requiring a JSX compilation step inside the content layer.
The Honest Threshold
There’s a clean way to decide. Ask: does this post need stateful interaction that lives inside the prose?
A callout box is not stateful. A code block is not stateful. An embedded video is not stateful. A table of contents is not stateful. These are presentation concerns the build pipeline handles.
A live code playground where the reader edits JavaScript and sees output? That’s stateful. A component demo where the user toggles props? Stateful. An interactive chart where hovering over a bar filters a table elsewhere on the page? Stateful.
If the answer is yes — if you’re authoring learning content, component documentation, or interactive tutorials — MDX is correct. Use it.
If the answer is no — if you’re writing explanatory prose with code snippets, opinion pieces, and how-tos — you’re paying MDX’s compile-time and fragility cost for something remark plugins handle at build time.
Most dev blogs are in the second category. The “rich content” they want is syntax-highlighted code, callout boxes, and the occasional embedded demo. That’s a remark-directive install and a Shiki integration, not a JSX transform in every post file.
The Migration That Never Happens
One more thing worth naming: the MDX migration that sits in TODO.md forever.
A common pattern is a blog that started in plain Markdown, hit one post that needed a live demo, added @next/mdx, and then left every other post as .md with no actual MDX in it. The setup is now bifurcated — some posts in .md, some in .mdx, the build handling both, the team occasionally confused about which format to use for a new post — and there’s exactly one interactive demo in the whole archive.
That’s MDX at its worst: complexity installed for a one-off that could have been an iframe or a CodeSandbox embed link.
If you’re in this situation, it’s worth asking whether the one truly interactive post can move to a standalone page rendered separately, with the post linking to it. The post stays .md. The demo lives at /demos/my-demo. The blog build stays simple. You link between them.
This doesn’t always work. If the interactivity needs to be embedded in the prose for the tutorial to land — if “run this right here, see the output, keep reading” is the learning arc — then you need MDX in that post. Fine. Use it for that post. The question is whether you need it for the hundred other posts too.
The Pattern That Scales
Here’s what a sensible setup looks like for a dev blog that occasionally needs rich content:
-
Default to plain Markdown. Every post is
.md. Remark and rehype plugins handle callouts, code highlighting, embeds, and image optimization at build time. The content layer is fast and forgiving. -
Add
remark-directiveearly. This gives you a syntax for custom containers (:::warning,:::callout) that map to components. Cover 80% of “rich content” needs without touching JSX. -
Reach for MDX only when you need client-side state inside the post. Not for every post. For the specific post that ships an interactive demo. That post can be
.mdx; everything else stays.md. Most frameworks handle mixed.md/.mdxin the same content directory. -
For standalone interactives, consider a separate route. A live demo at
/demos/sort-visualizeris often a better experience than a demo embedded mid-post. It loads its JS independently, has its own URL to share, and doesn’t force MDX on every post page.
The goal is matching the tool to the actual content. MDX is a great tool. It’s just not the right default for a blog where 95% of posts are prose and code — and treating it as a default is how you end up maintaining a complex build pipeline for the privilege of a syntax you never use.
If your next post needs a live playground, open an .mdx file. If it needs a callout box, reach for a remark directive. The difference in complexity between those two choices is not subtle.