← All articles
Jun 12, 20263 min read

MDX at Scale: The Parser Traps You Hit at Article #47

  • MDX
  • Next.js
  • markdown
  • React
  • content
  • PakSolarInsights
MDX at Scale: The Parser Traps You Hit at Article #47

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:

  1. Close the JSX before the markdown. Let <div> wrappers wrap only JSX, and put headings/lists back at the top level.
  2. 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: 5rem on .prose h2 (4rem on h3) 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.

WRITTEN BY

Shahzaib Muhammad Akram

Senior Frontend EngineerCyberjaya, Malaysia