MDX at Scale: The Parser Traps You Hit at Article #47
- MDX
- Next.js
- markdown
- React
- content
- PakSolarInsights
PakSolarInsights grew from 5 articles to over 100 in one sustained content push — each one an MDX file under Next.js App Router (src/app/blog/<slug>/page.mdx). Writing five MDX files teaches you the happy path. Rewriting a hundred teaches you the parser. These are the traps, in the order they found me — as I posted at the time: the content was the easy part.
Trap 1: markdown dies quietly inside JSX
MDX's core promise is interleaving markdown and components. Its core gotcha: markdown syntax inside a JSX element is only parsed as markdown under specific whitespace conditions. Write this —
<div className="callout">
## Key takeaway
* Cheaper per kWh
* 10-year lifespan
</div>
— and depending on blank lines, you get a literal ## Key takeaway rendered as text, list items fused into a paragraph, or a compile error. The failure is per-article-random, because it depends on exactly how each writer (or each AI-assisted draft) spaced the block.
At scale you need a rule that's mechanical, not vibes-based. Two work:
- Close the JSX before the markdown. Let
<div>wrappers wrap only JSX, and put headings/lists back at the top level. - Go full JSX inside JSX. If content must live inside a styled block, write it as
<h3>,<ul>,<li>— HTML is unambiguous to the MDX parser in a way indented markdown never is.
We applied both across the catalog; the rewrite's MDX errors dropped to zero. The general principle: at the markdown–JSX boundary, be all one or all the other. The mixed zone is where the parser's mood lives.
Trap 2: <script dangerouslySetInnerHTML> in a markdown file
Every article carries JSON-LD structured data (BlogPosting, BreadcrumbList, FAQ markup) — which means a <script dangerouslySetInnerHTML={{...}}/> tag. Dropped into a plain markdown MDX file, that expression hits parser limitations and dies confusingly.
The first fix was architectural: move the schema scripts into a sibling layout.tsx per article — real TSX, no parser ambiguity. That works but doubles the files. The cleaner pattern, used across the 100-article pass, is MDX's escape hatch: export a default component from the .mdx file itself.
export default function Page() {
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: schema }} />
{/* article JSX */}
</>
);
}
Inside an exported function, you're in plain JSX land — the MDX document parser never touches the contents. You trade markdown ergonomics for predictability, which at 100 files is the right trade: uniform structure beats pleasant syntax when a build error means bisecting a catalog.
Trap 3: the platform errors you only see in bulk
Two more fell out of touching everything at once:
- React key warnings in the article list — fixed with
key={${article.slug}-${index}}. One warning is ignorable; a hundred articles' worth pollutes every console session and, since the console runs during crawler audits too, it's worth zero tolerance. - CSS spacing vs. injected ads — AdSense inserts ad units between article elements, and default prose margins let text crowd them. Explicit
margin-top: 5remon.prose h2(4rem onh3) keeps the layout compliant no matter where an ad lands. If a third party injects into your flow, your spacing is part of your contract with it.
The meta-lesson
MDX is genuinely the right tool for component-rich articles — but it's a compiler, and at scale you treat it like one: pick mechanical authoring rules (markdown XOR JSX per block, export-default for anything exotic), validate the whole catalog in CI via the build, and never let per-file creativity into the syntax layer. The content strategy those 100 articles served — the SEO and E-E-A-T side — is its own story.