Your MDX Blog Will Throw EvalError on Cloudflare Workers. Here's the Fix.
Our bilingual product blog had a bug that never appeared locally, never appeared in next build, and turned every blog post into an HTTP 500 in production — on exactly one runtime. The error: EvalError: Code generation
Our bilingual product blog had a bug that never appeared locally, never appeared in
next build, and turned every blog post into an HTTP 500 in production — on exactly one
runtime. The error:
EvalError: Code generation from strings disallowed for this context
If you are shipping a Next.js app to Cloudflare Workers (we run
Christmas Photo AI on Workers via OpenNext) and your
content pipeline uses next-mdx-remote, this post is your future. Here is what happens,
why, and the surprisingly small fix.
The bug that only exists in production
The stack: Next.js 16 App Router, React 19, MDX posts rendered through
next-mdx-remote/rsc. Everything was green locally with next dev, green in CI, and the
build itself succeeded. Deployed to Workers, the home page, style pages, and pricing all
worked — but every single blog URL returned a 500.
That gap is the first lesson: the compile step that hurts you doesn't run where you
test. next build pre-renders what it can, but request-time rendering on a different
runtime is a different execution environment with different rules.
Why Workers disallow it
Cloudflare Workers run with V8 isolates configured to forbid dynamic code evaluation —
eval() and new Function(...) both throw EvalError. It is a security posture, not a
bug, and it will not change.
next-mdx-remote's whole job is turning MDX source into executable JavaScript at runtime.
Under the hood it compiles the MDX and evaluates the result as a function. On Node.js
that's fine. On Workers it is precisely the operation the platform forbids.
So the 500 was correct behavior. Our dependency was doing the one thing this runtime
refuses to do — at request time, in production only.
The audit that made the decision easy
Before choosing a replacement, we asked a simpler question: do we actually render any
MDX?
The answer was no. The posts were plain GFM — headings, paragraphs, lists, tables, links.
No imported components, no custom JSX inside the content. We were paying for a runtime
MDX compiler to render… markdown.
That reframed the fix from "find an MDX compiler that works on Workers" to "use a
markdown parser." Which is a much smaller problem.
The fix: marked + sanitize + one CSS block
We switched to marked — a pure parser, no code generation —
and ran its HTML output through a sanitizer before injecting it. Rough shape:
import { marked } from 'marked';
import { sanitizeHtml } from '@/lib/security/sanitize';
export function renderPostBody(markdown: string): string {
const raw = marked.parse(markdown, { async: false }) as string;
return sanitizeHtml(raw);
}
The component layer renders that with dangerouslySetInnerHTML, and a .blog-prose
style block (semantic tokens, table overflow handling for narrow screens) keeps the
typography looking like the rest of the site.
next-mdx-remote and remark-gfm left package.json the same day.
Three gotchas from the migration
1. Sanitizer allowlists eat attributes silently. Our sanitizer stripped unknown
attributes, and rel wasn't on the allowlist — so the transform adding
rel="noreferrer" to external links ran, and then the sanitizer stripped it right back
off. If you sanitize, audit the allowlist against the transforms you actually apply.
2. Soft 404s hide behind optimistic UI. While testing the migration we found retired
blog URLs were rendering the not-found page with a 200 status. The culprit was a
locale-level loading.tsx boundary that swallowed notFound() semantics. If a URL
should be gone, verify it returns a real 404 — not a friendly page with a 200.
3. Test on the runtime you ship. The entire class of bug disappears if your CI or a
pre-deploy step runs a smoke test on the actual Workers runtime (or wrangler dev with
the Workers-compatible mode). next dev on Node proves nothing about EvalError.
When you should keep MDX
To be clear: if your content genuinely embeds React components, MDX is the right tool,
and there are compile-at-build-time approaches that work on Workers. The mistake wasn't
using MDX — it was shipping a runtime compiler for content that never used the
compiler's features.
The rule we took away:
Ship a parser for content, a compiler only for code — and know which one your
dependency actually is.
The blog this fix landed on belongs to Christmas Photo AI
— turn one photo into a private AI Christmas portrait in about two minutes. First
portrait is free.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.