Upgrading to next-mdx-remote v6 silently stripped all array and object literal props from JSX components in MDX. StepList, Checklist, and LessonObjectives rendered empty. Resolved in 41 minutes.
2026-05-12
ai-execution-lab
Impact
All MDX components using array or object literal props rendered empty after upgrading next-mdx-remote from v5 to v6. StepList, Checklist, and LessonObjectives showed wrapper shells and headings but no content items. No build error, no TypeScript error — the failure was completely silent at compile time. Only visible via browser inspection.
Affected: All MDX content pages — StepList, Checklist, LessonObjectives, ValidationPipeline
Time to resolve: 41 minutes
Root Cause
next-mdx-remote v6 introduced blockJS: true as a new default. This activates the removeJavaScriptExpressions remark plugin during MDX serialization, which strips all JavaScript expressions from MDX source — including array and object literals used as JSX props. Components receiving items={['a', 'b']} got undefined instead of the array. The change shipped as opt-out rather than opt-in.
Resolution
Set blockJS: false in the MDXRemote options object inside content-renderer.tsx. One line. MDX on this platform is author-controlled static content, not user-submitted input — the JavaScript expression restriction provides no security value here and breaks all components using structured props.
Prevention Pattern
When upgrading MDX-related packages: read the full changelog before deploying, test all custom components with real MDX content in a staging environment, and look specifically for new boolean options that default to restrictive behavior. For next-mdx-remote, explicitly set blockJS: false in all author-controlled contexts and document why at the call site.
Upgraded next-mdx-remote from v5 to v6 as part of a routine dependency update pass. The build compiled without errors. TypeScript passed. Vercel deployed the build successfully.
Then I opened a lesson page and saw that every <StepList> and <Checklist> component was rendering its wrapper shell but had no items inside. <LessonObjectives> showed the "What you'll build" header with an empty list below it. <ValidationPipeline> rendered the container div but nothing in it.
The symptom was visually obvious but the cause was completely non-obvious — because the build gave no indication anything was wrong.
next-mdx-remote v6 added a blockJS option. In v6, it defaults to true.
When blockJS: true is active, the removeJavaScriptExpressions remark plugin runs during MDX serialization. This plugin strips all JavaScript expression nodes from the MDX AST before rendering, including:
items={['step one', 'step two', 'step three']}config={{ theme: 'dark', compact: true }}count={items.length}, active={index === selected}The rationale in the v6 release notes: safety for platforms that process user-submitted MDX. If untrusted users can write MDX, you don't want arbitrary JavaScript executing. That's a legitimate security boundary for those use cases — a CMS or forum where users can write MDX content.
For a static site where the MDX is written by the developer and committed to a private repository, this restriction does nothing useful and silently breaks any component with structured props.
The silent failure made this more expensive to diagnose than a build error would have been. The components rendered — they just rendered empty. No exception, no console warning, no TypeScript error. The serialized MDX output was syntactically valid; it just had the prop values removed.
Step 1 — Inspect the component. Traced through StepList.tsx. The items prop was undefined inside the component. Not an empty array — undefined. The component was being called but receiving nothing.
Step 2 — Inspect the serialized MDX. Added temporary logging to the compileMDX call in content-renderer.tsx to print the serialized output. The JSX component tags were present in the output but the prop values were gone. <StepList items={undefined}> instead of <StepList items={['a','b','c']}>.
Step 3 — Check the changelog. Found blockJS in the next-mdx-remote v6 release notes:
blockJS— Defaults totruefor security. Set tofalseif your MDX content is trusted author-controlled content.
One option change, one default flip, no migration guide callout. 41 minutes of diagnostic work on what turned out to be a single-line fix.
⚠Why this class of failure is hard to catch
The failure signature — components that render shell structure but no content — looks like a data loading issue, not a build issue. First instinct is to check the MDX files and the component props. The serialization layer is further down the stack and easy to overlook when the build and TypeScript both report clean.
ℹThe fix — one line in content-renderer.tsx
const { content } = await compileMDX({
source,
components: allComponents,
options: {
parseFrontmatter: true,
// next-mdx-remote v6 defaults blockJS: true, which runs
// removeJavaScriptExpressions and strips all array/object
// literal props from JSX. Our MDX is author-controlled
// static content — opt out of this restriction explicitly.
blockJS: false,
},
})
Documenting the why at the call site prevents this from being "fixed" by a future developer who sees an unfamiliar option and removes it.
Built from real execution
Documented from the production deployment of ai-execution-lab on Vercel. Screenshots taken from the Vercel dashboard and live platform immediately following incident resolution on 2026-05-12.
The following reconstructs the relevant Vercel build sequence. The initial deployment post-upgrade completed successfully — the failure only surfaces at render time, not compile time.
The before/after shows the Vercel dashboard state: the build failure investigation on the left, all deployments restored to production-ready status on the right.
All Deployments Ready
Build FailureFull resolution screenshots from the incident. Click any image to expand. Use arrow keys to navigate.
A build error would have caught this at deploy time. Instead the failure path was:
This class of failure — a default behavioral change that produces syntactically valid but semantically empty output — requires visual inspection to catch. Standard automated checks miss it entirely:
undefined if not typed strictly)Only a human looking at a rendered page would notice that <StepList> has no steps. Or a test that asserts on rendered content rather than HTTP status. Visual regression testing would catch it; most CI pipelines don't run it.
Routine dependency update. next-mdx-remote 5 → 6. Build succeeded, TypeScript clean, deployed to Vercel production.
Opened a lesson page. StepList, Checklist, and LessonObjectives rendered wrapper elements but no content. No console errors.
Added temporary logging to the compiled MDX output. Confirmed that array and object literal props were missing from the serialized component calls. Props were undefined at render time.
Searched next-mdx-remote v6 release notes for breaking changes. Found blockJS: true as the new default with opt-out instructions.
Added blockJS: false to compileMDX options in content-renderer.tsx with explanatory comment. One line change.
Pushed fix, monitored Vercel build, verified all MDX components rendering with real content. Platform fully operational.
⚠Why next-mdx-remote v6 broke components
The v6 blockJS change was not a bug — it was a deliberate, documented security feature for platforms that serve user-submitted MDX. It shipped as opt-out rather than opt-in, which means any existing codebase upgrading from v5 inherits the restriction silently. The failure only manifests at render time, not at build time, because the serialization step produces syntactically valid JSX — just with stripped prop values.
Key pattern: Any library that processes developer-authored content with a new default restriction should ship the restriction as opt-in, not opt-out. The inverse — shipping security defaults that break existing behavior — requires a migration guide and a major-version callout, not just a changelog note.
ℹHow Vercel surfaced the issue
Vercel did not surface this failure proactively — the build completed with a success status. The production deployment went live. The platform reported no errors. This is correct behavior: Vercel cannot know that rendered content is semantically empty versus intentionally empty. The only signal was visual — opening a page and observing missing content.
Key pattern: Build system health and application correctness are different. A green build is a necessary condition for a working application; it is not sufficient. Visual inspection of rendered output after dependency upgrades is not optional.
✓Why blockJS: false fixed the execution path
Setting blockJS: false disables the removeJavaScriptExpressions remark plugin in the MDX serialization pipeline. With the plugin off, array and object literals in JSX props are preserved in the compiled output. items={['step one', 'step two']} reaches the component as an actual array. The fix is surgical — it doesn't change how MDX is processed in any other way, and it explicitly documents at the call site why the opt-out is intentional.
The security concern blockJS addresses — arbitrary JavaScript execution from untrusted MDX authors — does not apply here. All MDX on this platform is written by the developer, committed to a private repository, and built at deploy time. There is no user-submitted MDX surface.
⬡Why evidence-backed documentation matters
This incident report exists because silent failures are the hardest to diagnose the second time. Without documentation, this failure pattern repeats — a future developer sees blockJS: false in the config, doesn't know why it's there, removes it as dead code, and triggers the exact same 41-minute debugging session.
The screenshots provide proof that the incident is real and the resolution was verified in production — not reconstructed from memory or extrapolated from the changelog. Evidence-backed documentation is specifically useful for failure modes that leave no build artifact: no error log, no failed deployment, just empty components on a green build.
After any MDX-related package upgrade:
items={[...]} or config objectsfalse, write the reason at the call site so future developers understand it was intentionalFor next-mdx-remote specifically: if your MDX is author-controlled and committed to a private repository, always set blockJS: false explicitly. The behavior should be documented, not inherited from a default that can change across versions.