DEV Community

Cover image for Your MDX Blog Will Throw EvalError on Cloudflare Workers. Here's the Fix.
Mr.Rain
Mr.Rain

Posted on Edited on

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 from strings disallowed for this context
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)